Skip to content

Commit 04feb2e

Browse files
committed
refactor: move install marker logic to a class
1 parent 555c848 commit 04feb2e

9 files changed

Lines changed: 114 additions & 91 deletions

dk-installer.py

Lines changed: 62 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -266,51 +266,60 @@ def command_hint(prod: str, subcmd: str, menu_label: str) -> str:
266266
return f"run `python3 {INSTALLER_NAME} {prod} {subcmd}`"
267267

268268

269-
def read_install_mode(data_folder: pathlib.Path, prod: str, compose_file_name: str) -> typing.Optional[str]:
270-
"""Return 'docker', 'pip', or None for the ``prod`` install in data_folder.
271-
272-
Reads the install marker file. Falls back to detecting a legacy Docker install
273-
(the product's compose file + credentials) from before the marker was introduced.
269+
class InstallMarker:
270+
"""Read/write the TestGen install marker file. Falls back to detecting
271+
a legacy Docker install (compose file + credentials) from before the
272+
marker was introduced.
274273
"""
275-
marker_path = data_folder / INSTALL_MARKER_FILE.format(prod)
276-
if marker_path.exists():
277-
try:
278-
data = json.loads(marker_path.read_text())
279-
except Exception:
280-
LOG.exception("Failed to read install marker at %s", marker_path)
281-
else:
282-
install_mode = data.get("install_mode")
283-
if install_mode in (INSTALL_MODE_DOCKER, INSTALL_MODE_PIP):
284-
return install_mode
285-
LOG.warning("Install marker has unexpected install_mode: %r", install_mode)
286274

287-
if (data_folder / compose_file_name).exists() and (data_folder / CREDENTIALS_FILE.format(prod)).exists():
288-
LOG.info("No marker present; detected legacy Docker install in %s", data_folder)
289-
return INSTALL_MODE_DOCKER
275+
def __init__(self, data_folder: pathlib.Path, prod: str, compose_file_name: typing.Optional[str] = None):
276+
self._data_folder = data_folder
277+
self._prod = prod
278+
self._compose_file_name = compose_file_name
279+
self.path = data_folder / INSTALL_MARKER_FILE.format(prod)
290280

291-
return None
281+
def read(self) -> typing.Optional[str]:
282+
if self.path.exists():
283+
try:
284+
data = json.loads(self.path.read_text())
285+
except Exception:
286+
LOG.exception("Failed to read install marker at %s", self.path)
287+
else:
288+
install_mode = data.get("install_mode")
289+
if install_mode in (INSTALL_MODE_DOCKER, INSTALL_MODE_PIP):
290+
return install_mode
291+
LOG.warning("Install marker has unexpected install_mode: %r", install_mode)
292+
if (
293+
self._compose_file_name
294+
and (self._data_folder / self._compose_file_name).exists()
295+
and (self._data_folder / CREDENTIALS_FILE.format(self._prod)).exists()
296+
):
297+
LOG.info("No marker present; detected legacy Docker install in %s", self._data_folder)
298+
return INSTALL_MODE_DOCKER
299+
return None
292300

301+
def write(self, mode: str, **extra) -> None:
302+
if mode not in (INSTALL_MODE_DOCKER, INSTALL_MODE_PIP):
303+
raise ValueError(f"Unknown install_mode: {mode}")
304+
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
305+
created_on = now
306+
if self.path.exists():
307+
try:
308+
existing = json.loads(self.path.read_text())
309+
if isinstance(existing.get("created_on"), str):
310+
created_on = existing["created_on"]
311+
except Exception:
312+
LOG.exception("Failed to read existing install marker at %s", self.path)
313+
self.path.write_text(
314+
json.dumps(
315+
{"install_mode": mode, "created_on": created_on, "last_updated_on": now, **extra},
316+
indent=2,
317+
)
318+
)
293319

294-
def write_install_marker(data_folder: pathlib.Path, prod: str, install_mode: str, **extra) -> None:
295-
if install_mode not in (INSTALL_MODE_DOCKER, INSTALL_MODE_PIP):
296-
raise ValueError(f"Unknown install_mode: {install_mode}")
297-
marker_path = data_folder / INSTALL_MARKER_FILE.format(prod)
298-
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
299-
created_on = now
300-
if marker_path.exists():
301-
try:
302-
existing = json.loads(marker_path.read_text())
303-
if isinstance(existing.get("created_on"), str):
304-
created_on = existing["created_on"]
305-
except Exception:
306-
LOG.exception("Failed to read existing install marker at %s", marker_path)
307-
data = {
308-
"install_mode": install_mode,
309-
"created_on": created_on,
310-
"last_updated_on": now,
311-
**extra,
312-
}
313-
marker_path.write_text(json.dumps(data, indent=2))
320+
def unlink(self) -> None:
321+
if self.path.exists():
322+
self.path.unlink()
314323

315324

316325
@contextlib.contextmanager
@@ -2737,7 +2746,7 @@ def get_requirements(self, args):
27372746
return []
27382747

27392748
def _resolve_install_mode(self, args):
2740-
existing = read_install_mode(self.data_folder, args.prod, args.compose_file_name)
2749+
existing = InstallMarker(self.data_folder, args.prod, args.compose_file_name).read()
27412750
if existing:
27422751
CONSOLE.msg(f"Found an existing TestGen {existing} installation in {self.data_folder}.")
27432752
CONSOLE.space()
@@ -2769,14 +2778,14 @@ def _auto_select_mode(self, args):
27692778
CONSOLE.msg("[d] Docker Compose (Recommended)")
27702779
CONSOLE.msg(" The most stable TestGen experience for persistent use.")
27712780
CONSOLE.msg(" Provides a fully managed environment with an isolated PostgreSQL container.")
2772-
prereq_status = " ".join(
2773-
f"{'(✓)' if ok else '(X)'} {req.label or req.key}" for req, ok in prereq_results
2774-
)
2781+
prereq_status = " ".join(f"{'(✓)' if ok else '(X)'} {req.label or req.key}" for req, ok in prereq_results)
27752782
CONSOLE.msg(f" Prerequisites: {prereq_status}")
27762783
CONSOLE.space()
27772784
CONSOLE.msg("[p] Pip + embedded PostgreSQL")
27782785
CONSOLE.msg(" A lightweight Python installation suited for evaluation.")
2779-
CONSOLE.msg(" Sets up an isolated Python environment and manages the PostgreSQL database on the file system.")
2786+
CONSOLE.msg(
2787+
" Sets up an isolated Python environment and manages the PostgreSQL database on the file system."
2788+
)
27802789
CONSOLE.space()
27812790

27822791
if docker_ready:
@@ -2805,7 +2814,7 @@ def _auto_select_mode(self, args):
28052814
def execute(self, args):
28062815
self.intro_text = self.pip_intro if self._resolved_mode == INSTALL_MODE_PIP else self.docker_intro
28072816
super().execute(args)
2808-
write_install_marker(self.data_folder, args.prod, self._resolved_mode)
2817+
InstallMarker(self.data_folder, args.prod).write(self._resolved_mode)
28092818
# Pip mode: keep the app running so the user has a one-command install
28102819
# experience. Docker mode already runs as detached containers via
28112820
# ``docker compose up --wait``, so no need to start anything here.
@@ -2889,7 +2898,7 @@ def get_requirements(self, args):
28892898
]
28902899

28912900
def _resolve_install_mode(self, args):
2892-
mode = read_install_mode(self.data_folder, args.prod, args.compose_file_name)
2901+
mode = InstallMarker(self.data_folder, args.prod, args.compose_file_name).read()
28932902
if mode is None:
28942903
CONSOLE.msg(f"No TestGen installation found in {self.data_folder}.")
28952904
CONSOLE.msg(f"To install TestGen, {command_hint(args.prod, 'install', 'Install TestGen')}.")
@@ -2902,7 +2911,7 @@ def _resolve_install_mode(self, args):
29022911

29032912
def execute(self, args):
29042913
super().execute(args)
2905-
write_install_marker(self.data_folder, args.prod, self._resolved_mode)
2914+
InstallMarker(self.data_folder, args.prod).write(self._resolved_mode)
29062915

29072916

29082917
class TestgenStartAction(Action, ComposeActionMixin):
@@ -2928,7 +2937,7 @@ def get_requirements(self, args):
29282937
return []
29292938

29302939
def _resolve_install_mode(self, args):
2931-
mode = read_install_mode(self.data_folder, args.prod, args.compose_file_name)
2940+
mode = InstallMarker(self.data_folder, args.prod, args.compose_file_name).read()
29322941
if mode is None:
29332942
CONSOLE.msg(f"No TestGen installation found in {self.data_folder}.")
29342943
CONSOLE.msg(f"To install TestGen, {command_hint(args.prod, 'install', 'Install TestGen')}.")
@@ -2992,7 +3001,7 @@ def get_requirements(self, args):
29923001
def _resolve_install_mode(self, args):
29933002
# Unlike install/upgrade, "no install found" is not an abort here —
29943003
# ``tg delete`` is idempotent. execute() handles the None case.
2995-
mode = read_install_mode(self.data_folder, args.prod, args.compose_file_name)
3004+
mode = InstallMarker(self.data_folder, args.prod, args.compose_file_name).read()
29963005
self._resolved_mode = mode
29973006
if mode is not None:
29983007
self.analytics.additional_properties["install_mode"] = mode
@@ -3008,7 +3017,7 @@ def execute(self, args):
30083017
self._delete_docker(args)
30093018
else:
30103019
self._delete_pip(args)
3011-
remove_path(self.data_folder / INSTALL_MARKER_FILE.format(args.prod))
3020+
InstallMarker(self.data_folder, args.prod, args.compose_file_name).unlink()
30123021

30133022
def _delete_docker(self, args):
30143023
if self.get_compose_file_path(args).exists():
@@ -3087,7 +3096,7 @@ def get_requirements(self, args):
30873096
return []
30883097

30893098
def _resolve_install_mode(self, args):
3090-
mode = read_install_mode(self.data_folder, args.prod, args.compose_file_name)
3099+
mode = InstallMarker(self.data_folder, args.prod, args.compose_file_name).read()
30913100
if mode is None:
30923101
CONSOLE.msg(f"No TestGen installation found in {self.data_folder}.")
30933102
CONSOLE.msg(f"To install TestGen, {command_hint(args.prod, 'install', 'Install TestGen')}.")
@@ -3159,7 +3168,7 @@ def get_requirements(self, args):
31593168

31603169
def _resolve_install_mode(self, args):
31613170
# Like delete: idempotent, so "no install" returns rather than aborts.
3162-
mode = read_install_mode(self.data_folder, args.prod, args.compose_file_name)
3171+
mode = InstallMarker(self.data_folder, args.prod, args.compose_file_name).read()
31633172
self._resolved_mode = mode
31643173
if mode is not None:
31653174
self.analytics.additional_properties["install_mode"] = mode

tests/test_install_type.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
INSTALL_MARKER_FILE,
99
INSTALL_MODE_DOCKER,
1010
INSTALL_MODE_PIP,
11+
InstallMarker,
1112
TESTGEN_COMPOSE_FILE,
12-
read_install_mode,
13-
write_install_marker,
1413
)
1514

1615

@@ -21,43 +20,43 @@ def data_folder(tmp_data_folder):
2120

2221
@pytest.mark.unit
2322
def test_read_install_mode_returns_none_when_empty(data_folder):
24-
assert read_install_mode(data_folder, "tg", "docker-compose.yml") is None
23+
assert InstallMarker(data_folder, "tg", "docker-compose.yml").read() is None
2524

2625

2726
@pytest.mark.unit
2827
@pytest.mark.parametrize("install_mode", [INSTALL_MODE_DOCKER, INSTALL_MODE_PIP])
2928
def test_read_install_mode_from_marker(data_folder, install_mode):
3029
(data_folder / INSTALL_MARKER_FILE.format("tg")).write_text(json.dumps({"install_mode": install_mode}))
3130

32-
assert read_install_mode(data_folder, "tg", "docker-compose.yml") == install_mode
31+
assert InstallMarker(data_folder, "tg", "docker-compose.yml").read() == install_mode
3332

3433

3534
@pytest.mark.unit
3635
def test_read_install_mode_legacy_docker_backfill(data_folder):
3736
(data_folder / TESTGEN_COMPOSE_FILE).write_text("version: '3'")
3837
(data_folder / CREDENTIALS_FILE.format("tg")).write_text("admin\n")
3938

40-
assert read_install_mode(data_folder, "tg", "docker-compose.yml") == INSTALL_MODE_DOCKER
39+
assert InstallMarker(data_folder, "tg", "docker-compose.yml").read() == INSTALL_MODE_DOCKER
4140

4241

4342
@pytest.mark.unit
4443
def test_read_install_mode_legacy_backfill_honors_compose_file_name(data_folder):
45-
# Verifies the function isn't TestGen-specific: pass a different product's
44+
# Verifies the marker isn't TestGen-specific: pass a different product's
4645
# compose file name and it should detect that product's legacy install.
4746
(data_folder / "obs-docker-compose.yml").write_text("version: '3'")
4847
(data_folder / CREDENTIALS_FILE.format("obs")).write_text("admin\n")
4948

50-
assert read_install_mode(data_folder, "obs", "obs-docker-compose.yml") == INSTALL_MODE_DOCKER
49+
assert InstallMarker(data_folder, "obs", "obs-docker-compose.yml").read() == INSTALL_MODE_DOCKER
5150
# And does NOT match if we point at the wrong compose file name.
52-
assert read_install_mode(data_folder, "obs", "docker-compose.yml") is None
51+
assert InstallMarker(data_folder, "obs", "docker-compose.yml").read() is None
5352

5453

5554
@pytest.mark.unit
5655
def test_read_install_mode_legacy_requires_both_files(data_folder):
5756
# Only compose file, missing credentials → not a legacy install
5857
(data_folder / TESTGEN_COMPOSE_FILE).write_text("version: '3'")
5958

60-
assert read_install_mode(data_folder, "tg", "docker-compose.yml") is None
59+
assert InstallMarker(data_folder, "tg", "docker-compose.yml").read() is None
6160

6261

6362
@pytest.mark.unit
@@ -66,35 +65,37 @@ def test_read_install_mode_malformed_marker_falls_back_to_legacy(data_folder):
6665
(data_folder / TESTGEN_COMPOSE_FILE).write_text("version: '3'")
6766
(data_folder / CREDENTIALS_FILE.format("tg")).write_text("admin\n")
6867

69-
assert read_install_mode(data_folder, "tg", "docker-compose.yml") == INSTALL_MODE_DOCKER
68+
assert InstallMarker(data_folder, "tg", "docker-compose.yml").read() == INSTALL_MODE_DOCKER
7069

7170

7271
@pytest.mark.unit
7372
def test_read_install_mode_unknown_value_falls_back(data_folder):
7473
(data_folder / INSTALL_MARKER_FILE.format("tg")).write_text(json.dumps({"install_mode": "bogus"}))
7574

76-
assert read_install_mode(data_folder, "tg", "docker-compose.yml") is None
75+
assert InstallMarker(data_folder, "tg", "docker-compose.yml").read() is None
7776

7877

7978
@pytest.mark.unit
8079
def test_write_install_marker_round_trip(data_folder):
81-
write_install_marker(data_folder, "tg", INSTALL_MODE_PIP, version="5.9.4", python_version="3.13.1")
80+
InstallMarker(data_folder, "tg", "docker-compose.yml").write(
81+
INSTALL_MODE_PIP, version="5.9.4", python_version="3.13.1"
82+
)
8283

8384
data = json.loads((data_folder / INSTALL_MARKER_FILE.format("tg")).read_text())
8485
assert data["install_mode"] == INSTALL_MODE_PIP
8586
assert data["version"] == "5.9.4"
8687
assert data["python_version"] == "3.13.1"
8788
assert "created_on" in data
8889
assert "last_updated_on" in data
89-
assert read_install_mode(data_folder, "tg", "docker-compose.yml") == INSTALL_MODE_PIP
90+
assert InstallMarker(data_folder, "tg", "docker-compose.yml").read() == INSTALL_MODE_PIP
9091

9192

9293
@pytest.mark.unit
9394
def test_write_install_marker_preserves_created_on_across_writes(data_folder):
94-
write_install_marker(data_folder, "tg", INSTALL_MODE_PIP, version="5.9.4")
95+
InstallMarker(data_folder, "tg").write(INSTALL_MODE_PIP, version="5.9.4")
9596
initial = json.loads((data_folder / INSTALL_MARKER_FILE.format("tg")).read_text())
9697

97-
write_install_marker(data_folder, "tg", INSTALL_MODE_PIP, version="5.10.0")
98+
InstallMarker(data_folder, "tg").write(INSTALL_MODE_PIP, version="5.10.0")
9899
after = json.loads((data_folder / INSTALL_MARKER_FILE.format("tg")).read_text())
99100

100101
assert after["created_on"] == initial["created_on"]
@@ -104,6 +105,19 @@ def test_write_install_marker_preserves_created_on_across_writes(data_folder):
104105
@pytest.mark.unit
105106
def test_write_install_marker_rejects_unknown_type(data_folder):
106107
with pytest.raises(ValueError, match="Unknown install_mode"):
107-
write_install_marker(data_folder, "tg", "sideways")
108+
InstallMarker(data_folder, "tg").write("sideways")
108109

109110

111+
@pytest.mark.unit
112+
def test_marker_unlink_removes_file(data_folder):
113+
InstallMarker(data_folder, "tg").write(INSTALL_MODE_PIP)
114+
assert (data_folder / INSTALL_MARKER_FILE.format("tg")).exists()
115+
116+
InstallMarker(data_folder, "tg").unlink()
117+
assert not (data_folder / INSTALL_MARKER_FILE.format("tg")).exists()
118+
119+
120+
@pytest.mark.unit
121+
def test_marker_unlink_is_idempotent(data_folder):
122+
# No marker present — unlink should be a no-op, not raise.
123+
InstallMarker(data_folder, "tg").unlink()

tests/test_tg_pip_delete.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
INSTALL_MODE_DOCKER,
1010
INSTALL_MODE_PIP,
1111
TestgenDeleteAction,
12-
write_install_marker,
12+
InstallMarker,
1313
)
1414

1515

@@ -19,7 +19,7 @@ def pip_delete_action(action_cls, args_mock, tmp_data_folder, start_cmd_mock, tm
1919
action = TestgenDeleteAction()
2020
args_mock.prod = "tg"
2121
args_mock.action = "delete"
22-
write_install_marker(action.data_folder, args_mock.prod, INSTALL_MODE_PIP)
22+
InstallMarker(action.data_folder, args_mock.prod).write(INSTALL_MODE_PIP)
2323
# Bypass check_requirements: pre-resolve mode so execute() runs the
2424
# delete branch directly.
2525
action._resolved_mode = INSTALL_MODE_PIP
@@ -158,7 +158,7 @@ def test_delete_nothing_to_delete(delete_action, args_mock, console_msg_mock):
158158

159159
@pytest.mark.integration
160160
def test_delete_routes_to_pip_and_removes_marker(delete_action, args_mock, tmp_data_folder, tmp_path):
161-
write_install_marker(Path(tmp_data_folder), "tg", INSTALL_MODE_PIP)
161+
InstallMarker(Path(tmp_data_folder), "tg").write(INSTALL_MODE_PIP)
162162
assert (Path(tmp_data_folder) / INSTALL_MARKER_FILE.format("tg")).exists()
163163

164164
delete_action._resolve_install_mode(args_mock)

0 commit comments

Comments
 (0)