• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import annotations
6
7import argparse
8import datetime as dt
9import json
10import pathlib
11import unittest
12from typing import Any
13from urllib import parse as urlparse
14
15from crossbench.parse import (DurationParser, NumberParser, ObjectParser,
16                              PathParser)
17from tests import test_helper
18from tests.crossbench.base import CrossbenchFakeFsTestCase
19
20
21class DurationParserTestCase(unittest.TestCase):
22
23  def test_parse_negative(self):
24    with self.assertRaises(argparse.ArgumentTypeError):
25      DurationParser.positive_duration(-1)
26    with self.assertRaises(argparse.ArgumentTypeError) as cm:
27      DurationParser.positive_duration("-1")
28    with self.assertRaises(argparse.ArgumentTypeError) as cm:
29      DurationParser.positive_or_zero_duration("-1")
30    with self.assertRaises(argparse.ArgumentTypeError) as cm:
31      DurationParser.positive_or_zero_duration(dt.timedelta(seconds=-1))
32    with self.assertRaises(argparse.ArgumentTypeError) as cm:
33      DurationParser.positive_duration("-1")
34    with self.assertRaises(argparse.ArgumentTypeError) as cm:
35      DurationParser.positive_duration(dt.timedelta(seconds=-1))
36    self.assertIn("-1", str(cm.exception))
37    self.assertEqual(DurationParser.any_duration("-1.5").total_seconds(), -1.5)
38
39  def test_parse_zero(self):
40    self.assertEqual(DurationParser.any_duration("0").total_seconds(), 0)
41    self.assertEqual(DurationParser.any_duration("0s").total_seconds(), 0)
42    self.assertEqual(DurationParser.any_duration("0.0").total_seconds(), 0)
43    self.assertEqual(
44        DurationParser.positive_or_zero_duration("0.0").total_seconds(), 0)
45    for invalid in (-1, 0, "-1", "0", "invalid", dt.timedelta(0),
46                    dt.timedelta(seconds=-1)):
47      with self.assertRaises(argparse.ArgumentTypeError) as cm:
48        DurationParser.positive_duration(invalid)
49      self.assertIn(str(invalid), str(cm.exception))
50      with self.assertRaises(argparse.ArgumentTypeError) as cm:
51        DurationParser.positive_duration(invalid)
52      self.assertIn(str(invalid), str(cm.exception))
53
54  def test_parse_empty(self):
55    with self.assertRaises(argparse.ArgumentTypeError):
56      DurationParser.positive_duration("")
57    with self.assertRaises(argparse.ArgumentTypeError):
58      DurationParser.any_duration("")
59    with self.assertRaises(argparse.ArgumentTypeError):
60      DurationParser.positive_or_zero_duration("")
61    with self.assertRaises(argparse.ArgumentTypeError):
62      DurationParser.positive_duration("")
63
64  def test_invalid_suffix(self):
65    with self.assertRaises(argparse.ArgumentTypeError) as cm:
66      DurationParser.positive_duration("100XXX")
67    self.assertIn("Unknown duration format", str(cm.exception))
68    with self.assertRaises(argparse.ArgumentTypeError):
69      DurationParser.positive_duration("X0XX")
70    with self.assertRaises(argparse.ArgumentTypeError):
71      DurationParser.positive_duration("100X0XX")
72
73  def test_no_unit(self):
74    self.assertEqual(
75        DurationParser.positive_duration("200"), dt.timedelta(seconds=200))
76    self.assertEqual(
77        DurationParser.positive_duration(200), dt.timedelta(seconds=200))
78
79  def test_milliseconds(self):
80    self.assertEqual(
81        DurationParser.positive_duration("27.5ms"),
82        dt.timedelta(milliseconds=27.5))
83    self.assertEqual(
84        DurationParser.positive_duration(dt.timedelta(milliseconds=27.5)),
85        dt.timedelta(milliseconds=27.5))
86    self.assertEqual(
87        DurationParser.positive_duration("27.5 millis"),
88        dt.timedelta(milliseconds=27.5))
89    self.assertEqual(
90        DurationParser.positive_duration("27.5 milliseconds"),
91        dt.timedelta(milliseconds=27.5))
92
93  def test_seconds(self):
94    self.assertEqual(
95        DurationParser.positive_duration("27.5s"), dt.timedelta(seconds=27.5))
96    self.assertEqual(
97        DurationParser.positive_duration("1 sec"), dt.timedelta(seconds=1))
98    self.assertEqual(
99        DurationParser.positive_duration("27.5 secs"),
100        dt.timedelta(seconds=27.5))
101    self.assertEqual(
102        DurationParser.positive_duration("1 second"), dt.timedelta(seconds=1))
103    self.assertEqual(
104        DurationParser.positive_duration("27.5 seconds"),
105        dt.timedelta(seconds=27.5))
106
107  def test_minutes(self):
108    self.assertEqual(
109        DurationParser.positive_duration("27.5m"), dt.timedelta(minutes=27.5))
110    self.assertEqual(
111        DurationParser.positive_duration("1 min"), dt.timedelta(minutes=1))
112    self.assertEqual(
113        DurationParser.positive_duration("27.5 mins"),
114        dt.timedelta(minutes=27.5))
115    self.assertEqual(
116        DurationParser.positive_duration("1 minute"), dt.timedelta(minutes=1))
117    self.assertEqual(
118        DurationParser.positive_duration("27.5 minutes"),
119        dt.timedelta(minutes=27.5))
120
121  def test_hours(self):
122    self.assertEqual(
123        DurationParser.positive_duration("27.5h"), dt.timedelta(hours=27.5))
124    self.assertEqual(
125        DurationParser.positive_duration("0.1 h"), dt.timedelta(hours=0.1))
126    self.assertEqual(
127        DurationParser.positive_duration("27.5 hrs"), dt.timedelta(hours=27.5))
128    self.assertEqual(
129        DurationParser.positive_duration("1 hour"), dt.timedelta(hours=1))
130    self.assertEqual(
131        DurationParser.positive_duration("27.5 hours"),
132        dt.timedelta(hours=27.5))
133
134
135class ObjectParserHelperTestCase(CrossbenchFakeFsTestCase):
136
137  def setUp(self):
138    super().setUp()
139    self._json_test_data = {"int": 1, "array": [1, "2"]}
140
141  def test_parse_any_str(self):
142    self.assertEqual(ObjectParser.any_str(""), "")
143    self.assertEqual(ObjectParser.any_str("1234"), "1234")
144
145  def test_parse_any_str_invalid(self):
146    for invalid in (None, 1, [], {}, [1], ["a"], {"a": "a"}):
147      with self.assertRaises(argparse.ArgumentTypeError) as cm:
148        ObjectParser.any_str(invalid)
149      self.assertIn(str(invalid), str(cm.exception))
150
151  def test_parse_non_empty_str(self):
152    self.assertEqual(ObjectParser.non_empty_str("a string"), "a string")
153    with self.assertRaises(argparse.ArgumentTypeError) as cm:
154      ObjectParser.non_empty_str("")
155    self.assertIn("empty", str(cm.exception))
156
157  def test_parse_httpx_url_str(self):
158    for valid in ("http://foo.com", "https://foo.com", "http://localhost:800"):
159      self.assertEqual(ObjectParser.httpx_url_str(valid), valid)
160    for invalid in ("", "ftp://localhost:32", "http://///"):
161      with self.assertRaises(argparse.ArgumentTypeError) as cm:
162        _ = ObjectParser.httpx_url_str(invalid)
163      self.assertIn(invalid, str(cm.exception))
164
165  def test_parse_any_int(self):
166    self.assertEqual(NumberParser.any_int("-123456"), -123456)
167    self.assertEqual(NumberParser.any_int(-123456), -123456)
168    self.assertEqual(NumberParser.any_int("-1"), -1)
169    self.assertEqual(NumberParser.any_int(-1), -1)
170    self.assertEqual(NumberParser.any_int("0"), 0)
171    self.assertEqual(NumberParser.any_int(0), 0)
172    self.assertEqual(NumberParser.any_int("1"), 1)
173    self.assertEqual(NumberParser.any_int(1), 1)
174    self.assertEqual(NumberParser.any_int("123456"), 123456)
175    self.assertEqual(NumberParser.any_int(123456), 123456)
176
177  def test_parse_any_int_invalid(self):
178    for invalid in ("", "-1.2", "1.2", "100.001", "Nan", "inf", "-inf",
179                    "invalid"):
180      with self.assertRaises(argparse.ArgumentTypeError):
181        _ = NumberParser.any_int(invalid)
182
183  def test_parse_positive_int(self):
184    self.assertEqual(NumberParser.positive_int("1"), 1)
185    self.assertEqual(NumberParser.positive_int("123"), 123)
186
187  def test_parse_positive_int_ivalid(self):
188    for invalid in ("", "0", "-1", "-1.2", "1.2", "Nan", "inf", "-inf",
189                    "invalid"):
190      with self.assertRaises(argparse.ArgumentTypeError):
191        _ = NumberParser.positive_int(invalid)
192
193  def test_parse_positive_zero_int(self):
194    self.assertEqual(NumberParser.positive_zero_int("1"), 1)
195    self.assertEqual(NumberParser.positive_zero_int("0"), 0)
196
197  def test_parse_positive_zero_int_invalid(self):
198    for invalid in ("", "-1", "-1.2", "1.2", "NaN", "inf", "-inf", "invalid"):
199      with self.assertRaises(argparse.ArgumentTypeError):
200        _ = NumberParser.positive_zero_int(invalid)
201
202  def test_parse_any_float(self):
203    self.assertEqual(NumberParser.any_float("-1.2"), -1.2)
204    self.assertEqual(NumberParser.any_float(-1.2), -1.2)
205    self.assertEqual(NumberParser.any_float("-1"), -1.0)
206    self.assertEqual(NumberParser.any_float(-1), -1.0)
207    self.assertEqual(NumberParser.any_float("0"), 0.0)
208    self.assertEqual(NumberParser.any_float(0), 0.0)
209    self.assertEqual(NumberParser.any_float("0.0"), 0.0)
210    self.assertEqual(NumberParser.any_float(0.0), 0.0)
211    self.assertEqual(NumberParser.any_float("0.1"), 0.1)
212    self.assertEqual(NumberParser.any_float(0.1), 0.1)
213
214  def test_parse_float_invalid(self):
215    for invalid in ("", "abc", "NaN", "inf", "-inf", "invalid"):
216      with self.assertRaises(argparse.ArgumentTypeError):
217        _ = NumberParser.positive_zero_float(invalid)
218
219  def test_parse_positive_zero_float(self):
220    self.assertEqual(NumberParser.positive_zero_float("1"), 1.0)
221    self.assertEqual(NumberParser.positive_zero_float("0"), 0.0)
222    self.assertEqual(NumberParser.positive_zero_float("0.0"), 0.0)
223    self.assertEqual(NumberParser.positive_zero_float("1.23"), 1.23)
224
225  def test_parse_positive_zero_float_invlid(self):
226    for invalid in ("", "-1", "-1.2", "NaN", "inf", "-inf", "invalid"):
227      with self.assertRaises(argparse.ArgumentTypeError):
228        _ = NumberParser.positive_zero_float(invalid)
229
230  def test_parse_port_number(self):
231    self.assertEqual(NumberParser.port_number(1), 1)
232    self.assertEqual(NumberParser.port_number("1"), 1)
233    self.assertEqual(NumberParser.port_number(440), 440)
234    self.assertEqual(NumberParser.port_number("440"), 440)
235    self.assertEqual(NumberParser.port_number(65535), 65535)
236    self.assertEqual(NumberParser.port_number("65535"), 65535)
237
238  def test_parse_port_number_invalid(self):
239    for invalid in ("", "-1", "-1.2", "6553500", "inf", "-inf", "invalid"):
240      with self.assertRaises(argparse.ArgumentTypeError):
241        _ = NumberParser.port_number(invalid)
242
243  def _json_file_test_helper(self, parser) -> Any:
244    with self.assertRaises(argparse.ArgumentTypeError):
245      parser("file")
246
247    path = pathlib.Path("file.json")
248    self.assertFalse(path.exists())
249    with self.assertRaises(argparse.ArgumentTypeError):
250      parser(path)
251
252    path.touch()
253    with self.assertRaises(argparse.ArgumentTypeError):
254      parser(path)
255
256    with path.open("w", encoding="utf-8") as f:
257      f.write("{invalid json data")
258    with self.assertRaises(argparse.ArgumentTypeError):
259      parser(path)
260    # Test very long lines too.
261    with path.open("w", encoding="utf-8") as f:
262      f.write("{\n invalid json data" + "." * 100)
263    with self.assertRaises(argparse.ArgumentTypeError):
264      parser(path)
265
266    with path.open("w", encoding="utf-8") as f:
267      f.write("""{
268              'a': {},
269              'c': }}
270              """)
271    with self.assertRaises(argparse.ArgumentTypeError):
272      parser(path)
273
274    with path.open("w", encoding="utf-8") as f:
275      json.dump(self._json_test_data, f)
276    str_result = parser(str(path))
277    path_result = parser(path)
278    self.assertEqual(str_result, path_result)
279    return str_result
280
281  def test_parse_json_file(self):
282    result = self._json_file_test_helper(ObjectParser.json_file)
283    self.assertDictEqual(self._json_test_data, result)
284
285  def test_parse_json_file_path(self):
286    result = self._json_file_test_helper(PathParser.json_file_path)
287    self.assertEqual(pathlib.Path("file.json"), result)
288
289  def test_parse_hjson_file_path(self):
290    result = self._json_file_test_helper(PathParser.hjson_file_path)
291    self.assertEqual(pathlib.Path("file.json"), result)
292
293  def test_parse_inline_hjson(self):
294    with self.assertRaises(argparse.ArgumentTypeError):
295      ObjectParser.inline_hjson("")
296    with self.assertRaises(argparse.ArgumentTypeError):
297      ObjectParser.inline_hjson("{invalid json}")
298    with self.assertRaises(argparse.ArgumentTypeError):
299      ObjectParser.inline_hjson("{'asdfas':'asdf}")
300    self.assertDictEqual(
301        self._json_test_data,
302        ObjectParser.inline_hjson(json.dumps(self._json_test_data)))
303
304  def test_parse_dir_path(self):
305    with self.assertRaises(argparse.ArgumentTypeError):
306      PathParser.dir_path("")
307    file = pathlib.Path("file")
308    with self.assertRaises(argparse.ArgumentTypeError) as cm:
309      PathParser.dir_path(file)
310    self.assertIn("does not exist", str(cm.exception))
311    file.touch()
312    with self.assertRaises(argparse.ArgumentTypeError) as cm:
313      PathParser.dir_path(file)
314    self.assertIn("not a folder", str(cm.exception))
315    folder = pathlib.Path("folder")
316    folder.mkdir()
317    self.assertEqual(folder, PathParser.dir_path(folder))
318    self.assertEqual(folder, PathParser.dir_path(str(folder)))
319
320  def test_parse_non_empty_dir_path(self):
321    folder = pathlib.Path("folder")
322    folder.mkdir()
323    with self.assertRaises(argparse.ArgumentTypeError) as cm:
324      PathParser.non_empty_dir_path(folder)
325    self.assertIn("empty", str(cm.exception))
326    (folder / "foo").touch()
327    self.assertEqual(folder, PathParser.non_empty_dir_path(folder))
328    self.assertEqual(folder, PathParser.non_empty_dir_path(str(folder)))
329
330  def test_parse_non_empty_file_path(self):
331    with self.assertRaises(argparse.ArgumentTypeError):
332      PathParser.non_empty_file_path("")
333    folder = pathlib.Path("folder")
334    with self.assertRaises(argparse.ArgumentTypeError) as cm:
335      PathParser.non_empty_file_path(folder)
336    self.assertIn("does not exist", str(cm.exception))
337    folder.mkdir()
338    with self.assertRaises(argparse.ArgumentTypeError) as cm:
339      PathParser.non_empty_file_path(folder)
340    self.assertIn("not a file", str(cm.exception))
341    file = pathlib.Path("file")
342    file.touch()
343    with self.assertRaises(argparse.ArgumentTypeError) as cm:
344      self.assertEqual(file, PathParser.non_empty_file_path(file))
345    self.assertIn("is an empty file", str(cm.exception))
346
347    with file.open("w", encoding="utf-8") as f:
348      f.write("fooo")
349    self.assertEqual(file, PathParser.non_empty_file_path(file))
350
351  def test_parse_existing_file_path(self):
352    with self.assertRaises(argparse.ArgumentTypeError):
353      PathParser.existing_file_path("")
354    folder = pathlib.Path("folder")
355    with self.assertRaises(argparse.ArgumentTypeError) as cm:
356      PathParser.existing_file_path(folder)
357    self.assertIn("does not exist", str(cm.exception))
358    folder.mkdir()
359    with self.assertRaises(argparse.ArgumentTypeError) as cm:
360      PathParser.existing_file_path(folder)
361    self.assertIn("not a file", str(cm.exception))
362    file = pathlib.Path("file")
363    file.touch()
364    self.assertEqual(file, PathParser.existing_file_path(file))
365
366  def test_parse_path(self):
367    with self.assertRaises(argparse.ArgumentTypeError):
368      PathParser.path("")
369    folder = pathlib.Path("folder")
370    folder.mkdir()
371    self.assertEqual(folder, PathParser.path(folder))
372    file = pathlib.Path("file")
373    file.touch()
374    self.assertEqual(file, PathParser.path(file))
375
376  def test_parse_bool_success(self):
377    self.assertIs(ObjectParser.bool("true"), True)
378    self.assertIs(ObjectParser.bool("True"), True)
379    self.assertIs(ObjectParser.bool(True), True)
380    self.assertIs(ObjectParser.bool("false"), False)
381    self.assertIs(ObjectParser.bool("False"), False)
382    self.assertIs(ObjectParser.bool(False), False)
383
384  def test_parse_bool_invalid(self):
385    for invalid in (1, 0, "1", "0", "", None, [], tuple()):
386      with self.assertRaises(argparse.ArgumentTypeError):
387        ObjectParser.bool(invalid)
388
389  def test_parse_sh_cmd(self):
390    self.assertListEqual(ObjectParser.sh_cmd("ls -al ."), ["ls", "-al", "."])
391    self.assertListEqual(ObjectParser.sh_cmd("ls -al '.'"), ["ls", "-al", "."])
392    self.assertListEqual(
393        ObjectParser.sh_cmd(";ls -al '.'"), [";ls", "-al", "."])
394    self.assertListEqual(
395        ObjectParser.sh_cmd(("ls", "-al", ".")), ["ls", "-al", "."])
396
397  def test_parse_sh_cmd_invalid(self):
398    for invalid in (1, "", None, [], "ls -al \"."):
399      with self.assertRaises(argparse.ArgumentTypeError):
400        ObjectParser.sh_cmd(invalid)
401
402  def test_parse_dict_invalid(self):
403    for invalid in (1, 0, "1", "0", "", None, [], tuple()):
404      with self.assertRaises(argparse.ArgumentTypeError):
405        ObjectParser.dict(invalid)
406
407  def test_parse_dict(self):
408    self.assertDictEqual(ObjectParser.dict({}), {})
409    self.assertDictEqual(ObjectParser.dict({"A": 2}), {"A": 2})
410
411  def test_parse_non_empty_dict_invalid(self):
412    for invalid in (1, 0, "1", "0", "", None, [], tuple(), {}):
413      with self.assertRaises(argparse.ArgumentTypeError):
414        ObjectParser.non_empty_dict(invalid)
415
416  def test_parse_non_empty_dict(self):
417    result = ObjectParser.non_empty_dict({"a": 1})
418    self.assertDictEqual(result, {"a": 1})
419
420  def test_parse_unique_sequence(self):
421    self.assertListEqual(ObjectParser.unique_sequence([]), [])
422    self.assertTupleEqual(ObjectParser.unique_sequence(tuple()), tuple())
423    self.assertListEqual(ObjectParser.unique_sequence([1, 2, 3]), [1, 2, 3])
424    self.assertTupleEqual(ObjectParser.unique_sequence((1, 2, 3)), (1, 2, 3))
425
426  def test_parse_unique_sequence_invalid(self):
427    with self.assertRaises(argparse.ArgumentTypeError) as cm:
428      ObjectParser.unique_sequence([1, 1, 2, 2, 2, 3, 5, 5])
429    self.assertIn("duplicates", str(cm.exception))
430    self.assertIn("1, 2, 5", str(cm.exception))
431
432  def test_parse_unique_sequence_custom_exception(self):
433
434    class CustomException(Exception):
435      pass
436
437    with self.assertRaises(CustomException):
438      ObjectParser.unique_sequence([1, 1], error_cls=CustomException)
439
440  def test_parse_unique_sequence_custom_name(self):
441    with self.assertRaises(argparse.ArgumentTypeError) as cm:
442      ObjectParser.unique_sequence([1, 1], name="custom test name")
443    self.assertIn("custom test name", str(cm.exception))
444
445  def test_parse_sequence(self):
446    self.assertListEqual(ObjectParser.sequence([]), [])
447    self.assertListEqual(ObjectParser.sequence([1, 2]), [1, 2])
448    self.assertTupleEqual(ObjectParser.sequence(tuple()), tuple())
449    self.assertTupleEqual(ObjectParser.sequence((1, 2)), (1, 2))
450
451  def test_parse_sequence_invalid(self):
452    for invalid in ("", "1", 1, {}, {"a": 1}, set(), set((1, 2))):
453      with self.subTest(invalid=invalid):
454        with self.assertRaises(argparse.ArgumentTypeError):
455          ObjectParser.sequence(invalid)
456
457  def test_parse_non_empty_sequence(self):
458    with self.assertRaises(argparse.ArgumentTypeError):
459      _ = ObjectParser.non_empty_sequence([])
460    self.assertListEqual(ObjectParser.non_empty_sequence([1, 2]), [1, 2])
461    with self.assertRaises(argparse.ArgumentTypeError):
462      _ = ObjectParser.non_empty_sequence(tuple())
463    self.assertTupleEqual(ObjectParser.non_empty_sequence((1, 2)), (1, 2))
464
465  def test_parse_non_empty_sequence_invalid(self):
466    for invalid in ("", "1", 1, {}, {"a": 1}, set(), set((1, 2)), (), []):
467      with self.subTest(invalid=invalid):
468        with self.assertRaises(argparse.ArgumentTypeError):
469          ObjectParser.non_empty_sequence(invalid)
470
471  def test_parse_fuzzy_url(self):
472    expected = (
473        ("/foo/bar", "file:///foo/bar"),
474        ("C:/foo/bar", "file://C:/foo/bar"),
475        ("1234.com", "https://1234.com"),
476        ("http://1234.com", "http://1234.com"),
477        ("test.com", "https://test.com"),
478        ("test.com/", "https://test.com/"),
479        ("test.com/1234", "https://test.com/1234"),
480        ("test.com/bar", "https://test.com/bar"),
481        ("test.com/bar?x=1", "https://test.com/bar?x=1"),
482        ("test.com:1234", "https://test.com:1234"),
483        ("test.com:1234/", "https://test.com:1234/"),
484        ("test.com:1234/56", "https://test.com:1234/56"),
485        ("test.com:1234/56/", "https://test.com:1234/56/"),
486        ("test.com:1234/bar", "https://test.com:1234/bar"),
487        ("test.com:1234/bar?x=1", "https://test.com:1234/bar?x=1"),
488        ("localhost:8123", "https://localhost:8123"),
489        ("localhost:8123/", "https://localhost:8123/"),
490        ("localhost:8123/77", "https://localhost:8123/77"),
491        ("localhost:8123/77/", "https://localhost:8123/77/"),
492        ("localhost:8123/bar", "https://localhost:8123/bar"),
493        ("localhost:8123/bar?x=1", "https://localhost:8123/bar?x=1"),
494    )
495    for url, result in expected:
496      with self.subTest(url=url):
497        self.assertEqual(ObjectParser.parse_fuzzy_url_str(url), result)
498        parsed = ObjectParser.parse_fuzzy_url(url)
499        self.assertEqual(urlparse.urlunparse(parsed), result)
500
501  def test_parse_fuzzy_url_default_scheme(self):
502    expected = ("test.com", "test.com/", "test.com/bar", "test.com/bar?x=1",
503                "test.com:1234", "test.com:1234/", "test.com:1234/bar",
504                "test.com:1234/bar?x=1", "localhost:8123", "localhost:8123/",
505                "localhost:8123/bar", "localhost:8123/bar?x1")
506    for url in expected:
507      with self.subTest(url=url):
508        result_default = f"https://{url}"
509        self.assertEqual(ObjectParser.parse_fuzzy_url_str(url), result_default)
510        parsed = ObjectParser.parse_fuzzy_url(url)
511        self.assertEqual(urlparse.urlunparse(parsed), result_default)
512        result_custom = f"ftp://{url}"
513        self.assertEqual(
514            ObjectParser.parse_fuzzy_url_str(url, default_scheme="ftp"),
515            result_custom)
516        parsed = ObjectParser.parse_fuzzy_url(url, default_scheme="ftp")
517        self.assertEqual(urlparse.urlunparse(parsed), result_custom)
518
519  def test_parse_url(self):
520    expected = (
521        ("file:///foo/bar", "file:///foo/bar"),
522        ("about:blank", "about:blank"),
523        ("http://test.com/bar", "http://test.com/bar"),
524        ("https://test.com/bar", "https://test.com/bar"),
525        ("http://test.com", "http://test.com"),
526        ("https://test.com/", "https://test.com/"),
527        ("http://test.com/bar", "http://test.com/bar"),
528        ("https://test.com/bar?x=1", "https://test.com/bar?x=1"),
529        ("http://test.com:1234", "http://test.com:1234"),
530        ("https://test.com:1234/", "https://test.com:1234/"),
531        ("http://test.com:1234/bar", "http://test.com:1234/bar"),
532        ("https://test.com:1234/bar?x=1", "https://test.com:1234/bar?x=1"),
533        ("http://localhost:8123", "http://localhost:8123"),
534        ("https://localhost:8123/", "https://localhost:8123/"),
535        ("http://localhost:8123/bar", "http://localhost:8123/bar"),
536        ("https://localhost:8123/bar?x=1", "https://localhost:8123/bar?x=1"),
537    )
538    for url, result in expected:
539      with self.subTest(url=url):
540        self.assertEqual(ObjectParser.url_str(url), result)
541        self.assertEqual(ObjectParser.parse_fuzzy_url_str(url), result)
542        parsed = ObjectParser.url(url)
543        self.assertEqual(urlparse.urlunparse(parsed), result)
544        parsed_fuzzy = ObjectParser.parse_fuzzy_url(url)
545        self.assertEqual(urlparse.urlunparse(parsed_fuzzy), result)
546
547  def test_parse_url_invalid(self):
548    for invalid in (None, "", {}, "http:// foo .com/bar", "htt p://foo.com",
549                    "http://foo.com:-123/bar"):
550      with self.subTest(invalid=invalid):
551        with self.assertRaises(argparse.ArgumentTypeError):
552          _ = ObjectParser.url(invalid)
553        with self.assertRaises(argparse.ArgumentTypeError):
554          _ = ObjectParser.url_str(invalid)
555        with self.assertRaises(argparse.ArgumentTypeError):
556          _ = ObjectParser.httpx_url_str(invalid)
557        with self.assertRaises(argparse.ArgumentTypeError):
558          _ = ObjectParser.parse_fuzzy_url_str(invalid)
559        with self.assertRaises(argparse.ArgumentTypeError):
560          _ = ObjectParser.parse_fuzzy_url(invalid)
561
562  def test_parse_httpx_url_str_invalid(self):
563    for invalid in ("ftp://foo.com:123/bar", "ssh://test.com"):
564      with self.subTest(invalid=invalid):
565        with self.assertRaises(argparse.ArgumentTypeError):
566          _ = ObjectParser.httpx_url_str(invalid)
567
568  def test_parse_url_scheme(self):
569    url = "ftp://foo.com"
570    parsed = ObjectParser.url(url)
571    self.assertEqual(urlparse.urlunparse(parsed), url)
572    with self.assertRaises(argparse.ArgumentTypeError):
573      _ = ObjectParser.url(url, schemes=("https",))
574    parsed = ObjectParser.url(
575        url, schemes=(
576            "https",
577            "ftp",
578        ))
579    self.assertEqual(urlparse.urlunparse(parsed), url)
580
581  def test_parse_regexp(self):
582    with self.assertRaises(argparse.ArgumentTypeError):
583      ObjectParser.regexp("\\")
584    pattern = ObjectParser.regexp("^abc$")
585    self.assertEqual(pattern.pattern, "^abc$")
586
587
588if __name__ == "__main__":
589  test_helper.run_pytest(__file__)
590