Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/adloop/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,12 @@ def _generate_config_yaml(
if project_id:
lines.append(f' project_id: "{project_id}"')
if credentials_path:
lines.append(f' credentials_path: "{credentials_path}"')
# YAML treats `\` inside double-quoted strings as escape sequences (e.g.
# `\U` begins a Unicode escape, breaking Windows paths like `c:\Users\...`).
# Single-quoted strings are literal — backslashes are safe. Embedded
# single quotes (rare in paths) are escaped per YAML spec by doubling them.
escaped = credentials_path.replace("'", "''")
lines.append(f" credentials_path: '{escaped}'")
else:
lines.append(" # Using built-in credentials (no credentials_path needed)")
lines.append(' token_path: "~/.adloop/token.json"')
Expand Down
56 changes: 56 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Tests for the `adloop init` wizard helpers in `adloop.cli`."""

from __future__ import annotations

import yaml

from adloop.cli import _generate_config_yaml


class TestGenerateConfigYaml:
"""YAML generation in the init wizard must produce parseable output."""

def _generate(self, **overrides):
defaults = {
"project_id": "",
"credentials_path": "",
"property_id": "123456789",
"developer_token": "abc123",
"customer_id": "123-456-7890",
"login_customer_id": "987-654-3210",
"max_daily_budget": 50.0,
"require_dry_run": True,
}
defaults.update(overrides)
return _generate_config_yaml(**defaults)

def test_windows_credentials_path_parses(self):
"""Regression for Windows backslash paths breaking YAML parsing.

Previously the wizard wrote `credentials_path: "c:\\Users\\..."` which
YAML interpreted as a `\\U` Unicode escape sequence and raised
ScannerError. Single quotes treat backslashes literally.
"""
win_path = r"c:\Users\user\.adloop\credentials.json"
text = self._generate(credentials_path=win_path)
parsed = yaml.safe_load(text)
assert parsed["google"]["credentials_path"] == win_path

def test_posix_credentials_path_parses(self):
posix_path = "/home/user/.adloop/credentials.json"
text = self._generate(credentials_path=posix_path)
parsed = yaml.safe_load(text)
assert parsed["google"]["credentials_path"] == posix_path

def test_path_with_embedded_apostrophe_parses(self):
"""YAML single-quoted strings escape `'` by doubling — make sure we do."""
weird_path = r"c:\Users\o'brien\.adloop\credentials.json"
text = self._generate(credentials_path=weird_path)
parsed = yaml.safe_load(text)
assert parsed["google"]["credentials_path"] == weird_path

def test_no_credentials_path_uses_bundled_comment(self):
text = self._generate(credentials_path="")
assert "Using built-in credentials" in text
parsed = yaml.safe_load(text)
assert "credentials_path" not in parsed.get("google", {})