diff options
| author | Craig Jennings <c@cjennings.net> | 2026-10-06 13:58:40 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-10-06 13:58:40 -0600 |
| commit | 5189b7cdc7647f92382ddda3ca7d1a30fcffa4ce (patch) | |
| tree | 316016bacf6534fe1b4f1168e2b40491be4b04f3 | |
| parent | 385b0021d0ad028c1bc63fcc8e81de0eb55db4ab (diff) | |
| download | archsetup-5189b7cdc7647f92382ddda3ca7d1a30fcffa4ce.tar.gz archsetup-5189b7cdc7647f92382ddda3ca7d1a30fcffa4ce.zip | |
feat(audio): ship the Framework 13 AMD internal-mic workaround
On the Framework 13 with AMD Ryzen AI 300 the kernel's ACP PDM digital-mic path returns a constant full-scale rail instead of audio. With UCM on, that broken DMIC is the only internal mic PipeWire offers, so the built-in microphone is dead in every app. The new step installs a WirePlumber drop-in that turns UCM off and disables the standalone PDM card so nothing can select it. Without UCM the ALC285 falls back to its analog-stereo profile, whose internal-mic pin carries real audio.
I gated it on DMI rather than on the acp-pdm-mach card or the hostname. The card exists on every AMD laptop with a PDM mic and stays present after the upstream fix lands, so its presence says nothing about whether the workaround belongs. The fix belongs to the board, not to one machine. The match needs all three of Framework, "Laptop 13" and "AMD Ryzen AI 300". That rules out the Framework Desktop ("AI Max 300") along with the Intel 13, the Ryzen 7040 13 and the 16. The rule inside the drop-in matches the PDM card by name pattern for the same reason: the gate covers the whole board family, so the rule can't pin the one PCI path velox enumerates.
The drop-in ships as a file under configs/, so a re-run refreshes it and deleting it plus a wireplumber restart reverts the machine. The installer doesn't restart wireplumber itself: there's no user session bus during install, so the drop-in applies when wireplumber next starts.
The audio failure taxonomy gains a row for the bug and a Cluster 5 note, so the audio doctor's use-ucm remedies treat the installed drop-in as the fix rather than as an offending config to remove.
| -rwxr-xr-x | archsetup | 56 | ||||
| -rw-r--r-- | configs/wireplumber/50-disable-ucm.conf | 22 | ||||
| -rw-r--r-- | docs/design/2026-07-10-audio-failure-taxonomy.org | 2 | ||||
| -rw-r--r-- | tests/installer-steps/test_configure_framework_amd_internal_mic.py | 277 |
4 files changed, 357 insertions, 0 deletions
@@ -2781,6 +2781,7 @@ desktop_environment() { pipewire-docs pamixer pulsemixer easyeffects ffmpeg rtkit; do pacman_install "$software" done + configure_framework_amd_internal_mic # disable the pc speaker beep rmmod pcspkr 2>/dev/null || true echo "blacklist pcspkr" > /etc/modprobe.d/nobeep.conf @@ -2900,6 +2901,61 @@ desktop_environment() { (systemctl enable cups.service >> "$logfile" 2>&1) || error_warn "$action" "$?" } +# Is this a Framework Laptop 13 on AMD Ryzen AI 300 -- the board whose ACP PDM +# digital mic the kernel gets wrong? $1 is the DMI directory, defaulting to the +# system path so the gate runs against fixtures. +# +# The gate reads the model string rather than looking for the acp-pdm-mach +# card: that card exists on every AMD laptop with a PDM mic, fine or broken, +# and it stays present after the upstream fix lands, so its presence says +# nothing about whether the workaround belongs. The hostname is out for the +# usual reason -- the fix belongs to the board, not to velox. +# +# All three facts are needed. "Framework" lives only in sys_vendor. "Laptop +# 13" rules out the Framework Desktop (ratio, which has no internal mic to +# break) and the Framework 16. "AMD Ryzen AI 300" rules out the Intel 13 (SOF +# stack, no ACP path) and the Ryzen 7040 13 (Phoenix; the workaround is +# unverified there), and it does not match ratio's "AMD Ryzen AI Max 300". +is_framework_13_ryzen_ai_300() { + local dmi="${1:-/sys/class/dmi/id}" + local vendor model + vendor="$(cat "$dmi/sys_vendor" 2>/dev/null)" + model="$(cat "$dmi/product_name" 2>/dev/null)" + printf '%s' "$vendor" | grep -qi "framework" || return 1 + printf '%s' "$model" | grep -qi "laptop 13" || return 1 + printf '%s' "$model" | grep -q "AMD Ryzen AI 300" || return 1 + return 0 +} + +configure_framework_amd_internal_mic() { + # Internal-mic workaround for the Framework 13 on AMD Ryzen AI 300, matched + # by DMI (see is_framework_13_ryzen_ai_300). A known, still-open kernel bug + # in the ACP PDM digital-mic path makes the DMIC card (acp-pdm-mach) emit a + # constant full-scale rail instead of audio, and with UCM on that broken + # DMIC is the only internal mic PipeWire offers, so the built-in microphone + # is dead in every app (velox, 2026-10-06; Framework community threads + # 81866 and 75748). The shipped WirePlumber drop-in turns UCM off, which + # makes the ALC285 fall back to its analog-stereo profile whose Internal + # Microphone pin carries real audio, and disables the standalone PDM card + # so nothing can select it. It is the shipped layer: a re-run refreshes it, + # and removing it plus a wireplumber restart reverts the machine. + # The installer cannot restart wireplumber itself -- it is a user service + # and there is no user session bus during install -- so the drop-in takes + # effect when wireplumber next starts (first login on a fresh install; + # `systemctl --user restart wireplumber` on a repair re-run). + # Lives beside the pipewire/wireplumber package install because that is + # what creates the daemon the drop-in configures. When the upstream fix + # lands, the DMIC records real audio with UCM back on, and this step goes. + # $1/$2 are the wireplumber conf.d dir and the DMI dir, defaulting to the + # system paths so tests can run against fixtures. + local confd="${1:-/etc/wireplumber/wireplumber.conf.d}" dmi="${2:-/sys/class/dmi/id}" + is_framework_13_ryzen_ai_300 "$dmi" || return 0 + action="installing the Framework 13 AMD internal-mic workaround (UCM off)" && display "task" "$action" + local src="${user_archsetup_dir:-/home/$username/code/archsetup}/configs/wireplumber/50-disable-ucm.conf" + install -Dm644 "$src" "$confd/50-disable-ucm.conf" >> "$logfile" 2>&1 \ + || error_warn "$action" "$?" +} + ### Gaming gaming() { display "title" "Gaming" diff --git a/configs/wireplumber/50-disable-ucm.conf b/configs/wireplumber/50-disable-ucm.conf new file mode 100644 index 0000000..99cf8cb --- /dev/null +++ b/configs/wireplumber/50-disable-ucm.conf @@ -0,0 +1,22 @@ +# Framework Laptop 13 (AMD Ryzen AI 300 Series): the ACP PDM digital-mic driver +# returns saturated full-scale garbage, a known and still-open kernel bug on +# Strix Point and Krackan Point. Framework community threads: +# https://community.frame.work/t/81866 (Krackan Point, SOF/ACP timeline) +# https://community.frame.work/t/75748 (Ryzen AI 340, Fedora 42) +# With UCM on, PipeWire exposes only that broken DMIC as the internal mic. Turning +# UCM off makes the ALC285 codec fall back to its analog-stereo profile, whose +# Internal Microphone pin works. Remove this file and restart wireplumber to revert. +monitor.alsa.properties = { + alsa.use-ucm = false +} + +# The standalone ACP PDM card (the broken digital mic) still appears without UCM; +# disable it so no app or default-selection can pick it. The ~ prefix makes the +# match a regex over the card name rather than one PCI path (pci-0000_c1_00.5 on +# velox), so the same board with a renumbered bus is still caught. +monitor.alsa.rules = [ + { + matches = [ { device.name = "~alsa_card.pci-.*-platform-acp-pdm-mach" } ] + actions = { update-props = { device.disabled = true } } + } +] diff --git a/docs/design/2026-07-10-audio-failure-taxonomy.org b/docs/design/2026-07-10-audio-failure-taxonomy.org index efb6e0a..bea0f65 100644 --- a/docs/design/2026-07-10-audio-failure-taxonomy.org +++ b/docs/design/2026-07-10-audio-failure-taxonomy.org @@ -29,6 +29,7 @@ Built 2026-07-10 from a parallel research sweep across eight layers (kernel/driv - AMD ACP6x DMIC needs a DMI quirk, not firmware (both). Ryzen laptop internal array mic absent or silent; sof-firmware changes nothing. The acp6x mic array needs the laptop's DMI entry in the =SND_SOC_AMD_YC_MACH= quirk table. Fix: newer kernel with the board's DMI quirk, or add the entry and rebuild. [[https://bbs.archlinux.org/viewtopic.php?id=311093][arch 311093]] - Missing NHLT ACPI table in firmware (both). No DMIC; dmesg shows "NHLT table not found". The platform firmware omits the NHLT table, so SOF can't discover the digital-mic count. Fix: flash firmware that supplies NHLT, or override the dmic count via SOF modprobe params. [[https://github.com/MrChromebox/firmware/issues/920][mrchromebox 920]] - Brand-new AMD ACP7.0 (Strix Halo) has no mic driver/UCM yet (both). Internal DMIC non-functional on very recent AMD hardware. The kernel ACP7.0 SOF driver and UCM profile weren't upstreamed yet. Fix: wait for the kernel + alsa-ucm-conf that add ACP7.0 support. [[https://github.com/alsa-project/alsa-ucm-conf/issues/745][ucm-conf 745]] +- AMD ACP PDM digital mic emits a full-scale rail, Framework 13 on Ryzen AI 300 (yes). Internal mic dead in every app; a raw capture of =hw:acppdmmach= reads rms 0.99999 and peak 1.0 on every sample, which is a rail, not silence and not the privacy switch. A still-open kernel bug in the ACP PDM path on Strix Point and Krackan Point (no AMD SOF firmware exists for the platform); with UCM on, alsa-ucm-conf links the broken =acp-pdm-mach= card into the ALC285 codec card, so that DMIC is the only internal mic PipeWire offers. Fix: =alsa.use-ucm = false= plus a =monitor.alsa.rules= entry disabling the =acp-pdm-mach= card, shipped by archsetup as =/etc/wireplumber/wireplumber.conf.d/50-disable-ucm.conf= on this board; the ALC285 falls back to analog-stereo, whose Internal Microphone pin carries real audio (drop the source volume, the analog path stacks +30 dB capture on +30 dB boost). Re-test the DMIC after kernel updates; once upstream fixes it, remove the file and UCM can go back on. Found on velox 2026-10-06. [[https://community.frame.work/t/81866][framework 81866]], [[https://community.frame.work/t/75748][framework 75748]] ** ALSA (profiles, UCM, mixer, pins, state) @@ -180,6 +181,7 @@ Probe: kernel-vs-graph (the mic-unrecognized rule) plus WirePlumber liveness. Th - ACP vs UCM detection mismatch :: AUTO — set =use-acp=/=use-ucm= plus restart wireplumber. - Missing UCM capture profile :: AUTO — fall back to ACP; GUIDE the UCM patch/install. +- AMD ACP PDM rail (Framework 13 on Ryzen AI 300) :: PRIV — install the =50-disable-ucm.conf= drop-in (UCM off plus the =acp-pdm-mach= card disabled) under =/etc= and restart wireplumber; archsetup ships it on this board. The two use-ucm remedies above and the drop-in removals in this cluster must treat that installed file as the fix, not as an offending config: flipping =use-ucm= back on, or deleting the file, puts the railing DMIC back as the only internal mic. The doctor should recognise the rail itself (a raw =hw:acppdmmach= capture at rms 0.99999) before it touches UCM on this hardware. - WirePlumber fails to start on old config :: AUTO — remove the user drop-in plus restart (PRIV if the file is under =/etc=). - pipewire-pulse dead or wrong socket :: AUTO — restart the user services; check XDG_RUNTIME_DIR. - Broken ~/.config/wireplumber override :: AUTO — remove/repair the drop-in plus restart. diff --git a/tests/installer-steps/test_configure_framework_amd_internal_mic.py b/tests/installer-steps/test_configure_framework_amd_internal_mic.py new file mode 100644 index 0000000..7a45fb1 --- /dev/null +++ b/tests/installer-steps/test_configure_framework_amd_internal_mic.py @@ -0,0 +1,277 @@ +"""Test configure_framework_amd_internal_mic and its DMI gate. + +The Framework Laptop 13 on AMD Ryzen AI 300 (Strix Point and Krackan Point +alike) has a known, still-open kernel bug in the ACP PDM digital-mic path: the +DMIC card (acp-pdm-mach, hw:acppdmmach) returns a constant full-scale rail +(rms 0.99999) instead of audio. With UCM on, alsa-ucm-conf links that card +into the ALC285 codec card, so the only "internal mic" PipeWire offers IS the +broken DMIC, and the built-in microphone is dead in every app. Found on velox +2026-10-06 after the 08-13 reinstall; Framework community threads 81866 and +75748 document it across several HX 370 owners. + +The workaround is a WirePlumber drop-in that turns UCM off (the ALC285 falls +back to its analog-stereo profile, whose Internal Microphone pin carries real +audio) and disables the standalone PDM card so nothing can pick it. The step +installs the shipped copy of that drop-in, and only on this hardware: the +gate reads DMI, not the hostname, because the fix belongs to the board, not +to velox. + +The gate has to be exact in both directions. The same bug does not exist on +the Intel Framework 13 (SOF stack, not ACP), and the Framework Desktop (ratio, +"AMD Ryzen AI Max 300") has no internal mic at all, so a vendor-only or +"AMD"-only match would drop a UCM-off file onto machines whose audio is fine +and silently flatten their codec profiles. + +Method: sed-extract both functions from the real `archsetup`, point the step +at a temp conf.d dir and a fixture DMI dir, and fake display / error_warn. +The shipped drop-in is read from the real configs/ tree so the test pins the +content the installer actually delivers. + +Run from repo root: + python3 -m unittest tests.installer-steps.test_configure_framework_amd_internal_mic +""" + +import os +import re +import stat +import subprocess +import tempfile +import textwrap +import unittest + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +ARCHSETUP = os.path.join(REPO_ROOT, "archsetup") +DROPIN_SRC = os.path.join(REPO_ROOT, "configs", "wireplumber", "50-disable-ucm.conf") +DROPIN_NAME = "50-disable-ucm.conf" +# The broken DMIC card as velox names it, and the codec card that must stay. +VELOX_PDM_CARD = "alsa_card.pci-0000_c1_00.5-platform-acp-pdm-mach" +VELOX_CODEC_CARD = "alsa_card.pci-0000_c1_00.6" + +# DMI fixtures. The product strings are what the real machines report. +VELOX = dict(sys_vendor="Framework", product_name="Laptop 13 (AMD Ryzen AI 300 Series)") +RATIO = dict(sys_vendor="Framework", product_name="Desktop (AMD Ryzen AI Max 300 Series)") +INTEL_13 = dict(sys_vendor="Framework", product_name="Laptop (13th Gen Intel Core)") +AMD_7040_13 = dict(sys_vendor="Framework", product_name="Laptop 13 (AMD Ryzen 7040Series)") +AMD_16 = dict(sys_vendor="Framework", product_name="Laptop 16 (AMD Ryzen 7040 Series)") +OTHER_VENDOR = dict(sys_vendor="LENOVO", product_name="Laptop 13 (AMD Ryzen AI 300 Series)") + + +def write_dmi(dmi_dir, fields): + os.makedirs(dmi_dir, exist_ok=True) + for name, value in fields.items(): + with open(os.path.join(dmi_dir, name), "w") as f: + f.write(value + "\n") + + +def gate(dmi_dir): + script = textwrap.dedent(f"""\ + source <(sed -n '/^is_framework_13_ryzen_ai_300() {{/,/^}}/p' "{ARCHSETUP}") + if is_framework_13_ryzen_ai_300 "{dmi_dir}"; then echo "GATE=yes"; else echo "GATE=no"; fi + exit 0 + """) + r = subprocess.run(["bash", "-c", script], capture_output=True, text=True, timeout=10) + m = re.search(r"^GATE=(yes|no)$", r.stdout, re.M) + assert m, "no GATE line in output: %r / %r" % (r.stdout, r.stderr) + return m.group(1) == "yes" + + +def run(confd, dmi_dir, src_root=REPO_ROOT): + # The step sends install's output to $logfile; the fakes report on stdout + # so the test can see the warning and the function's own return code. + script = textwrap.dedent(f"""\ + logfile=/dev/null + action="" + username=nobody + display() {{ :; }} + error_warn() {{ echo "WARN: $1"; return 1; }} + user_archsetup_dir="{src_root}" + source <(sed -n '/^is_framework_13_ryzen_ai_300() {{/,/^}}/p' "{ARCHSETUP}") + source <(sed -n '/^configure_framework_amd_internal_mic() {{/,/^}}/p' "{ARCHSETUP}") + configure_framework_amd_internal_mic "{confd}" "{dmi_dir}" + echo "RC=$?" + exit 0 + """) + return subprocess.run( + ["bash", "-c", script], capture_output=True, text=True, timeout=10, + ) + + +def rc_of(r): + m = re.search(r"^RC=(\d+)$", r.stdout, re.M) + assert m, "no RC line in output: %r / %r" % (r.stdout, r.stderr) + return int(m.group(1)) + + +class Framework13RyzenAi300Gate(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.dmi = os.path.join(self.tmp.name, "dmi") + + def tearDown(self): + self.tmp.cleanup() + + # ------------------------------------------------------------ normal ---- + def test_matches_the_framework_13_on_ryzen_ai_300(self): + write_dmi(self.dmi, VELOX) + self.assertTrue(gate(self.dmi)) + + # ---------------------------------------------------------- boundary ---- + def test_rejects_the_framework_desktop_on_ryzen_ai_max(self): + """ratio: same vendor, "AMD Ryzen AI" in the name, no internal mic. + + "AMD Ryzen AI Max 300" must not satisfy an "AMD Ryzen AI 300" match. + """ + write_dmi(self.dmi, RATIO) + self.assertFalse(gate(self.dmi)) + + def test_rejects_the_framework_13_on_intel(self): + """Intel's SOF stack has no ACP PDM path; the bug does not exist there.""" + write_dmi(self.dmi, INTEL_13) + self.assertFalse(gate(self.dmi)) + + def test_rejects_the_framework_13_on_ryzen_7040(self): + """Phoenix is not Ryzen AI 300; the workaround is unverified there.""" + write_dmi(self.dmi, AMD_7040_13) + self.assertFalse(gate(self.dmi)) + + def test_rejects_the_framework_16(self): + write_dmi(self.dmi, AMD_16) + self.assertFalse(gate(self.dmi)) + + def test_rejects_another_vendor_with_the_same_model_string(self): + write_dmi(self.dmi, OTHER_VENDOR) + self.assertFalse(gate(self.dmi)) + + def test_rejects_a_missing_dmi_directory(self): + self.assertFalse(gate(os.path.join(self.tmp.name, "nope"))) + + +class ConfigureFrameworkAmdInternalMic(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.dmi = os.path.join(self.tmp.name, "dmi") + self.confd = os.path.join(self.tmp.name, "wireplumber.conf.d") + self.dropin = os.path.join(self.confd, DROPIN_NAME) + + def tearDown(self): + self.tmp.cleanup() + + # ------------------------------------------------------------ normal ---- + def test_framework_13_ryzen_ai_300_gets_the_shipped_dropin(self): + write_dmi(self.dmi, VELOX) + r = run(self.confd, self.dmi) + self.assertEqual(rc_of(r), 0, r.stdout) + self.assertTrue(os.path.exists(self.dropin), "drop-in not installed") + with open(self.dropin) as f, open(DROPIN_SRC) as src: + self.assertEqual(f.read(), src.read(), + "the installed file must be the shipped file, byte for byte") + + def test_dropin_mode_is_0644(self): + """Owner-writable, world-readable, like every file under /etc.""" + write_dmi(self.dmi, VELOX) + run(self.confd, self.dmi) + mode = stat.S_IMODE(os.stat(self.dropin).st_mode) + self.assertEqual(mode, 0o644) + + def test_shipped_dropin_turns_ucm_off(self): + """Pin the shipped content, not just the copy: use-ucm must be an + active setting, not a commented example.""" + with open(DROPIN_SRC) as f: + body = f.read() + active = [ln for ln in body.splitlines() + if "alsa.use-ucm" in ln and not ln.lstrip().startswith("#")] + self.assertEqual(len(active), 1, body) + self.assertRegex(active[0], r"alsa\.use-ucm\s*=\s*false") + + def test_shipped_dropin_disables_the_standalone_pdm_card(self): + """Without UCM the broken DMIC still shows up as its own card; the + rule has to catch it and disable it, or an app or the default + selection can still land on the full-scale rail. + + The gate fires on every Framework 13 on Ryzen AI 300, so the rule + must match the card by name pattern (WirePlumber's ~regex form), not + by the one PCI path velox happens to enumerate: a renumbered bus on + the same board would otherwise get UCM turned off with the broken + card left selectable. WirePlumber's regex match is unanchored, so + re.search mirrors it. + """ + with open(DROPIN_SRC) as f: + body = f.read() + m = re.search(r'device\.name\s*=\s*"~([^"]+)"', body) + self.assertIsNotNone(m, "the rule must match device.name with a ~regex, " + "not a literal PCI path") + assert m is not None # narrows the type; the assertion above is the test + pattern = m.group(1) + self.assertTrue(re.search(pattern, VELOX_PDM_CARD), + f"{pattern!r} must match velox's card {VELOX_PDM_CARD!r}") + self.assertTrue(re.search(pattern, "alsa_card.pci-0000_c3_00.5-platform-acp-pdm-mach"), + "the same card on another bus number must match too") + self.assertFalse(re.search(pattern, VELOX_CODEC_CARD), + "the ALC285 codec card carries the working mic and must not be disabled") + self.assertRegex(body, r"device\.disabled\s*=\s*true") + + # ---------------------------------------------------------- boundary ---- + def test_other_hardware_writes_nothing(self): + for fixture in (RATIO, INTEL_13, AMD_7040_13, AMD_16, OTHER_VENDOR): + with self.subTest(product=fixture["product_name"], vendor=fixture["sys_vendor"]): + write_dmi(self.dmi, fixture) + r = run(self.confd, self.dmi) + self.assertEqual(rc_of(r), 0, r.stdout) + self.assertFalse(os.path.exists(self.confd), + "no conf.d may be created on hardware the step does not apply to") + self.assertNotIn("WARN:", r.stdout) + + def test_missing_dmi_directory_is_a_quiet_no_op(self): + """A VM or a stripped sysfs reports no DMI; that is not an error.""" + r = run(self.confd, os.path.join(self.tmp.name, "nope")) + self.assertEqual(rc_of(r), 0, r.stdout) + self.assertFalse(os.path.exists(self.confd)) + self.assertNotIn("WARN:", r.stdout) + + def test_running_twice_leaves_one_identical_dropin(self): + write_dmi(self.dmi, VELOX) + run(self.confd, self.dmi) + with open(self.dropin) as f: + first = f.read() + r = run(self.confd, self.dmi) + self.assertEqual(rc_of(r), 0, r.stdout) + with open(self.dropin) as f: + second = f.read() + self.assertEqual(first, second) + self.assertEqual(os.listdir(self.confd), [DROPIN_NAME]) + + def test_absent_confd_is_created(self): + """wireplumber's package ships /etc/wireplumber/ but not conf.d/; the + step must create the nested path rather than fail on it.""" + write_dmi(self.dmi, VELOX) + nested = os.path.join(self.tmp.name, "etc", "wireplumber", "wireplumber.conf.d") + r = run(nested, self.dmi) + self.assertEqual(rc_of(r), 0, r.stdout) + self.assertTrue(os.path.exists(os.path.join(nested, DROPIN_NAME))) + + # ------------------------------------------------------------- error ---- + def test_missing_shipped_file_warns_and_does_not_crash(self): + """A partial checkout has no configs/ tree; surface it, keep going.""" + write_dmi(self.dmi, VELOX) + empty_root = os.path.join(self.tmp.name, "empty-checkout") + os.makedirs(empty_root) + r = run(self.confd, self.dmi, src_root=empty_root) + self.assertIn("WARN:", r.stdout) + self.assertNotEqual(rc_of(r), 0) + self.assertFalse(os.path.exists(self.dropin)) + + @unittest.skipUnless(os.geteuid() != 0, "root ignores directory write bits") + def test_unwritable_confd_warns_and_does_not_crash(self): + write_dmi(self.dmi, VELOX) + os.makedirs(self.confd) + os.chmod(self.confd, 0o500) + try: + r = run(self.confd, self.dmi) + self.assertIn("WARN:", r.stdout) + self.assertNotEqual(rc_of(r), 0) + finally: + os.chmod(self.confd, 0o700) + + +if __name__ == "__main__": + unittest.main() |
