Repository: kmyk/online-judge-tools
Pull request #239 (2019-01-04 12:36:09)

Fix generate output and add test
テストですがこういう感じでどうでしょうかというのと、早速バグが入っていたので修正しました。
方向性が良ければテストケース増やします。


Patch:
diff --git a/onlinejudge/implementation/command/generate_output.py b/onlinejudge/implementation/command/generate_output.py
index cdd0a73..16a5c1d 100644
--- a/onlinejudge/implementation/command/generate_output.py
+++ b/onlinejudge/implementation/command/generate_output.py
@@ -3,6 +3,7 @@ import onlinejudge
 import onlinejudge.implementation.utils as utils
 import onlinejudge.implementation.logging as log
 import onlinejudge.implementation.format_utils as cutils
+import os
 import time
 from typing import *
 if TYPE_CHECKING:
@@ -31,8 +32,14 @@ def generate_output(args: 'argparse.Namespace') -> None:
             log.info('skipped.')
             continue
         log.emit(log.bold(answer.decode().rstrip()))
-        name = cutils.match_with_format(args.format, it['in']).groupdict()['name']  # type: ignore
-        path = cutils.path_from_format(args.directory, args.format, name=name, ext='out')
+        match_result = cutils.match_with_format(args.directory, args.format, it['in'])  # type: Optional[Match[Any]]
+        if match_result is not None:
+            matched_name = match_result.groupdict()['name']  # type: str
+        else:
+            assert False
+        path = cutils.path_from_format(args.directory, args.format, name=matched_name, ext='out')
+        if not path.parent.is_dir():
+            os.makedirs(str(path.parent), exist_ok=True)
         with path.open('wb') as fh:
             fh.write(answer)
         log.success('saved to: %s', path)


Test patch:
diff --git a/tests/command_generate_output.py b/tests/command_generate_output.py
new file mode 100644
index 0000000..473f65d
--- /dev/null
+++ b/tests/command_generate_output.py
@@ -0,0 +1,137 @@
+import unittest
+
+import contextlib
+import glob
+import os
+import subprocess
+import sys
+import tempfile
+
+
+@contextlib.contextmanager
+def chdir(path):
+    cwd = os.getcwd()
+    try:
+        os.chdir(path)
+        yield
+    finally:
+        for file in glob.glob('*/*.out'):
+            os.remove(file)
+        os.chdir(cwd)
+
+def prepare_files(input_files):
+    for f in input_files:
+        if os.path.dirname(f['path']):
+            os.makedirs(os.path.dirname(f['path']), exist_ok=True)
+        with open(f['path'], 'w') as fh:
+            fh.write(f['data'])
+        if f.get('executable', False):
+            os.chmod(f['path'], 0o755)
+
+class GenerateOutputTest(unittest.TestCase):
+
+    def snippet_call_generate_output(self, args, input_files, expected_values, disallowed_files=None):
+        ojtools = os.path.abspath('oj')
+        with tempfile.TemporaryDirectory() as tempdir:
+            with chdir(tempdir):
+                prepare_files(input_files)
+                _ = subprocess.check_output([ojtools, 'generate-output'] + args, stderr=sys.stderr)
+                for expect in expected_values:
+                    with open(expect['path']) as f:
+                        self.assertEqual(''.join(f.readlines()), expect['data'])
+                if disallowed_files is not None:
+                    for file in disallowed_files:
+                        self.assertFalse(os.path.exists(file))
+
+    def test_call_generate_output_simple(self):
+        self.snippet_call_generate_output(
+            args=[ '-c', 'cat' ],
+            input_files=[
+                { 'path': 'test/sample-1.in', 'data': 'foo\n' },
+                { 'path': 'test/sample-2.in', 'data': 'bar\n' },
+            ],
+            expected_values=[
+                { 'path': 'test/sample-1.out', 'data': 'foo\n' },
+                { 'path': 'test/sample-2.out', 'data': 'bar\n' },
+            ],
+        )
+
+    def test_call_generate_output_select(self):
+        self.snippet_call_generate_output(
+            args=[ '-c', 'cat', 'test/sample-1.in', 'test/sample-2.in' ],
+            input_files=[
+                { 'path': 'test/sample-1.in', 'data': 'foo\n' },
+                { 'path': 'test/sample-2.in', 'data': 'bar\n' },
+                { 'path': 'test/sample-3.in', 'data': 'baz\n' },
+            ],
+            expected_values=[
+                { 'path': 'test/sample-1.out', 'data': 'foo\n' },
+                { 'path': 'test/sample-2.out', 'data': 'bar\n' },
+            ],
+            disallowed_files=['test/sample-3.out']
+        )
+
+    def test_call_generate_output_already_exists(self):
+        # Since sample-1.out already exists, sample-1.out will not be updated.
+        self.snippet_call_generate_output(
+            args=[ '-c', 'cat' ],
+            input_files=[
+                { 'path': 'test/sample-1.in', 'data': 'foo\n' },
+                { 'path': 'test/sample-1.out', 'data': 'bar\n' },
+            ],
+            expected_values=[
+                { 'path': 'test/sample-1.out', 'data': 'bar\n' },
+            ],
+        )
+
+    def test_call_generate_output_dir(self):
+        self.snippet_call_generate_output(
+            args=[ '-c', 'cat', '-d', 'p/o/../../p/o/y/o' ],
+            input_files=[
+                { 'path': 'p/o/y/o/sample-1.in', 'data': 'foo\n' },
+                { 'path': 'p/o/y/o/sample-2.in', 'data': 'bar\n' },
+            ],
+            expected_values=[
+                { 'path': 'p/o/y/o/sample-1.out', 'data': 'foo\n' },
+                { 'path': 'p/o/y/o/sample-2.out', 'data': 'bar\n' },
+            ],
+        )
+
+    def test_call_generate_output_format(self):
+        self.snippet_call_generate_output(
+            args=[ '-c', 'cat', '-d', 'yuki/coder', '-f', 'test_%e/%s' ],
+            input_files=[
+                { 'path': 'yuki/coder/test_in/sample-1.txt', 'data': 'foo\n' },
+                { 'path': 'yuki/coder/test_in/sample-2.txt', 'data': 'bar\n' },
+            ],
+            expected_values=[
+                { 'path': 'yuki/coder/test_out/sample-1.txt', 'data': 'foo\n' },
+                { 'path': 'yuki/coder/test_out/sample-2.txt', 'data': 'bar\n' },
+            ],
+        )
+
+    def test_call_generate_output_format_select(self):
+        self.snippet_call_generate_output(
+            args=[ '-c', 'cat', '-d', 'yuki/coder', '-f', 'test_%e/%s', 'yuki/coder/test_in/sample-2.txt', 'yuki/coder/test_in/sample-3.txt' ],
+            input_files=[
+                { 'path': 'yuki/coder/test_in/sample-2.txt', 'data': 'bar\n' },
+                { 'path': 'yuki/coder/test_in/sample-3.txt', 'data': 'baz\n' },
+            ],
+            expected_values=[
+                { 'path': 'yuki/coder/test_out/sample-2.txt', 'data': 'bar\n' },
+                { 'path': 'yuki/coder/test_out/sample-3.txt', 'data': 'baz\n' },
+            ],
+        )
+
+    def test_call_generate_output_format_hack(self):
+        self.snippet_call_generate_output(
+            args=[ '-c', 'cat', '-d', 'a/b', '-f', 'c/test_%e/d/%s/e.case.txt' ],
+            input_files=[
+                { 'path': 'a/b/c/test_in/d/sample.case.1/e.case.txt', 'data': 'foo\n' },
+                { 'path': 'a/b/c/test_in/d/sample.case.2/e.case.txt', 'data': 'bar\n' },
+            ],
+            expected_values=[
+                { 'path': 'a/b/c/test_out/d/sample.case.1/e.case.txt', 'data': 'foo\n' },
+                { 'path': 'a/b/c/test_out/d/sample.case.2/e.case.txt', 'data': 'bar\n' },
+            ],
+        )