1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
|
#!/usr/bin/python3
# Author: Benjamin Drung <bdrung@ubuntu.com>
"""Test debconf configuration."""
import contextlib
import os
import pathlib
import re
import subprocess
import sys
import typing
import unittest
class TestDebconf(unittest.TestCase):
"""Test debconf configuration."""
etc_localtime = pathlib.Path("/etc/localtime")
etc_timezone = pathlib.Path("/etc/timezone")
def setUp(self) -> None:
self.orig_timezone = self._get_timezone()
with contextlib.suppress(FileNotFoundError):
self.etc_timezone.unlink()
def tearDown(self) -> None:
self._set_timezone(self.orig_timezone)
@staticmethod
def _call_debconf_set_selections(selections: str) -> None:
subprocess.run(
["debconf-set-selections"], check=True, encoding="utf-8", input=selections
)
@staticmethod
def _call_dpkg_reconfigure() -> None:
subprocess.run(
["dpkg-reconfigure", "--frontend", "noninteractive", "tzdata"], check=True
)
@staticmethod
def _call_postinst(preversion: str) -> None:
subprocess.run(
["/var/lib/dpkg/info/tzdata.postinst", "configure", preversion], check=True
)
@staticmethod
def _get_debconf_selections() -> dict[str, str]:
debconf_show = subprocess.run(
["debconf-show", "tzdata"], capture_output=True, check=True, text=True
)
debconf_re = re.compile("^[ *] ([^:]+): *(.*)$", flags=re.MULTILINE)
return dict(debconf_re.findall(debconf_show.stdout))
def _get_selection(self) -> str:
selections = self._get_debconf_selections()
area = selections["tzdata/Areas"]
zone = selections[f"tzdata/Zones/{area}"]
return f"{area}/{zone}"
def _get_timezone(self) -> str:
absolute_path = self.etc_localtime.parent / self.etc_localtime.readlink()
timezone = pathlib.Path(os.path.normpath(absolute_path))
return str(timezone.relative_to("/usr/share/zoneinfo"))
def _set_timezone(self, timezone: typing.Union[pathlib.Path, str]) -> None:
with contextlib.suppress(FileNotFoundError):
self.etc_localtime.unlink()
if isinstance(timezone, str):
target = pathlib.Path("/usr/share/zoneinfo") / timezone
else:
target = timezone
self.etc_localtime.symlink_to(target)
@staticmethod
def _reset_debconf() -> None:
subprocess.run(
["debconf-communicate", "tzdata"],
check=True,
encoding="utf-8",
env={"DEBIAN_FRONTEND": "noninteractive"},
input="RESET tzdata/Areas\nRESET tzdata/Zones/Etc\n",
stdout=subprocess.DEVNULL,
)
def test_broken_symlink(self) -> None:
"""Test pointing /etc/localtime to an invalid location."""
self._set_timezone(pathlib.Path("/bin/sh"))
self._reset_debconf()
self._call_dpkg_reconfigure()
self.assertEqual(self._get_timezone(), "Etc/UTC")
self.assertEqual(self._get_selection(), "Etc/UTC")
def test_broken_symlink_but_debconf_preseed(self) -> None:
"""Test broken /etc/localtime but existing debconf answers."""
self._set_timezone(pathlib.Path("/bin/sh"))
self._call_debconf_set_selections(
"tzdata tzdata/Areas select Pacific\n"
"tzdata tzdata/Zones/Pacific select Yap\n"
)
self._call_dpkg_reconfigure()
self.assertEqual(self._get_timezone(), "Pacific/Yap")
self.assertEqual(self._get_selection(), "Pacific/Yap")
def test_etc_localtime_precedes_debconf_preseed(self) -> None:
"""Test dpkg-reconfigure uses /etc/localtime over preseed."""
self._set_timezone("Asia/Jerusalem")
self._call_debconf_set_selections(
"tzdata tzdata/Areas select Australia\n"
"tzdata tzdata/Zones/Australia select Sydney\n"
)
self._call_dpkg_reconfigure()
self.assertEqual(self._get_timezone(), "Asia/Jerusalem")
self.assertEqual(self._get_selection(), "Asia/Jerusalem")
def test_default_to_utc(self) -> None:
"""Test dpkg-reconfigure defaults to Etc/UTC."""
self.etc_localtime.unlink()
self._reset_debconf()
self._call_dpkg_reconfigure()
self.assertEqual(self._get_timezone(), "Etc/UTC")
self.assertEqual(self._get_selection(), "Etc/UTC")
def test_postinst_removes_etc_timezone_on_upgrade(self) -> None:
"""Test postinst removes existing /etc/timezone on upgrade."""
for version in [
"2024a-0+deb11u1",
"2025b-0+deb11u2",
"2025b-0+deb12u2",
"2025b-0ubuntu0.24.04.1",
"2025b-3ubuntu1",
"2025b-4+deb13u1",
"2026b-0ubuntu0.25.10.1",
"2030a-1+deb13u1",
"2030a-0ubuntu0.24.04.1",
"2025c-1",
]:
with self.subTest(version=version):
self.etc_timezone.write_bytes(b"Europe/Oslo")
self._call_postinst(version)
self.assertFalse(self.etc_timezone.exists())
for version in ["2025c-2", "2026b-0ubuntu0.26.04.1", "2030a-1"]:
with self.subTest(version=version):
self.etc_timezone.write_bytes(b"Europe/Oslo")
self._call_postinst(version)
self.assertTrue(self.etc_timezone.exists())
def test_reconfigure_does_not_create_etc_timezone(self) -> None:
"""Test dpkg-reconfigure does not create /etc/timezone."""
self._set_timezone("America/New_York")
self._call_dpkg_reconfigure()
self.assertEqual(self._get_timezone(), "America/New_York")
self.assertFalse(self.etc_timezone.exists())
self.assertEqual(self._get_selection(), "America/New_York")
def test_reconfigure_does_no_modify_etc_timezone(self) -> None:
"""Test dpkg-reconfigure does not modify existing /etc/timezone."""
self._set_timezone("Europe/Oslo")
self.etc_timezone.write_bytes(b"Foo/Bar")
self._call_dpkg_reconfigure()
self.assertEqual(self._get_timezone(), "Europe/Oslo")
self.assertEqual(self._get_selection(), "Europe/Oslo")
self.assertTrue(self.etc_timezone.exists())
self.assertEqual(self.etc_timezone.read_bytes(), b"Foo/Bar")
def test_reconfigure_symlinked_timezone(self) -> None:
"""Test dpkg-reconfigure for symlinked timezone."""
self._set_timezone("Arctic/Longyearbyen")
self._call_dpkg_reconfigure()
self.assertEqual(self._get_timezone(), "Arctic/Longyearbyen")
self.assertEqual(self._get_selection(), "Arctic/Longyearbyen")
def test_relative_symlink(self) -> None:
"""Test relative symlink /etc/localtime."""
timezone = pathlib.Path("../usr/share/zoneinfo/Europe/Berlin")
self._set_timezone(timezone)
self._call_dpkg_reconfigure()
self.assertEqual(self._get_timezone(), "Europe/Berlin")
self.assertEqual(self.etc_localtime.readlink(), timezone)
self.assertEqual(self._get_selection(), "Europe/Berlin")
def test_update_obsolete_timezone(self) -> None:
"""Test updating obsolete timezone to current one."""
self._set_timezone("Mideast/Riyadh88")
self._call_dpkg_reconfigure()
self.assertEqual(self._get_timezone(), "Asia/Riyadh")
self.assertEqual(self._get_selection(), "Asia/Riyadh")
def test_preesed(self) -> None:
"""Test preseeding answers with non-existing /etc/localtime."""
self.etc_localtime.unlink()
self._call_debconf_set_selections(
"tzdata tzdata/Areas select Europe\n"
"tzdata tzdata/Zones/Europe select Berlin\n"
)
self._call_dpkg_reconfigure()
self.assertEqual(self._get_timezone(), "Europe/Berlin")
self.assertEqual(self._get_selection(), "Europe/Berlin")
def main() -> None:
"""Run unit tests in verbose mode."""
argv = sys.argv.copy()
argv.insert(1, "-v")
unittest.main(argv=argv)
if __name__ == "__main__":
main()
|