From 636ffdea9bd61d4993d315901dfb3e5734a15d45 Mon Sep 17 00:00:00 2001 From: ste1hi <49638961+ste1hi@users.noreply.github.com> Date: Thu, 9 May 2024 11:37:46 +0800 Subject: [PATCH] Add auto delete freopen (#15) --- .gitignore | 3 +- OiRunner/BetterRunner.py | 82 ++++++--- docs/automation.md | 31 +++- pyproject.toml | 5 +- setup.py | 2 +- tests/data/test_freopen/test_freopen.cpp.bak | 13 ++ tests/test_basic.py | 175 ++++++------------- tests/test_func.py | 79 +++++++++ tests/util.py | 12 ++ 9 files changed, 255 insertions(+), 147 deletions(-) create mode 100644 tests/data/test_freopen/test_freopen.cpp.bak create mode 100644 tests/test_func.py create mode 100644 tests/util.py diff --git a/.gitignore b/.gitignore index d3346c9..f5839e4 100755 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,5 @@ cython_debug/ #.idea/ token -*.html \ No newline at end of file +*.html +.vscode \ No newline at end of file diff --git a/OiRunner/BetterRunner.py b/OiRunner/BetterRunner.py index c1a77f1..1b93a4d 100755 --- a/OiRunner/BetterRunner.py +++ b/OiRunner/BetterRunner.py @@ -3,6 +3,7 @@ import argparse import sys import os +import re import shutil import time from typing import Optional @@ -23,10 +24,10 @@ def _modify_file(self, file_name: str, file_type: str) -> int: file_type -- The filename extension of input file. Returns: - i -- Number of output files. + i -- Number of output files. Raise: - SystemExit -- The file is empty. + SystemExit -- The file is empty. ''' i = 1 a = "" @@ -35,6 +36,7 @@ def _modify_file(self, file_name: str, file_type: str) -> int: os.mkdir("~tmp") with open(file_name, "r") as f: for line in f: + file_path = os.path.join("~tmp", f"{i}.{file_type}") flag = 1 if line.rstrip(): a += f"{line}" @@ -43,8 +45,8 @@ def _modify_file(self, file_name: str, file_type: str) -> int: print(f"error:{file_name} is empty.") shutil.rmtree("~tmp") sys.exit() - with open(f"~tmp/{i}.{file_type}", "w") as _f: - _f.write(a) + with open(file_path, "w") as f: + f.write(a) i += 1 a = "" if not flag: @@ -53,8 +55,9 @@ def _modify_file(self, file_name: str, file_type: str) -> int: sys.exit() if a: - with open(f"~tmp/{i}.{file_type}", "w") as _f: - _f.write(a) + file_path = os.path.join("~tmp", f"{i}.{file_type}") + with open(file_path, "w") as f: + f.write(a) return i def _output(self, num: int, opt_file: str) -> None: @@ -62,18 +65,42 @@ def _output(self, num: int, opt_file: str) -> None: Merge and output the split files. Args: - num -- The number of files to merge. - opt_file -- Output file name. + num -- The number of files to merge. + + opt_file -- Output file name. ''' a = "" for file_num in range(1, num+1): a += f"#{file_num}:\n" - with open(f"~tmp/{file_num}.out", "r") as _f: - for line in _f: + out_file = os.path.join("~tmp", f"{file_num}.out") + with open(out_file, "r") as f: + for line in f: a += f"{line}" - with open(opt_file, "w") as _out: - _out.write(a) + with open(opt_file, "w") as out: + out.write(a) + + def delete_freopen(self, path: str) -> None: + ''' + Delete freopen command. + + Args: + path -- The cpp file path. + ''' + if not os.path.exists(path): + raise ValueError("File not exists.") + + with open(path, "r") as file: + content = file.read() + + back_file_path = os.path.join(os.path.dirname(path), os.path.basename(path) + ".bak") + shutil.copy2(path, 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) class BetterRunner: @@ -94,6 +121,7 @@ def cmd_parse(self) -> None: 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("--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).") @@ -111,7 +139,7 @@ def compile(self) -> None: Compile files and generate executable files. Raise: - SystemExit -- Compilation failed. + SystemExit -- Compilation failed. ''' try: compile = sp.Popen(["g++", self.args.filename + ".cpp", "-g", "-o", self.args.name]) @@ -133,18 +161,20 @@ def _check(self, opt_file: str, ipt_file: str, ans_file: str, Local evaluation and get results. Args: - opt_file -- Output file name. + opt_file -- Output file name. + + ipt_file -- Input file name. - ipt_file -- Input file name. + ans_file -- Answer file name. - ans_file -- Answer file name. + file_num -- File number. - file_num -- File number. - run_file -- Executable file name (None means use the value of the command line parameter). - if_print -- Whether to output (None means use the value of the command line parameter). + run_file -- Executable file name (None means use the value of the command line parameter). + + if_print -- Whether to output (None means use the value of the command line parameter). Return: - if_pass -- Whether to pass the test. + if_pass -- Whether to pass the test. ''' print(f"#{file_num}:") @@ -204,6 +234,9 @@ def run(self) -> None: gdb.wait() sys.exit() + if not self.args.judge and self.args.freopen: + self.func.delete_freopen(self.args.filename + ".cpp") + if self.args.judge: flag = 0 @@ -213,8 +246,10 @@ def run(self) -> None: i = self.func._modify_file(self.answer_file, "ans") for file_num in range(1, i + 1): - judge = self._check(f"~tmp/{file_num}.out", f"~tmp/{file_num}.in", f"~tmp/{file_num}.ans", - file_num) + out_file = os.path.join("~tmp", f"{file_num}.out") + in_file = os.path.join("~tmp", f"{file_num}.in") + ans_file = os.path.join("~tmp", f"{file_num}.ans") + judge = self._check(out_file, in_file, ans_file, file_num) if not judge: flag += 1 @@ -222,6 +257,9 @@ def run(self) -> None: self.func._output(i, self.output_file) shutil.rmtree("~tmp") + if flag == 0 and self.args.freopen: + self.func.delete_freopen(self.args.filename + ".cpp") + if self.args.gdb and flag > 0: gdb = sp.Popen(["gdb", self.args.name]) gdb.wait() diff --git a/docs/automation.md b/docs/automation.md index 9e3e589..63b31d2 100644 --- a/docs/automation.md +++ b/docs/automation.md @@ -17,4 +17,33 @@ When this feature is enabled, the program will get input from standard input sto The default output file is `out.txt`.You can change it by `--outputfile` flag. [See details](judge.md#file-directory-structure) ## Auto gdb -Add `-d` or `--directgdb` will start gdb to debug the program directly. \ No newline at end of file +Add `-d` or `--directgdb` will start gdb to debug the program directly. + +## Auto delete freopen command +Add `-f` of `--freopen` to enable auto delete `freopen` command. + +> [!WARNING] +> This feature will modify your program file in disk. Use this flag carefully, though you have a backup file. + +During modification, the backup file will be created in the same directory, named `.bak`. + +This feature only support the freopen function call that is directly written in the `main()`. + +The form like below is supported: +```c++ +freopen + ("file name" , "r" , stdin), // Supports line breaks and blanks. + +freopen("file name", "w", stdout); +``` +The form like below is unsupported: +```c++ +#define fre(file_name) freopen(file_name, "w", stdout) // Presented in a #define form. + +void fre(char file_name){ + freopen(file_name, "w", stdout); // Nested function call. +} +``` +When your code contains unsupported form, use `-f` flag might lead to unexpected outcomes. + +When use this flag without `-j`, the package will modify your program file directly.With `-j`, the package will modify your program file after all test cases were passed. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ff12850..8df55d7 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "OiRunner" authors = [{name = "ste1", email = "1874076121@qq.com"}] description = "This package is designed to help oier compile the cpp file conveniently." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.6" license = {file = "LICENSE"} dynamic = ["version"] classifiers = [ @@ -17,5 +17,8 @@ classifiers = [ "Operating System :: Microsoft :: Windows", ] +[project.scripts] +oirun = "OiRunner.BetterRunner:main" + [tool.setuptools.dynamic] version = {attr = "OiRunner.__version__"} \ No newline at end of file diff --git a/setup.py b/setup.py index 96f01bf..456db36 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ description = 'This package is designed to help oier compile the cpp file conveniently.', url = 'https://github.com/ste1hi/OiRunner', packages = find_packages(), - python_requires = '>=3.8', + python_requires = '>=3.6', include_package_data = True, entry_points={ 'console_scripts': [ diff --git a/tests/data/test_freopen/test_freopen.cpp.bak b/tests/data/test_freopen/test_freopen.cpp.bak new file mode 100644 index 0000000..8991916 --- /dev/null +++ b/tests/data/test_freopen/test_freopen.cpp.bak @@ -0,0 +1,13 @@ +#include + +using namespace std; + +int main(){ + freopen + ("in", "r" , stdin), + freopen("out" , "w", stdout); + + int a = 1; + cin >> a; + cout << a; +} diff --git a/tests/test_basic.py b/tests/test_basic.py index 645fd86..0de49e8 100755 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -2,19 +2,9 @@ import unittest import os import sys -import shutil from unittest import mock from OiRunner import BetterRunner - -PY_FILE_PATH = "../../OiRunner/BetterRunner.py" -FILEOUT = "#1:\nfile1\n#2:\nfile2\n#3:\nfile3\n" -GARBAGE = ["a.out", "a.exe", "~temp", "~err_temp", "~out"] - - -def clean(filelist): - for filename in filelist: - if os.path.exists(filename): - os.remove(filename) +from .util import GARBAGE, clean class TestRunner(unittest.TestCase): @@ -22,7 +12,7 @@ class TestRunner(unittest.TestCase): @classmethod def setUpClass(self): if os.getcwd().split(os.sep)[-1] != "data": - os.chdir("tests/data") + os.chdir(os.path.join("tests", "data")) def setUp(self): self.runner = BetterRunner.BetterRunner() @@ -151,160 +141,79 @@ def test_long_parser(self): self.assertTrue(self.runner.args.onlyoutput) -class TestFunction(unittest.TestCase): - - def setUp(self): - self.func = BetterRunner.Functions() - if os.getcwd().split(os.sep)[-1] != "data": - os.chdir("tests/data") - - def tearDown(self): - clean(GARBAGE) - - 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 - 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 - - ret_code = self.func._modify_file("a.in", "in") - self.assertEqual(ret_code, 3) - self.assertTrue(os.path.exists("~tmp")) - for i in range(1, 4): - self.assertTrue(os.path.exists(f"~tmp{os.sep}{i}.in")) - for i in range(1, 4): - with open(f"~tmp{os.sep}{i}.in", "r") as f: - self.assertEqual(f.read(), f"file{i}\n") - - def test_output(self): - for i in range(1, 4): - os.rename(f"~tmp{os.sep}{i}.in", f"~tmp{os.sep}{i}.out") - self.func._output(3, "~out") - - with open("~out", "r") as f: - self.assertEqual(f.read(), FILEOUT) - - shutil.rmtree("~tmp") - - class TestCheck(unittest.TestCase): def setUp(self): self.runner = BetterRunner.BetterRunner() if os.getcwd().split(os.sep)[-1] != "data": - os.chdir("tests/data") + os.chdir(os.path.join("tests", "data")) def tearDown(self): clean(GARBAGE) - def test_pass(self): + def run_check(self, filename, if_print=False, if_pass=True): out = sys.stdout + in_file = os.path.join("check_data", f"{filename}.in") + out_file = os.path.join("check_data", f"{filename}.out") + ans_file = os.path.join("check_data", f"{filename}.ans") with open("~temp", "w") as f: sys.stdout = f sys.argv = ["BetterRunner.py", "test"] self.runner.cmd_parse() self.runner.args.if_print = False if sys.platform == "win32": - self.runner.args.name = "../../tests/data/check.exe" + self.runner.args.name = os.path.join("..", "..", "tests", "data", "check.exe") else: - self.runner.args.name = "../../tests/data/check.out" + self.runner.args.name = os.path.join("..", "..", "tests", "data", "check.out") - if_pass = self.runner._check("check_data/1.out", "check_data/1.in", - "check_data/1.ans", 1) - self.assertTrue(if_pass) + if not if_print: + passed = self.runner._check(out_file, in_file, ans_file, 1) + else: + passed = self.runner._check(out_file, in_file, ans_file, 1, if_print=if_print) + + if if_pass: + self.assertTrue(passed) + else: + self.assertFalse(passed) sys.stdout = out + def test_pass(self): + self.run_check("1") + with open("~temp", "r") as f: self.assertEqual(f.read(), "#1:\nCorrect answer.\n") os.remove("~temp") - with open("~temp", "w") as f: - sys.stdout = f - self.runner.args.if_print = True - self.runner._check("check_data/1.out", "check_data/1.in", "check_data/1.ans", 1, if_print=True) - - sys.stdout = out + self.run_check("1", True) with open("~temp", "r") as f: # We don't know the exact running time. self.assertIn("Correct answer, takes", f.read()) def test_retval(self): - out = sys.stdout - with open("~temp", "w") as f: - sys.stdout = f - sys.argv = ["BetterRunner.py", "test"] - self.runner.cmd_parse() - if sys.platform == "win32": - self.runner.args.name = "../../tests/data/check.exe" - else: - self.runner.args.name = "../../tests/data/check.out" - - self.runner._check("check_data/3.out", "check_data/3.in", "check_data/3.ans", 1) + self.run_check("3", if_pass=False) - sys.stdout = out with open("~temp", "r") as f: self.assertIn("#1:\nThe return value is 1. There may be issues with the program running.\n", f.read()) def test_fail(self): - out = sys.stdout - with open("~temp", "w") as f: - sys.stdout = f - sys.argv = ["BetterRunner.py", "test"] - self.runner.cmd_parse() - self.runner.args.if_print = False - if sys.platform == "win32": - self.runner.args.name = "../../tests/data/check.exe" - else: - self.runner.args.name = "../../tests/data/check.out" - - if_pass = self.runner._check("check_data/2.out", "check_data/2.in", - "check_data/2.ans", 1) - self.assertFalse(if_pass) - - sys.stdout = out + self.run_check("2", if_pass=False) with open("~temp", "r") as f: self.assertEqual(f.read(), "#1:\nWrong answer.\n") os.remove("~temp") - with open("~temp", "w") as f: - sys.stdout = f - self.runner.args.if_print = True - self.runner._check("check_data/2.out", "check_data/2.in", "check_data/2.ans", 1, if_print=True) - - sys.stdout = out + self.run_check("2", True, False) with open("~temp", "r") as f: self.assertEqual("#1:\nStandard answer:['wrong_answer']\nYour answer:['2']\n" "Wrong answer.\nError data:\n2\n\n", f.read()) def test_large(self): - out = sys.stdout - with open("~temp", "w") as f: - sys.stdout = f - sys.argv = ["BetterRunner.py", "test"] - self.runner.cmd_parse() - self.runner.args.if_print = True - if sys.platform == "win32": - self.runner.args.name = "../../tests/data/check.exe" - else: - self.runner.args.name = "../../tests/data/check.out" - - if_pass = self.runner._check("check_data/large.out", "check_data/large.in", - "check_data/large.ans", 1, if_print=True) - self.assertFalse(if_pass) - - sys.stdout = out + self.run_check("large", True, False) with open("~temp", "r") as f: output = f.read() @@ -317,7 +226,7 @@ class Testrun(unittest.TestCase): def setUp(self): self.runner = BetterRunner.BetterRunner() if os.getcwd().split(os.sep)[-1] != "data": - os.chdir("tests/data") + os.chdir(os.path.join("tests", "data")) def tearDown(self): clean(GARBAGE) @@ -340,7 +249,7 @@ def test_not_judge(self): sys.argv = ["BetterRunner.py", "test"] self.runner.cmd_parse() self.runner.args.onlyinput = True - self.runner.input_file = "../../tests/data/check_data/1.out" + self.runner.input_file = os.path.join("..", "..", "tests", "data", "check_data", "1.out") def wait(): print("1") @@ -359,7 +268,7 @@ def wait(): self.runner.args.onlyinput = False self.runner.args.onlyoutput = True - self.runner.output_file = "../../tests/data/check_data/1.out" + self.runner.output_file = os.path.join("..", "..", "tests", "data", "check_data", "1.out") os.remove("~temp") @@ -442,13 +351,37 @@ def mock_new_dir(a, b): mock_sp.assert_called_once_with(["gdb", "test"]) sys.stdout = out + @mock.patch("OiRunner.BetterRunner.Functions.delete_freopen") + @mock.patch("subprocess.Popen") + def test_call_delete_freopen(self, mock_sp, mock_delete_freopen): + sys.argv = ["BetterRunner.py", "test"] + self.runner.cmd_parse() + self.runner.args.judge = False + self.runner.args.freopen = True + + out = sys.stdout + sys.stdout = None + self.runner.run() + + def mock_new_dir(a, b): + os.mkdir("~tmp") + + self.runner.args.judge = True + with mock.patch("OiRunner.BetterRunner.Functions._modify_file", return_value=3): + with mock.patch("OiRunner.BetterRunner.Functions._output", side_effect=mock_new_dir): + with mock.patch("OiRunner.BetterRunner.BetterRunner._check", return_value=True): + self.runner.run() + self.assertEqual(mock_delete_freopen.call_count, 2) + mock_delete_freopen.assert_called_with("test.cpp") + sys.stdout = out + class TestMisc(unittest.TestCase): @mock.patch("OiRunner.BetterRunner.BetterRunner.cmd_parse") @mock.patch("OiRunner.BetterRunner.BetterRunner.compile") @mock.patch("OiRunner.BetterRunner.BetterRunner.run") - def test_main_call(self, mock_parse, mock_compile, mock_run): + def test_main_call(self, mock_run, mock_compile, mock_parse): BetterRunner.main() mock_compile.assert_called_once() mock_parse.assert_called_once() diff --git a/tests/test_func.py b/tests/test_func.py new file mode 100644 index 0000000..dea8d45 --- /dev/null +++ b/tests/test_func.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +import unittest +import os +import sys +import shutil +from OiRunner import BetterRunner +from .util import GARBAGE, FILEOUT, FREOPEN, clean + + +class TestFunction(unittest.TestCase): + + def setUp(self): + self.func = BetterRunner.Functions() + if os.getcwd().split(os.sep)[-1] != "data": + os.chdir(os.path.join("tests", "data")) + + def tearDown(self): + clean(GARBAGE) + + 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 + 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 + + ret_code = self.func._modify_file("a.in", "in") + self.assertEqual(ret_code, 3) + self.assertTrue(os.path.exists("~tmp")) + + for i in range(1, 4): + file_path = os.path.join("~tmp", f"{i}.in") + self.assertEqual(file_path, f"~tmp{os.sep}{i}.in") + + self.assertTrue(os.path.exists(file_path)) + + for i in range(1, 4): + file_path = os.path.join("~tmp", f"{i}.in") + with open(file_path, "r") as f: + self.assertEqual(f.read(), f"file{i}\n") + + def test_output(self): + for i in range(1, 4): + in_file = os.path.join("~tmp", f"{i}.in") + out_file = os.path.join("~tmp", f"{i}.out") + + os.rename(in_file, out_file) + self.func._output(3, "~out") + + with open("~out", "r") as f: + self.assertEqual(f.read(), FILEOUT) + + shutil.rmtree("~tmp") + + def test_delete_freopen(self): + origin_path = os.path.join("test_freopen", "test_freopen.cpp.bak") + file_path = os.path.join("test_freopen", "test.cpp") + back_path = os.path.join("test_freopen", "test.cpp.bak") + shutil.copy2(origin_path, file_path) + with open(file_path, "r") as f: + self.assertIn(FREOPEN, f.read()) + + self.func.delete_freopen(file_path) + + self.assertTrue(os.path.exists(back_path)) + with open(file_path, "r") as f: + self.assertNotIn(FREOPEN, f.read()) + with open(back_path, "r") as f: + self.assertIn(FREOPEN, f.read()) + + with self.assertRaises(ValueError): + self.func.delete_freopen("Not exists.") + + clean([file_path, back_path]) diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 0000000..d2ae9de --- /dev/null +++ b/tests/util.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +import os + +FILEOUT = "#1:\nfile1\n#2:\nfile2\n#3:\nfile3\n" +GARBAGE = ["a.out", "a.exe", "~temp", "~err_temp", "~out", ] +FREOPEN = 'freopen \n ("in", "r" , stdin),\n freopen("out" , "w", stdout);' + + +def clean(filelist): + for filename in filelist: + if os.path.exists(filename): + os.remove(filename)