mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 04:36:40 +00:00
Two independent bugs were compounding into a corrupted NIX_CONFIG that broke every local image rebuild: 1. scripts/tests/code/run.sh non-idempotently appended "accept-flake-config = true" to whatever NIX_CONFIG it inherited. On the next source of .env (which carried the previous value back), the line accumulated -- "true\naccept-flake-config = true\n..." -- until Nix rejected the Boolean. Fix: pipe through `awk '\!seen[$0]++'` to dedup lines while preserving order. 2. utils/env/writer.py silently re-encoded literal newlines as `\n` escape sequences inside double-quoted values. docker-compose's --env-file loader does not decode them and neither does bash `source`, so any multi-line value round-tripped into garbage on the next read. Fix: raise ValueError instead -- producers must collapse to a single line before handing off to the env-builder. Test fixtures for the writer adjusted to assert the new raise semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
166 lines
6.1 KiB
Python
166 lines
6.1 KiB
Python
"""Unit tests for :mod:`utils.env.writer`.
|
|
|
|
Covers value quoting (bare-safe vs quoted-with-escapes) and the
|
|
end-to-end ``write_dotenv`` shape (header, sorted keys, comment-above
|
|
pattern, multi-line escape).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
|
|
from utils.cache.files import read_text
|
|
from utils.env.builder import EnvBuilder
|
|
from utils.env.writer import quote_for_dotenv, write_dotenv
|
|
|
|
|
|
class TestQuoteForDotenv(unittest.TestCase):
|
|
def test_safe_value_emitted_bare(self) -> None:
|
|
self.assertEqual(quote_for_dotenv("plain"), "plain")
|
|
self.assertEqual(quote_for_dotenv("a/b.c"), "a/b.c")
|
|
self.assertEqual(quote_for_dotenv("v1.2.3"), "v1.2.3")
|
|
self.assertEqual(quote_for_dotenv("user@host"), "user@host")
|
|
self.assertEqual(quote_for_dotenv("a-b_c+d=e"), "a-b_c+d=e")
|
|
self.assertEqual(quote_for_dotenv("a:b%c"), "a:b%c")
|
|
|
|
def test_empty_string_is_quoted(self) -> None:
|
|
self.assertEqual(quote_for_dotenv(""), '""')
|
|
|
|
def test_value_with_space_is_quoted(self) -> None:
|
|
self.assertEqual(quote_for_dotenv("hello world"), '"hello world"')
|
|
|
|
def test_double_quote_is_escaped(self) -> None:
|
|
self.assertEqual(quote_for_dotenv('a"b'), '"a\\"b"')
|
|
|
|
def test_backslash_is_escaped_first(self) -> None:
|
|
# Backslash escaping must precede other escapes to avoid double-coding.
|
|
self.assertEqual(quote_for_dotenv("a\\b"), '"a\\\\b"')
|
|
|
|
def test_backslash_then_quote(self) -> None:
|
|
self.assertEqual(quote_for_dotenv('a\\"b'), '"a\\\\\\"b"')
|
|
|
|
def test_newline_raises(self) -> None:
|
|
with self.assertRaises(ValueError):
|
|
quote_for_dotenv("line1\nline2")
|
|
|
|
def test_carriage_return_raises(self) -> None:
|
|
with self.assertRaises(ValueError):
|
|
quote_for_dotenv("a\rb")
|
|
|
|
def test_unicode_passes_through_quoted(self) -> None:
|
|
self.assertEqual(quote_for_dotenv("naïve"), '"naïve"')
|
|
|
|
|
|
class TestWriteDotenv(unittest.TestCase):
|
|
def _build(
|
|
self, values: dict[str, str], comments: dict[str, str] | None = None
|
|
) -> EnvBuilder:
|
|
eb = EnvBuilder()
|
|
eb.values = values
|
|
eb.comments = comments or {}
|
|
return eb
|
|
|
|
def test_writes_header(self) -> None:
|
|
with TemporaryDirectory() as td:
|
|
dest = Path(td) / ".env"
|
|
write_dotenv(self._build({}), dest)
|
|
text = read_text(str(dest))
|
|
self.assertIn("Generated by `make dotenv`", text)
|
|
|
|
def test_sorts_keys_alphabetically(self) -> None:
|
|
with TemporaryDirectory() as td:
|
|
dest = Path(td) / ".env"
|
|
write_dotenv(self._build({"Z": "1", "A": "1", "M": "1"}), dest)
|
|
text = read_text(str(dest))
|
|
self.assertLess(text.index("A=1"), text.index("M=1"))
|
|
self.assertLess(text.index("M=1"), text.index("Z=1"))
|
|
|
|
def test_comment_emitted_above_key(self) -> None:
|
|
with TemporaryDirectory() as td:
|
|
dest = Path(td) / ".env"
|
|
write_dotenv(
|
|
self._build({"K": "v"}, {"K": "my doc"}),
|
|
dest,
|
|
)
|
|
text = read_text(str(dest))
|
|
i = text.index("K=")
|
|
self.assertIn("# my doc", text[:i])
|
|
|
|
def test_no_comment_emitted_when_blank(self) -> None:
|
|
with TemporaryDirectory() as td:
|
|
dest = Path(td) / ".env"
|
|
write_dotenv(self._build({"K": "v"}, {"K": " "}), dest)
|
|
text = read_text(str(dest))
|
|
self.assertNotIn("# ", text.split("K=")[0].split("\n")[-2])
|
|
|
|
def test_blank_line_between_entries(self) -> None:
|
|
with TemporaryDirectory() as td:
|
|
dest = Path(td) / ".env"
|
|
write_dotenv(self._build({"A": "1", "B": "2"}), dest)
|
|
text = read_text(str(dest))
|
|
# Each key block is preceded by a blank line.
|
|
self.assertRegex(text, r"\nA=1\n")
|
|
self.assertRegex(text, r"\nB=2\n")
|
|
|
|
def test_safe_value_emitted_bare(self) -> None:
|
|
with TemporaryDirectory() as td:
|
|
dest = Path(td) / ".env"
|
|
write_dotenv(self._build({"K": "plain"}), dest)
|
|
text = read_text(str(dest))
|
|
self.assertIn("K=plain", text)
|
|
self.assertNotIn('K="plain"', text)
|
|
|
|
def test_unsafe_value_quoted(self) -> None:
|
|
with TemporaryDirectory() as td:
|
|
dest = Path(td) / ".env"
|
|
write_dotenv(self._build({"K": "with space"}), dest)
|
|
text = read_text(str(dest))
|
|
self.assertIn('K="with space"', text)
|
|
|
|
def test_multiline_value_raises(self) -> None:
|
|
with TemporaryDirectory() as td:
|
|
dest = Path(td) / ".env"
|
|
with self.assertRaises(ValueError):
|
|
write_dotenv(self._build({"K": "a\nb"}), dest)
|
|
|
|
def test_trailing_newline(self) -> None:
|
|
with TemporaryDirectory() as td:
|
|
dest = Path(td) / ".env"
|
|
write_dotenv(self._build({"K": "v"}), dest)
|
|
text = read_text(str(dest))
|
|
self.assertTrue(text.endswith("\n"))
|
|
|
|
def test_empty_builder_writes_header_only(self) -> None:
|
|
with TemporaryDirectory() as td:
|
|
dest = Path(td) / ".env"
|
|
write_dotenv(self._build({}), dest)
|
|
text = read_text(str(dest))
|
|
# Only the 3 header comment lines + trailing newline.
|
|
self.assertEqual(text.count("\n"), 3)
|
|
for line in text.splitlines():
|
|
self.assertTrue(line.startswith("#"))
|
|
|
|
def test_keys_with_and_without_comment_mixed(self) -> None:
|
|
with TemporaryDirectory() as td:
|
|
dest = Path(td) / ".env"
|
|
write_dotenv(
|
|
self._build(
|
|
{"A": "1", "B": "2"},
|
|
{"A": "doc-a"},
|
|
),
|
|
dest,
|
|
)
|
|
text = read_text(str(dest))
|
|
self.assertIn("# doc-a\nA=1", text)
|
|
# B has no comment block above it.
|
|
b_block = text[text.index("B=") - 60 : text.index("B=")]
|
|
self.assertNotIn(
|
|
"# ", b_block.rsplit("\n", 2)[-2] if "\n" in b_block else ""
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|