test_config.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. # test_config.py -- Tests for reading and writing configuration files
  2. # Copyright (C) 2011 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as public by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Tests for reading and writing configuration files."""
  22. import os
  23. import sys
  24. from io import BytesIO
  25. from unittest import skipIf
  26. from unittest.mock import patch
  27. from dulwich.config import (
  28. CaseInsensitiveOrderedMultiDict,
  29. ConfigDict,
  30. ConfigFile,
  31. StackedConfig,
  32. _check_section_name,
  33. _check_variable_name,
  34. _escape_value,
  35. _format_string,
  36. _parse_string,
  37. apply_instead_of,
  38. parse_submodules,
  39. )
  40. from . import TestCase
  41. class ConfigFileTests(TestCase):
  42. def from_file(self, text):
  43. return ConfigFile.from_file(BytesIO(text))
  44. def test_empty(self) -> None:
  45. ConfigFile()
  46. def test_eq(self) -> None:
  47. self.assertEqual(ConfigFile(), ConfigFile())
  48. def test_default_config(self) -> None:
  49. cf = self.from_file(
  50. b"""[core]
  51. \trepositoryformatversion = 0
  52. \tfilemode = true
  53. \tbare = false
  54. \tlogallrefupdates = true
  55. """
  56. )
  57. self.assertEqual(
  58. ConfigFile(
  59. {
  60. (b"core",): {
  61. b"repositoryformatversion": b"0",
  62. b"filemode": b"true",
  63. b"bare": b"false",
  64. b"logallrefupdates": b"true",
  65. }
  66. }
  67. ),
  68. cf,
  69. )
  70. def test_from_file_empty(self) -> None:
  71. cf = self.from_file(b"")
  72. self.assertEqual(ConfigFile(), cf)
  73. def test_empty_line_before_section(self) -> None:
  74. cf = self.from_file(b"\n[section]\n")
  75. self.assertEqual(ConfigFile({(b"section",): {}}), cf)
  76. def test_comment_before_section(self) -> None:
  77. cf = self.from_file(b"# foo\n[section]\n")
  78. self.assertEqual(ConfigFile({(b"section",): {}}), cf)
  79. def test_comment_after_section(self) -> None:
  80. cf = self.from_file(b"[section] # foo\n")
  81. self.assertEqual(ConfigFile({(b"section",): {}}), cf)
  82. def test_comment_after_variable(self) -> None:
  83. cf = self.from_file(b"[section]\nbar= foo # a comment\n")
  84. self.assertEqual(ConfigFile({(b"section",): {b"bar": b"foo"}}), cf)
  85. def test_comment_character_within_value_string(self) -> None:
  86. cf = self.from_file(b'[section]\nbar= "foo#bar"\n')
  87. self.assertEqual(ConfigFile({(b"section",): {b"bar": b"foo#bar"}}), cf)
  88. def test_comment_character_within_section_string(self) -> None:
  89. cf = self.from_file(b'[branch "foo#bar"] # a comment\nbar= foo\n')
  90. self.assertEqual(ConfigFile({(b"branch", b"foo#bar"): {b"bar": b"foo"}}), cf)
  91. def test_closing_bracket_within_section_string(self) -> None:
  92. cf = self.from_file(b'[branch "foo]bar"] # a comment\nbar= foo\n')
  93. self.assertEqual(ConfigFile({(b"branch", b"foo]bar"): {b"bar": b"foo"}}), cf)
  94. def test_from_file_section(self) -> None:
  95. cf = self.from_file(b"[core]\nfoo = bar\n")
  96. self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
  97. self.assertEqual(b"bar", cf.get((b"core", b"foo"), b"foo"))
  98. def test_from_file_multiple(self) -> None:
  99. cf = self.from_file(b"[core]\nfoo = bar\nfoo = blah\n")
  100. self.assertEqual([b"bar", b"blah"], list(cf.get_multivar((b"core",), b"foo")))
  101. self.assertEqual([], list(cf.get_multivar((b"core",), b"blah")))
  102. def test_from_file_utf8_bom(self) -> None:
  103. text = "[core]\nfoo = b\u00e4r\n".encode("utf-8-sig")
  104. cf = self.from_file(text)
  105. self.assertEqual(b"b\xc3\xa4r", cf.get((b"core",), b"foo"))
  106. def test_from_file_section_case_insensitive_lower(self) -> None:
  107. cf = self.from_file(b"[cOre]\nfOo = bar\n")
  108. self.assertEqual(b"bar", cf.get((b"core",), b"foo"))
  109. self.assertEqual(b"bar", cf.get((b"core", b"foo"), b"foo"))
  110. def test_from_file_section_case_insensitive_mixed(self) -> None:
  111. cf = self.from_file(b"[cOre]\nfOo = bar\n")
  112. self.assertEqual(b"bar", cf.get((b"core",), b"fOo"))
  113. self.assertEqual(b"bar", cf.get((b"cOre", b"fOo"), b"fOo"))
  114. def test_from_file_with_mixed_quoted(self) -> None:
  115. cf = self.from_file(b'[core]\nfoo = "bar"la\n')
  116. self.assertEqual(b"barla", cf.get((b"core",), b"foo"))
  117. def test_from_file_section_with_open_brackets(self) -> None:
  118. self.assertRaises(ValueError, self.from_file, b"[core\nfoo = bar\n")
  119. def test_from_file_value_with_open_quoted(self) -> None:
  120. self.assertRaises(ValueError, self.from_file, b'[core]\nfoo = "bar\n')
  121. def test_from_file_with_quotes(self) -> None:
  122. cf = self.from_file(b'[core]\nfoo = " bar"\n')
  123. self.assertEqual(b" bar", cf.get((b"core",), b"foo"))
  124. def test_from_file_with_interrupted_line(self) -> None:
  125. cf = self.from_file(b"[core]\nfoo = bar\\\n la\n")
  126. self.assertEqual(b"barla", cf.get((b"core",), b"foo"))
  127. def test_from_file_with_boolean_setting(self) -> None:
  128. cf = self.from_file(b"[core]\nfoo\n")
  129. self.assertEqual(b"true", cf.get((b"core",), b"foo"))
  130. def test_from_file_subsection(self) -> None:
  131. cf = self.from_file(b'[branch "foo"]\nfoo = bar\n')
  132. self.assertEqual(b"bar", cf.get((b"branch", b"foo"), b"foo"))
  133. def test_from_file_subsection_invalid(self) -> None:
  134. self.assertRaises(ValueError, self.from_file, b'[branch "foo]\nfoo = bar\n')
  135. def test_from_file_subsection_not_quoted(self) -> None:
  136. cf = self.from_file(b"[branch.foo]\nfoo = bar\n")
  137. self.assertEqual(b"bar", cf.get((b"branch", b"foo"), b"foo"))
  138. def test_write_preserve_multivar(self) -> None:
  139. cf = self.from_file(b"[core]\nfoo = bar\nfoo = blah\n")
  140. f = BytesIO()
  141. cf.write_to_file(f)
  142. self.assertEqual(b"[core]\n\tfoo = bar\n\tfoo = blah\n", f.getvalue())
  143. def test_write_to_file_empty(self) -> None:
  144. c = ConfigFile()
  145. f = BytesIO()
  146. c.write_to_file(f)
  147. self.assertEqual(b"", f.getvalue())
  148. def test_write_to_file_section(self) -> None:
  149. c = ConfigFile()
  150. c.set((b"core",), b"foo", b"bar")
  151. f = BytesIO()
  152. c.write_to_file(f)
  153. self.assertEqual(b"[core]\n\tfoo = bar\n", f.getvalue())
  154. def test_write_to_file_section_multiple(self) -> None:
  155. c = ConfigFile()
  156. c.set((b"core",), b"foo", b"old")
  157. c.set((b"core",), b"foo", b"new")
  158. f = BytesIO()
  159. c.write_to_file(f)
  160. self.assertEqual(b"[core]\n\tfoo = new\n", f.getvalue())
  161. def test_write_to_file_subsection(self) -> None:
  162. c = ConfigFile()
  163. c.set((b"branch", b"blie"), b"foo", b"bar")
  164. f = BytesIO()
  165. c.write_to_file(f)
  166. self.assertEqual(b'[branch "blie"]\n\tfoo = bar\n', f.getvalue())
  167. def test_same_line(self) -> None:
  168. cf = self.from_file(b"[branch.foo] foo = bar\n")
  169. self.assertEqual(b"bar", cf.get((b"branch", b"foo"), b"foo"))
  170. def test_quoted_newlines_windows(self) -> None:
  171. cf = self.from_file(
  172. b"[alias]\r\n"
  173. b"c = '!f() { \\\r\n"
  174. b' printf \'[git commit -m \\"%s\\"]\\n\' \\"$*\\" && \\\r\n'
  175. b' git commit -m \\"$*\\"; \\\r\n'
  176. b" }; f'\r\n"
  177. )
  178. self.assertEqual(list(cf.sections()), [(b"alias",)])
  179. self.assertEqual(
  180. b'\'!f() { printf \'[git commit -m "%s"]\n\' "$*" && git commit -m "$*"',
  181. cf.get((b"alias",), b"c"),
  182. )
  183. def test_quoted(self) -> None:
  184. cf = self.from_file(
  185. b"""[gui]
  186. \tfontdiff = -family \\\"Ubuntu Mono\\\" -size 11 -overstrike 0
  187. """
  188. )
  189. self.assertEqual(
  190. ConfigFile(
  191. {
  192. (b"gui",): {
  193. b"fontdiff": b'-family "Ubuntu Mono" -size 11 -overstrike 0',
  194. }
  195. }
  196. ),
  197. cf,
  198. )
  199. def test_quoted_multiline(self) -> None:
  200. cf = self.from_file(
  201. b"""[alias]
  202. who = \"!who() {\\
  203. git log --no-merges --pretty=format:'%an - %ae' $@ | uniq -c | sort -rn;\\
  204. };\\
  205. who\"
  206. """
  207. )
  208. self.assertEqual(
  209. ConfigFile(
  210. {
  211. (b"alias",): {
  212. b"who": (
  213. b"!who() {git log --no-merges --pretty=format:'%an - "
  214. b"%ae' $@ | uniq -c | sort -rn;};who"
  215. )
  216. }
  217. }
  218. ),
  219. cf,
  220. )
  221. def test_set_hash_gets_quoted(self) -> None:
  222. c = ConfigFile()
  223. c.set(b"xandikos", b"color", b"#665544")
  224. f = BytesIO()
  225. c.write_to_file(f)
  226. self.assertEqual(b'[xandikos]\n\tcolor = "#665544"\n', f.getvalue())
  227. class ConfigDictTests(TestCase):
  228. def test_get_set(self) -> None:
  229. cd = ConfigDict()
  230. self.assertRaises(KeyError, cd.get, b"foo", b"core")
  231. cd.set((b"core",), b"foo", b"bla")
  232. self.assertEqual(b"bla", cd.get((b"core",), b"foo"))
  233. cd.set((b"core",), b"foo", b"bloe")
  234. self.assertEqual(b"bloe", cd.get((b"core",), b"foo"))
  235. def test_get_boolean(self) -> None:
  236. cd = ConfigDict()
  237. cd.set((b"core",), b"foo", b"true")
  238. self.assertTrue(cd.get_boolean((b"core",), b"foo"))
  239. cd.set((b"core",), b"foo", b"false")
  240. self.assertFalse(cd.get_boolean((b"core",), b"foo"))
  241. cd.set((b"core",), b"foo", b"invalid")
  242. self.assertRaises(ValueError, cd.get_boolean, (b"core",), b"foo")
  243. def test_dict(self) -> None:
  244. cd = ConfigDict()
  245. cd.set((b"core",), b"foo", b"bla")
  246. cd.set((b"core2",), b"foo", b"bloe")
  247. self.assertEqual([(b"core",), (b"core2",)], list(cd.keys()))
  248. self.assertEqual(cd[(b"core",)], {b"foo": b"bla"})
  249. cd[b"a"] = b"b"
  250. self.assertEqual(cd[b"a"], b"b")
  251. def test_items(self) -> None:
  252. cd = ConfigDict()
  253. cd.set((b"core",), b"foo", b"bla")
  254. cd.set((b"core2",), b"foo", b"bloe")
  255. self.assertEqual([(b"foo", b"bla")], list(cd.items((b"core",))))
  256. def test_items_nonexistant(self) -> None:
  257. cd = ConfigDict()
  258. cd.set((b"core2",), b"foo", b"bloe")
  259. self.assertEqual([], list(cd.items((b"core",))))
  260. def test_sections(self) -> None:
  261. cd = ConfigDict()
  262. cd.set((b"core2",), b"foo", b"bloe")
  263. self.assertEqual([(b"core2",)], list(cd.sections()))
  264. def test_set_vs_add(self) -> None:
  265. cd = ConfigDict()
  266. # Test add() creates multivars
  267. cd.add((b"core",), b"foo", b"value1")
  268. cd.add((b"core",), b"foo", b"value2")
  269. self.assertEqual(
  270. [b"value1", b"value2"], list(cd.get_multivar((b"core",), b"foo"))
  271. )
  272. # Test set() replaces values
  273. cd.set((b"core",), b"foo", b"value3")
  274. self.assertEqual([b"value3"], list(cd.get_multivar((b"core",), b"foo")))
  275. self.assertEqual(b"value3", cd.get((b"core",), b"foo"))
  276. class StackedConfigTests(TestCase):
  277. def test_default_backends(self) -> None:
  278. StackedConfig.default_backends()
  279. @skipIf(sys.platform != "win32", "Windows specific config location.")
  280. def test_windows_config_from_path(self) -> None:
  281. from dulwich.config import get_win_system_paths
  282. install_dir = os.path.join("C:", "foo", "Git")
  283. self.overrideEnv("PATH", os.path.join(install_dir, "cmd"))
  284. with patch("os.path.exists", return_value=True):
  285. paths = set(get_win_system_paths())
  286. self.assertEqual(
  287. {
  288. os.path.join(os.environ.get("PROGRAMDATA"), "Git", "config"),
  289. os.path.join(install_dir, "etc", "gitconfig"),
  290. },
  291. paths,
  292. )
  293. @skipIf(sys.platform != "win32", "Windows specific config location.")
  294. def test_windows_config_from_reg(self) -> None:
  295. import winreg
  296. from dulwich.config import get_win_system_paths
  297. self.overrideEnv("PATH", None)
  298. install_dir = os.path.join("C:", "foo", "Git")
  299. with patch("winreg.OpenKey"):
  300. with patch(
  301. "winreg.QueryValueEx",
  302. return_value=(install_dir, winreg.REG_SZ),
  303. ):
  304. paths = set(get_win_system_paths())
  305. self.assertEqual(
  306. {
  307. os.path.join(os.environ.get("PROGRAMDATA"), "Git", "config"),
  308. os.path.join(install_dir, "etc", "gitconfig"),
  309. },
  310. paths,
  311. )
  312. class EscapeValueTests(TestCase):
  313. def test_nothing(self) -> None:
  314. self.assertEqual(b"foo", _escape_value(b"foo"))
  315. def test_backslash(self) -> None:
  316. self.assertEqual(b"foo\\\\", _escape_value(b"foo\\"))
  317. def test_newline(self) -> None:
  318. self.assertEqual(b"foo\\n", _escape_value(b"foo\n"))
  319. class FormatStringTests(TestCase):
  320. def test_quoted(self) -> None:
  321. self.assertEqual(b'" foo"', _format_string(b" foo"))
  322. self.assertEqual(b'"\\tfoo"', _format_string(b"\tfoo"))
  323. def test_not_quoted(self) -> None:
  324. self.assertEqual(b"foo", _format_string(b"foo"))
  325. self.assertEqual(b"foo bar", _format_string(b"foo bar"))
  326. class ParseStringTests(TestCase):
  327. def test_quoted(self) -> None:
  328. self.assertEqual(b" foo", _parse_string(b'" foo"'))
  329. self.assertEqual(b"\tfoo", _parse_string(b'"\\tfoo"'))
  330. def test_not_quoted(self) -> None:
  331. self.assertEqual(b"foo", _parse_string(b"foo"))
  332. self.assertEqual(b"foo bar", _parse_string(b"foo bar"))
  333. def test_nothing(self) -> None:
  334. self.assertEqual(b"", _parse_string(b""))
  335. def test_tab(self) -> None:
  336. self.assertEqual(b"\tbar\t", _parse_string(b"\\tbar\\t"))
  337. def test_newline(self) -> None:
  338. self.assertEqual(b"\nbar\t", _parse_string(b"\\nbar\\t\t"))
  339. def test_quote(self) -> None:
  340. self.assertEqual(b'"foo"', _parse_string(b'\\"foo\\"'))
  341. class CheckVariableNameTests(TestCase):
  342. def test_invalid(self) -> None:
  343. self.assertFalse(_check_variable_name(b"foo "))
  344. self.assertFalse(_check_variable_name(b"bar,bar"))
  345. self.assertFalse(_check_variable_name(b"bar.bar"))
  346. def test_valid(self) -> None:
  347. self.assertTrue(_check_variable_name(b"FOO"))
  348. self.assertTrue(_check_variable_name(b"foo"))
  349. self.assertTrue(_check_variable_name(b"foo-bar"))
  350. class CheckSectionNameTests(TestCase):
  351. def test_invalid(self) -> None:
  352. self.assertFalse(_check_section_name(b"foo "))
  353. self.assertFalse(_check_section_name(b"bar,bar"))
  354. def test_valid(self) -> None:
  355. self.assertTrue(_check_section_name(b"FOO"))
  356. self.assertTrue(_check_section_name(b"foo"))
  357. self.assertTrue(_check_section_name(b"foo-bar"))
  358. self.assertTrue(_check_section_name(b"bar.bar"))
  359. class SubmodulesTests(TestCase):
  360. def testSubmodules(self) -> None:
  361. cf = ConfigFile.from_file(
  362. BytesIO(
  363. b"""\
  364. [submodule "core/lib"]
  365. \tpath = core/lib
  366. \turl = https://github.com/phhusson/QuasselC.git
  367. """
  368. )
  369. )
  370. got = list(parse_submodules(cf))
  371. self.assertEqual(
  372. [
  373. (
  374. b"core/lib",
  375. b"https://github.com/phhusson/QuasselC.git",
  376. b"core/lib",
  377. )
  378. ],
  379. got,
  380. )
  381. def testMalformedSubmodules(self) -> None:
  382. cf = ConfigFile.from_file(
  383. BytesIO(
  384. b"""\
  385. [submodule "core/lib"]
  386. \tpath = core/lib
  387. \turl = https://github.com/phhusson/QuasselC.git
  388. [submodule "dulwich"]
  389. \turl = https://github.com/jelmer/dulwich
  390. """
  391. )
  392. )
  393. got = list(parse_submodules(cf))
  394. self.assertEqual(
  395. [
  396. (
  397. b"core/lib",
  398. b"https://github.com/phhusson/QuasselC.git",
  399. b"core/lib",
  400. )
  401. ],
  402. got,
  403. )
  404. class ApplyInsteadOfTests(TestCase):
  405. def test_none(self) -> None:
  406. config = ConfigDict()
  407. self.assertEqual(
  408. "https://example.com/", apply_instead_of(config, "https://example.com/")
  409. )
  410. def test_apply(self) -> None:
  411. config = ConfigDict()
  412. config.set(("url", "https://samba.org/"), "insteadOf", "https://example.com/")
  413. self.assertEqual(
  414. "https://samba.org/", apply_instead_of(config, "https://example.com/")
  415. )
  416. def test_apply_multiple(self) -> None:
  417. config = ConfigDict()
  418. config.add(("url", "https://samba.org/"), "insteadOf", "https://blah.com/")
  419. config.add(("url", "https://samba.org/"), "insteadOf", "https://example.com/")
  420. self.assertEqual(
  421. [b"https://blah.com/", b"https://example.com/"],
  422. list(config.get_multivar(("url", "https://samba.org/"), "insteadOf")),
  423. )
  424. self.assertEqual(
  425. "https://samba.org/", apply_instead_of(config, "https://example.com/")
  426. )
  427. def test_apply_preserves_case_in_subsection(self) -> None:
  428. """Test that mixed-case URLs (like those with access tokens) are preserved."""
  429. config = ConfigDict()
  430. # GitHub access tokens have mixed case that must be preserved
  431. url_with_token = "https://ghp_AbCdEfGhIjKlMnOpQrStUvWxYz1234567890@github.com/"
  432. config.set(("url", url_with_token), "insteadOf", "https://github.com/")
  433. # Apply the substitution
  434. result = apply_instead_of(config, "https://github.com/jelmer/dulwich.git")
  435. expected = "https://ghp_AbCdEfGhIjKlMnOpQrStUvWxYz1234567890@github.com/jelmer/dulwich.git"
  436. self.assertEqual(expected, result)
  437. # Verify the token case is preserved
  438. self.assertIn("ghp_AbCdEfGhIjKlMnOpQrStUvWxYz1234567890", result)
  439. class CaseInsensitiveConfigTests(TestCase):
  440. def test_case_insensitive(self) -> None:
  441. config = CaseInsensitiveOrderedMultiDict()
  442. config[("core",)] = "value"
  443. self.assertEqual("value", config[("CORE",)])
  444. self.assertEqual("value", config[("CoRe",)])
  445. self.assertEqual([("core",)], list(config.keys()))
  446. def test_multiple_set(self) -> None:
  447. config = CaseInsensitiveOrderedMultiDict()
  448. config[("core",)] = "value1"
  449. config[("core",)] = "value2"
  450. # The second set overwrites the first one
  451. self.assertEqual("value2", config[("core",)])
  452. self.assertEqual("value2", config[("CORE",)])
  453. def test_get_all(self) -> None:
  454. config = CaseInsensitiveOrderedMultiDict()
  455. config[("core",)] = "value1"
  456. config[("CORE",)] = "value2"
  457. config[("CoRe",)] = "value3"
  458. self.assertEqual(
  459. ["value1", "value2", "value3"], list(config.get_all(("core",)))
  460. )
  461. self.assertEqual(
  462. ["value1", "value2", "value3"], list(config.get_all(("CORE",)))
  463. )
  464. def test_delitem(self) -> None:
  465. config = CaseInsensitiveOrderedMultiDict()
  466. config[("core",)] = "value1"
  467. config[("CORE",)] = "value2"
  468. config[("other",)] = "value3"
  469. del config[("core",)]
  470. self.assertNotIn(("core",), config)
  471. self.assertNotIn(("CORE",), config)
  472. self.assertEqual("value3", config[("other",)])
  473. self.assertEqual(1, len(config))
  474. def test_len(self) -> None:
  475. config = CaseInsensitiveOrderedMultiDict()
  476. self.assertEqual(0, len(config))
  477. config[("core",)] = "value1"
  478. self.assertEqual(1, len(config))
  479. config[("CORE",)] = "value2"
  480. self.assertEqual(1, len(config)) # Same key, case insensitive
  481. def test_subsection_case_preserved(self) -> None:
  482. """Test that subsection names preserve their case."""
  483. config = CaseInsensitiveOrderedMultiDict()
  484. # Section names should be case-insensitive, but subsection names should preserve case
  485. config[("url", "https://Example.COM/Path")] = "value1"
  486. # Can retrieve with different case section name
  487. self.assertEqual("value1", config[("URL", "https://Example.COM/Path")])
  488. self.assertEqual("value1", config[("url", "https://Example.COM/Path")])
  489. # But not with different case subsection name
  490. with self.assertRaises(KeyError):
  491. config[("url", "https://example.com/path")]
  492. # Verify the stored key preserves subsection case
  493. stored_keys = list(config.keys())
  494. self.assertEqual(1, len(stored_keys))
  495. self.assertEqual(("url", "https://Example.COM/Path"), stored_keys[0])
  496. config[("other",)] = "value3"
  497. self.assertEqual(2, len(config))
  498. def test_make_from_dict(self) -> None:
  499. original = {("core",): "value1", ("other",): "value2"}
  500. config = CaseInsensitiveOrderedMultiDict.make(original)
  501. self.assertEqual("value1", config[("core",)])
  502. self.assertEqual("value1", config[("CORE",)])
  503. self.assertEqual("value2", config[("other",)])
  504. def test_make_from_self(self) -> None:
  505. config1 = CaseInsensitiveOrderedMultiDict()
  506. config1[("core",)] = "value"
  507. config2 = CaseInsensitiveOrderedMultiDict.make(config1)
  508. self.assertIs(config1, config2)
  509. def test_make_invalid_type(self) -> None:
  510. self.assertRaises(TypeError, CaseInsensitiveOrderedMultiDict.make, "invalid")
  511. def test_get_with_default(self) -> None:
  512. config = CaseInsensitiveOrderedMultiDict()
  513. config[("core",)] = "value"
  514. self.assertEqual("value", config.get(("core",)))
  515. self.assertEqual("value", config.get(("CORE",)))
  516. self.assertEqual("default", config.get(("missing",), "default"))
  517. # Test SENTINEL behavior
  518. result = config.get(("missing",))
  519. self.assertIsInstance(result, CaseInsensitiveOrderedMultiDict)
  520. self.assertEqual(0, len(result))
  521. def test_setdefault(self) -> None:
  522. config = CaseInsensitiveOrderedMultiDict()
  523. # Set new value
  524. result1 = config.setdefault(("core",), "value1")
  525. self.assertEqual("value1", result1)
  526. self.assertEqual("value1", config[("core",)])
  527. # Try to set again with different case - should return existing
  528. result2 = config.setdefault(("CORE",), "value2")
  529. self.assertEqual("value1", result2)
  530. self.assertEqual("value1", config[("core",)])
  531. def test_values(self) -> None:
  532. config = CaseInsensitiveOrderedMultiDict()
  533. config[("core",)] = "value1"
  534. config[("other",)] = "value2"
  535. config[("CORE",)] = "value3" # Overwrites previous core value
  536. self.assertEqual({"value3", "value2"}, set(config.values()))
  537. def test_items_iteration(self) -> None:
  538. config = CaseInsensitiveOrderedMultiDict()
  539. config[("core",)] = "value1"
  540. config[("other",)] = "value2"
  541. config[("CORE",)] = "value3"
  542. items = list(config.items())
  543. self.assertEqual(3, len(items))
  544. self.assertEqual((("core",), "value1"), items[0])
  545. self.assertEqual((("other",), "value2"), items[1])
  546. self.assertEqual((("CORE",), "value3"), items[2])
  547. def test_str_keys(self) -> None:
  548. config = CaseInsensitiveOrderedMultiDict()
  549. config["core"] = "value"
  550. self.assertEqual("value", config["CORE"])
  551. self.assertEqual("value", config["CoRe"])
  552. def test_nested_tuple_keys(self) -> None:
  553. config = CaseInsensitiveOrderedMultiDict()
  554. config[("branch", "master")] = "value"
  555. # Section names are case-insensitive
  556. self.assertEqual("value", config[("BRANCH", "master")])
  557. self.assertEqual("value", config[("Branch", "master")])
  558. # But subsection names are case-sensitive
  559. with self.assertRaises(KeyError):
  560. config[("branch", "MASTER")]
  561. class ConfigFileSetTests(TestCase):
  562. def test_set_replaces_value(self) -> None:
  563. # Test that set() replaces the value instead of appending
  564. cf = ConfigFile()
  565. cf.set((b"core",), b"sshCommand", b"ssh -i ~/.ssh/id_rsa1")
  566. cf.set((b"core",), b"sshCommand", b"ssh -i ~/.ssh/id_rsa2")
  567. # Should only have one value
  568. self.assertEqual(b"ssh -i ~/.ssh/id_rsa2", cf.get((b"core",), b"sshCommand"))
  569. # When written to file, should only have one entry
  570. f = BytesIO()
  571. cf.write_to_file(f)
  572. content = f.getvalue()
  573. self.assertEqual(1, content.count(b"sshCommand"))
  574. self.assertIn(b"sshCommand = ssh -i ~/.ssh/id_rsa2", content)
  575. self.assertNotIn(b"id_rsa1", content)