Repository: online-judge-tools/api-client

Issue:
Remove trailing spaces from sample cases of Codeforces
example: https://codeforces.com/contest/1334/problem/D

``` console
$ oj --version                      
online-judge-tools 9.2.0

$ oj d -n https://codeforces.com/contest/1334/problem/D
[x] problem recognized: CodeforcesProblem.from_url('https://codeforces.com/contest/1334/problem/D'): https://codeforces.com/contest/1334/problem/D
[x] load cookie from: /home/user/.local/share/online-judge-tools/cookie.jar
[x] GET: https://codeforces.com/contest/1334/problem/D
[x] 200 OK
[x] save cookie to: /home/user/.local/share/online-judge-tools/cookie.jar

[*] sample 0
[x] input: sample-1
3
2 1 3
3 3 6
99995 9998900031 9998900031

[x] output: sample-1
1 2 1_(trailing spaces)
1 3 2 3_(trailing spaces)
1_(trailing spaces)

```

Comments:

Patch:
diff --git a/onlinejudge/_implementation/utils.py b/onlinejudge/_implementation/utils.py
index f0cbc18..eb25a74 100644
--- a/onlinejudge/_implementation/utils.py
+++ b/onlinejudge/_implementation/utils.py
@@ -46,11 +46,14 @@ def next_sibling_tag(tag: bs4.Tag) -> bs4.Tag:
     return tag
 
 
-# remove all HTML tag without interpretation (except <br>)
-# remove all comment
-# using DFS(Depth First Search)
-# discussed in https://github.com/kmyk/online-judge-tools/issues/553
+# TODO: Why this returns bs4.NavigableString?
 def parse_content(parent: Union[bs4.NavigableString, bs4.Tag, bs4.Comment]) -> bs4.NavigableString:
+    """parse_content convert a tag to a string with interpretting `<br>` and ignoring other tags.
+
+    .. seealso::
+        https://github.com/kmyk/online-judge-tools/issues/553
+    """
+
     res = ''
     if isinstance(parent, bs4.Comment):
         pass
@@ -104,10 +107,21 @@ class FormSender:
 
 
 def dos2unix(s: str) -> str:
+    """
+    .. deprecated:: 10.1.0
+        Use :func:`format_sample_case` instead.
+    """
+
     return s.replace('\r\n', '\n')
 
 
-def textfile(s: str) -> str:  # should have trailing newline
+def textfile(s: str) -> str:
+    """textfile convert a string s to the "text file" defined in POSIX
+
+    .. deprecated:: 10.1.0
+        Use :func:`format_sample_case` instead.
+    """
+
     if s.endswith('\n'):
         return s
     elif '\r\n' in s:
@@ -116,6 +130,19 @@ def textfile(s: str) -> str:  # should have trailing newline
         return s + '\n'
 
 
+def format_sample_case(s: str) -> str:
+    """format_sample_case convert a string s to a good form as a sample case.
+
+    A good form means that, it use LR instead of CRLF, it has the trailing newline, and it has no superfluous whitespaces.
+    """
+
+    if not s.strip():
+        return ''
+    lines = s.strip().splitlines()
+    lines = [line.strip() + '\n' for line in lines]
+    return ''.join(lines)
+
+
 def exec_command(command_str: str, *, stdin: 'Optional[IO[Any]]' = None, input: Optional[bytes] = None, timeout: Optional[float] = None, gnu_time: Optional[str] = None) -> Tuple[Dict[str, Any], subprocess.Popen]:
     if input is not None:
         assert stdin is None
diff --git a/onlinejudge/service/codeforces.py b/onlinejudge/service/codeforces.py
index 055b4c3..e61ee12 100644
--- a/onlinejudge/service/codeforces.py
+++ b/onlinejudge/service/codeforces.py
@@ -325,9 +325,8 @@ class CodeforcesProblem(onlinejudge.type.Problem):
             title, pre = list(non_empty_children)
             assert 'title' in title.attrs['class']
             assert pre.name == 'pre'
-            s = utils.parse_content(pre)
-            s = s.lstrip()
-            samples.add(s.encode(), title.string)
+            data = utils.format_sample_case(str(utils.parse_content(pre)))
+            samples.add(data.encode(), title.string)
         return samples.get()
 
     def get_available_languages(self, *, session: Optional[requests.Session] = None) -> List[Language]:
diff --git a/onlinejudge_api/login_service.py b/onlinejudge_api/login_service.py
index 58c5e5b..1e6bc77 100644
--- a/onlinejudge_api/login_service.py
+++ b/onlinejudge_api/login_service.py
@@ -25,7 +25,7 @@ def main(service: Service, *, username: Optional[str], password: Optional[str],
 
     result = {}  # type: Dict[str, Any]
     if check_only:
-        assert username is None
+        # We cannot check `assert username is None` because some environments defines $USERNAME and it is set here. See https://github.com/online-judge-tools/api-client/issues/53
         assert password is None
         result["loggedIn"] = service.is_logged_in(session=session)
     else:


Test patch:
diff --git a/tests/get_problem_codeforces.py b/tests/get_problem_codeforces.py
index 8687392..33fde3e 100644
--- a/tests/get_problem_codeforces.py
+++ b/tests/get_problem_codeforces.py
@@ -5,25 +5,144 @@ from onlinejudge_api.main import main
 
 class GetProblemCodeforcesTest(unittest.TestCase):
     def test_problemset_700_b(self):
+        """This tests a problem in the problemset.
+        """
+
         url = 'http://codeforces.com/problemset/problem/700/B'
-        expected = {"status": "ok", "messages": [], "result": {"url": "https://codeforces.com/problemset/problem/700/B", "tests": [{"input": "7 2\n1 5 6 2\n1 3\n3 2\n4 5\n3 7\n4 3\n4 6\n", "output": "6\n"}, {"input": "9 3\n3 2 1 6 5 9\n8 9\n3 2\n2 7\n3 4\n7 6\n4 5\n2 1\n2 8\n", "output": "9\n"}], "context": {}}}
+        expected = {
+            "status": "ok",
+            "messages": [],
+            "result": {
+                "url": "https://codeforces.com/problemset/problem/700/B",
+                "tests": [{
+                    "input": "7 2\n1 5 6 2\n1 3\n3 2\n4 5\n3 7\n4 3\n4 6\n",
+                    "output": "6\n"
+                }, {
+                    "input": "9 3\n3 2 1 6 5 9\n8 9\n3 2\n2 7\n3 4\n7 6\n4 5\n2 1\n2 8\n",
+                    "output": "9\n"
+                }],
+                "context": {}
+            },
+        }
         actual = main(['get-problem', url], debug=True)
         self.assertEqual(expected, actual)
 
     def test_contest_538_h(self):
+        """This tests a old problem.
+        """
+
         url = 'http://codeforces.com/contest/538/problem/H'
-        expected = {"status": "ok", "messages": [], "result": {"url": "https://codeforces.com/contest/538/problem/H", "tests": [{"input": "10 20\n3 0\n3 6\n4 9\n16 25\n", "output": "POSSIBLE\n4 16\n112\n"}, {"input": "1 10\n3 3\n0 10\n0 10\n0 10\n1 2\n1 3\n2 3\n", "output": "IMPOSSIBLE\n"}], "name": "Summer Dichotomy", "context": {"contest": {"name": "Codeforces Round #300", "url": "https://codeforces.com/contest/538"}, "alphabet": "H"}}}
+        expected = {
+            "status": "ok",
+            "messages": [],
+            "result": {
+                "url": "https://codeforces.com/contest/538/problem/H",
+                "tests": [{
+                    "input": "10 20\n3 0\n3 6\n4 9\n16 25\n",
+                    "output": "POSSIBLE\n4 16\n112\n"
+                }, {
+                    "input": "1 10\n3 3\n0 10\n0 10\n0 10\n1 2\n1 3\n2 3\n",
+                    "output": "IMPOSSIBLE\n"
+                }],
+                "name": "Summer Dichotomy",
+                "context": {
+                    "contest": {
+                        "name": "Codeforces Round #300",
+                        "url": "https://codeforces.com/contest/538"
+                    },
+                    "alphabet": "H"
+                }
+            },
+        }
         actual = main(['get-problem', url], debug=True)
         self.assertEqual(expected, actual)
 
     def test_gym_101020_a(self):
+        """This tests a problem in the gym.
+        """
+
         url = 'http://codeforces.com/gym/101020/problem/A'
-        expected = {"status": "ok", "messages": [], "result": {"url": "https://codeforces.com/gym/101020/problem/A", "tests": [{"input": "3\n5 4\n1 2\n33 33\n", "output": "20\n2\n1089\n"}], "name": "Jerry's Window", "context": {"contest": {"name": "2015 Syrian Private Universities Collegiate Programming Contest", "url": "https://codeforces.com/gym/101020"}, "alphabet": "A"}}}
+        expected = {
+            "status": "ok",
+            "messages": [],
+            "result": {
+                "url": "https://codeforces.com/gym/101020/problem/A",
+                "tests": [{
+                    "input": "3\n5 4\n1 2\n33 33\n",
+                    "output": "20\n2\n1089\n"
+                }],
+                "name": "Jerry's Window",
+                "context": {
+                    "contest": {
+                        "name": "2015 Syrian Private Universities Collegiate Programming Contest",
+                        "url": "https://codeforces.com/gym/101020"
+                    },
+                    "alphabet": "A"
+                }
+            },
+        }
         actual = main(['get-problem', url], debug=True)
         self.assertEqual(expected, actual)
 
     def test_contest_1080_a(self):
+        """Recent (Nov 2018) problems has leading spaces for sample cases. We should check that they are removed.
+
+        .. seealso::
+            https://github.com/online-judge-tools/oj/issues/198
+        """
+
         url = 'https://codeforces.com/contest/1080/problem/A'
-        expected = {"status": "ok", "messages": [], "result": {"url": "https://codeforces.com/contest/1080/problem/A", "tests": [{"input": "3 5\n", "output": "10\n"}, {"input": "15 6\n", "output": "38\n"}], "name": "Petya and Origami", "context": {"contest": {"name": "Codeforces Round #524 (Div. 2)", "url": "https://codeforces.com/contest/1080"}, "alphabet": "A"}}}
+        expected = {
+            "status": "ok",
+            "messages": [],
+            "result": {
+                "url": "https://codeforces.com/contest/1080/problem/A",
+                "tests": [{
+                    "input": "3 5\n",
+                    "output": "10\n"
+                }, {
+                    "input": "15 6\n",
+                    "output": "38\n"
+                }],
+                "name": "Petya and Origami",
+                "context": {
+                    "contest": {
+                        "name": "Codeforces Round #524 (Div. 2)",
+                        "url": "https://codeforces.com/contest/1080"
+                    },
+                    "alphabet": "A"
+                }
+            },
+        }
+        actual = main(['get-problem', url], debug=True)
+        self.assertEqual(expected, actual)
+
+    def test_contest_1344_d(self):
+        """The sample output of this problem has superfluous whitespaces. We should check that they are removed.
+
+        .. seealso::
+            see https://github.com/online-judge-tools/api-client/issues/24
+        """
+
+        url = 'https://codeforces.com/contest/1334/problem/D'
+        expected = {
+            "status": "ok",
+            "messages": [],
+            "result": {
+                "url": "https://codeforces.com/contest/1334/problem/D",
+                "tests": [{
+                    "input": "3\n2 1 3\n3 3 6\n99995 9998900031 9998900031\n",
+                    "output": "1 2 1\n1 3 2 3\n1\n"
+                }],
+                "name": "Minimum Euler Cycle",
+                "context": {
+                    "contest": {
+                        "name": "Educational Codeforces Round 85 (Rated for Div. 2)",
+                        "url": "https://codeforces.com/contest/1334"
+                    },
+                    "alphabet": "D"
+                }
+            },
+        }
         actual = main(['get-problem', url], debug=True)
         self.assertEqual(expected, actual)