Repository: online-judge-tools/oj

Issue:
`--display-mode=diff-all` ですべての行が表示されない
## Summary / 概要

`--display-mode=diff-all` ですべての行が表示されません。私の経験上、最終行のみが表示されません。最終行以外は合っていて最終行のみ間違えているときは WA がでるので表示のみの問題だと推測します。

## Steps to reproduce / 再現方法

1.  `oj t -M diff-all`

environments:

-   version: online-judge-tools 11.5.0 (+ online-judge-api-client 10.10.0)

## Expected behavior / 期待される挙動

`oj t -M diff` の出力です。
![image](https://user-images.githubusercontent.com/36944206/132996695-369a16d0-5503-4d1d-9db8-3d232ad85d03.png)

## Actual behavior / 実際の挙動

![image](https://user-images.githubusercontent.com/36944206/132996712-03571ac3-5a89-48e2-ab8e-62498b2fbcd4.png)

## Other notes / その他

- `--display-mode=all` の出力は正しそうです。
-   Will you try to create a pull request?
    -   no


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0827f0..7e1e212 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,5 @@
 # Change Log
 
-## 11.5.0 / 2021-09-09
-
--   [#900](https://github.com/online-judge-tools/oj/pull/900) allow `$ oj g/i --hack-actual ...` without `--hack-expected`
--   [#894](https://github.com/online-judge-tools/oj/pull/894) update `$ oj t --display-mode=diff`
--   [#892](https://github.com/online-judge-tools/oj/pull/892) implemente `$ oj t --display-mode=all`
-
 ## 11.4.0 / 2021-07-20
 
 -   [#886](https://github.com/online-judge-tools/oj/pull/886) add `test-interactive` subcommand as an alias of `test-reactive` subcommand ([@cai-lw](https://github.com/cai-lw))
diff --git a/onlinejudge_command/__about__.py b/onlinejudge_command/__about__.py
index 769f69c..9b6b618 100644
--- a/onlinejudge_command/__about__.py
+++ b/onlinejudge_command/__about__.py
@@ -3,6 +3,6 @@ __author__ = 'Kimiyuki Onaka'
 __email__ = 'kimiyuki95@gmail.com'
 __license__ = 'MIT License'
 __url__ = 'https://github.com/online-judge-tools/oj'
-__version_info__ = (11, 5, 0, 'final', 0)
+__version_info__ = (11, 4, 0, 'final', 0)
 __version__ = '.'.join(map(str, __version_info__[:3]))
 __description__ = 'CLI tool to solve problems of competitive programming'
diff --git a/onlinejudge_command/pretty_printers.py b/onlinejudge_command/pretty_printers.py
index 1c70780..3c89c26 100644
--- a/onlinejudge_command/pretty_printers.py
+++ b/onlinejudge_command/pretty_printers.py
@@ -651,7 +651,7 @@ def _tokenize_pretty_diff(output: str, *, expected: str, compare_mode: CompareMo
     ops = _make_diff_between_file_and_file(output, expected, compare_mode=compare_mode)
     merged_ops = _add_lines_around_diff_lines(output, expected, ops=ops, size=4)
     merged_ops = _add_dots_between_gaps(output, expected, ops=merged_ops)
-    tokens = _tokens_from_line_diff_ops(merged_ops[:limit], char_in_line=char_in_line)
+    tokens = _tokens_from_line_diff_ops(merged_ops[:limit] if limit != -1 else merged_ops, char_in_line=char_in_line)
     if limit != -1:
         tokens += _summary_token_of_diff_ops(merged_ops[limit:])
     return tokens
diff --git a/onlinejudge_command/subcommand/generate_input.py b/onlinejudge_command/subcommand/generate_input.py
index 2aa4749..4c9f336 100644
--- a/onlinejudge_command/subcommand/generate_input.py
+++ b/onlinejudge_command/subcommand/generate_input.py
@@ -43,7 +43,7 @@ tips:
     subparser.add_argument('--width', type=int, default=3, help='specify the width of indices of cases. (default: 3)')
     subparser.add_argument('--name', help='specify the base name of cases. (default: "random")')
     subparser.add_argument('-c', '--command', help='specify your solution to generate output')
-    subparser.add_argument('--hack-expected', dest='command', help='alias of --command. If this is not given, --hack runs until the actual solution fails with RE or TLE.')
+    subparser.add_argument('--hack-expected', dest='command', help='alias of --command')
     subparser.add_argument('--hack', '--hack-actual', dest='hack', help='specify your wrong solution to be compared with the reference solution given by --hack-expected')
     subparser.add_argument('generator', type=str, help='your program to generate test cases')
     subparser.add_argument('count', nargs='?', type=int, help='the number of cases to generate (default: 100)')
@@ -93,20 +93,14 @@ def write_result(input_data: bytes, output_data: Optional[bytes], *, input_path:
             logger.info(utils.SUCCESS + 'saved to: %s', output_path)
 
 
-def check_status(info: Dict[str, Any], proc: subprocess.Popen, *, submit: Callable[..., None], input_data: Optional[bytes]) -> bool:
+def check_status(info: Dict[str, Any], proc: subprocess.Popen, *, submit: Callable[..., None]) -> bool:
     submit(logger.info, 'time: %f sec', info['elapsed'])
     if proc.returncode is None:
         submit(logger.info, utils.FAILURE + utils.red('TLE'))
-        if input_data is not None:
-            submit(logger.info, utils.NO_HEADER + 'input:')
-            submit(logger.info, utils.NO_HEADER + '%s', pretty_printers.make_pretty_large_file_content(input_data, limit=40, head=20, tail=10))
         submit(logger.info, 'skipped.')
         return False
     elif proc.returncode != 0:
         submit(logger.info, utils.FAILURE + utils.red('RE') + ': return code %d', proc.returncode)
-        if input_data is not None:
-            submit(logger.info, utils.NO_HEADER + 'input:')
-            submit(logger.info, utils.NO_HEADER + '%s', pretty_printers.make_pretty_large_file_content(input_data, limit=40, head=20, tail=10))
         submit(logger.info, 'skipped.')
         return False
     assert info['answer'] is not None
@@ -148,7 +142,7 @@ def generate_input_single_case(generator: str, *, input_path: pathlib.Path, outp
         submit(logger.info, 'generate input...')
         info, proc = utils.exec_command(generator, timeout=tle)
         input_data: bytes = info['answer']
-        if not check_status(info, proc, submit=submit, input_data=input_data):
+        if not check_status(info, proc, submit=submit):
             return
 
         # check the randomness of generator
@@ -163,7 +157,7 @@ def generate_input_single_case(generator: str, *, input_path: pathlib.Path, outp
             submit(logger.info, 'generate output...')
             info, proc = utils.exec_command(command, input=input_data, timeout=tle)
             output_data = info['answer']
-            if not check_status(info, proc, submit=submit, input_data=input_data):
+            if not check_status(info, proc, submit=submit):
                 return
 
         # write result
@@ -179,7 +173,7 @@ def simple_match(a: str, b: str) -> bool:
     return False
 
 
-def try_hack_once(generator: str, command: Optional[str], hack: str, *, tle: Optional[float], attempt: int, lock: Optional[threading.Lock] = None, generated_input_hashes: Dict[bytes, str]) -> Optional[Tuple[bytes, Optional[bytes]]]:
+def try_hack_once(generator: str, command: str, hack: str, *, tle: Optional[float], attempt: int, lock: Optional[threading.Lock] = None, generated_input_hashes: Dict[bytes, str]) -> Optional[Tuple[bytes, bytes]]:
     with BufferedExecutor(lock) as submit:
 
         # print the header
@@ -190,7 +184,7 @@ def try_hack_once(generator: str, command: Optional[str], hack: str, *, tle: Opt
         submit(logger.info, 'generate input...')
         info, proc = utils.exec_command(generator, stdin=None, timeout=tle)
         input_data: Optional[bytes] = info['answer']
-        if not check_status(info, proc, submit=submit, input_data=input_data):
+        if not check_status(info, proc, submit=submit):
             return None
         assert input_data is not None
 
@@ -202,15 +196,13 @@ def try_hack_once(generator: str, command: Optional[str], hack: str, *, tle: Opt
             submit(logger.info, utils.NO_HEADER + 'input:')
             submit(logger.info, utils.NO_HEADER + '%s', pretty_printers.make_pretty_large_file_content(input_data, limit=40, head=20, tail=10))
 
-        # generate expected output
-        output_data: Optional[bytes] = None
-        if command is not None:
-            submit(logger.info, 'generate output...')
-            info, proc = utils.exec_command(command, input=input_data, timeout=tle)
-            output_data = info['answer']
-            if not check_status(info, proc, submit=submit, input_data=input_data):
-                return None
-            assert output_data is not None
+        # generate output
+        submit(logger.info, 'generate output...')
+        info, proc = utils.exec_command(command, input=input_data, timeout=tle)
+        output_data: Optional[bytes] = info['answer']
+        if not check_status(info, proc, submit=submit):
+            return None
+        assert output_data is not None
 
         # hack
         submit(logger.info, 'hack...')
@@ -225,14 +217,13 @@ def try_hack_once(generator: str, command: Optional[str], hack: str, *, tle: Opt
         elif proc.returncode != 0:
             logger.info(utils.FAILURE + '' + utils.red('RE') + ': return code %d', proc.returncode)
             status = 'RE'
-        if output_data is not None:
-            expected = output_data.decode()
-            if not simple_match(answer, expected):
-                logger.info(utils.FAILURE + '' + utils.red('WA'))
-                logger.info(utils.NO_HEADER + 'input:\n%s', pretty_printers.make_pretty_large_file_content(input_data, limit=40, head=20, tail=10))
-                logger.info(utils.NO_HEADER + 'output:\n%s', pretty_printers.make_pretty_large_file_content(answer.encode(), limit=40, head=20, tail=10))
-                logger.info(utils.NO_HEADER + 'expected:\n%s', pretty_printers.make_pretty_large_file_content(output_data, limit=40, head=20, tail=10))
-                status = 'WA'
+        expected = output_data.decode()
+        if not simple_match(answer, expected):
+            logger.info(utils.FAILURE + '' + utils.red('WA'))
+            logger.info(utils.NO_HEADER + 'input:\n%s', pretty_printers.make_pretty_large_file_content(input_data, limit=40, head=20, tail=10))
+            logger.info(utils.NO_HEADER + 'output:\n%s', pretty_printers.make_pretty_large_file_content(answer.encode(), limit=40, head=20, tail=10))
+            logger.info(utils.NO_HEADER + 'expected:\n%s', pretty_printers.make_pretty_large_file_content(output_data, limit=40, head=20, tail=10))
+            status = 'WA'
 
         if status == 'AC':
             return None
@@ -242,7 +233,7 @@ def try_hack_once(generator: str, command: Optional[str], hack: str, *, tle: Opt
 
 def run(args: argparse.Namespace) -> None:
     if args.hack and not args.command:
-        logger.info('--hack-actual is given but --hack-expected is not given. It will run until the actual solution gets RE or TLE.')
+        raise RuntimeError('--hack must be used with --command')
 
     if args.name is None:
         if args.hack:


Test patch:
diff --git a/tests/command_generate_input.py b/tests/command_generate_input.py
index 1c88158..8c7e5f3 100644
--- a/tests/command_generate_input.py
+++ b/tests/command_generate_input.py
@@ -114,23 +114,6 @@ class GenerateInputTest(unittest.TestCase):
             },
         ])
 
-    def test_call_generate_input_hack_with_re(self):
-        self.snippet_call_generate_input(args=[tests.utils.python_script('generate.py'), '--hack-actual', tests.utils.python_script('err.py'), '-j', '4'], input_files=[
-            {
-                'path': 'generate.py',
-                'data': 'import random\nprint(random.randint(0, 10))\n'
-            },
-            {
-                'path': 'err.py',
-                'data': 'n = int(input())\nassert n != 0\nprint(n)\n'
-            },
-        ], expected_values=[
-            {
-                'path': 'test/hack-000.in',
-                'data': '0\n'
-            },
-        ])
-
     def test_call_generate_input_failure(self):
         self.snippet_call_generate_input(
             args=[tests.utils.python_script('generate.py'), '3'],
diff --git a/tests/pretty_printers.py b/tests/pretty_printers.py
index d9c86be..996fcbc 100644
--- a/tests/pretty_printers.py
+++ b/tests/pretty_printers.py
@@ -455,3 +455,45 @@ class MakePrettyDiffTest(unittest.TestCase):
         tokens = _tokenize_pretty_diff(a, expected=b, compare_mode=compare_mode, char_in_line=char_in_line, limit=limit)
         actual = _render_tokens(tokens=tokens, font_dim=font_dim, font_bold=font_bold, font_red=font_red, font_blue=font_blue)
         self.assertEqual(actual, expected)
+
+
+class MakePrettyDiffLimitTest(unittest.TestCase):
+    def test_with_limit(self) -> None:
+        a = ''.join([
+            'a\n',
+        ] * 100)
+        b = ''.join([
+            'b\n',
+        ] * 100)
+        compare_mode = CompareMode.CRLF_INSENSITIVE_EXACT_MATCH
+        char_in_line = 40
+        limit = 40
+        expected = 1 + limit + 1
+
+        font_dim = lambda s: s
+        font_bold = lambda s: s
+        font_red = lambda s: s
+        font_blue = lambda s: s
+        tokens = _tokenize_pretty_diff(a, expected=b, compare_mode=compare_mode, char_in_line=char_in_line, limit=limit)
+        actual = _render_tokens(tokens=tokens, font_dim=font_dim, font_bold=font_bold, font_red=font_red, font_blue=font_blue)
+        self.assertEqual(len(actual.splitlines()), expected)
+
+    def test_without_limit(self) -> None:
+        a = ''.join([
+            'a\n',
+        ] * 100)
+        b = ''.join([
+            'b\n',
+        ] * 100)
+        compare_mode = CompareMode.CRLF_INSENSITIVE_EXACT_MATCH
+        char_in_line = 40
+        limit = -1
+        expected = 1 + 100
+
+        font_dim = lambda s: s
+        font_bold = lambda s: s
+        font_red = lambda s: s
+        font_blue = lambda s: s
+        tokens = _tokenize_pretty_diff(a, expected=b, compare_mode=compare_mode, char_in_line=char_in_line, limit=limit)
+        actual = _render_tokens(tokens=tokens, font_dim=font_dim, font_bold=font_bold, font_red=font_red, font_blue=font_blue)
+        self.assertEqual(len(actual.splitlines()), expected)