Skip to content

Commit acd9647

Browse files
authored
Fixed issues when replacing placeholders and added tests (#2)
* Fixed issues when replacing placeholders and added tests * Fixed lint issues
1 parent d6478a6 commit acd9647

2 files changed

Lines changed: 194 additions & 58 deletions

File tree

template_project_utils/template_initializer.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@
1919

2020
class TemplateInitializer:
2121

22-
def __init__(self, config_file_path: Path, working_dir_path: Path | None = None, dry_run=False):
23-
self.config_file_path = config_file_path
24-
self.working_dir_path = working_dir_path if working_dir_path else config_file_path.parent
22+
def __init__(self, config_file_or_dir_path: Path, working_dir_path: Path | None = None, dry_run=False):
23+
self.config_file_path = config_file_or_dir_path if not config_file_or_dir_path.is_dir() else config_file_or_dir_path / "template_config.yaml"
24+
self.working_dir_path = working_dir_path if working_dir_path else self.config_file_path.parent
2525
self.dry_run = dry_run
2626

27+
if not self.config_file_path.exists():
28+
raise RuntimeError(f"Config file does not exist: {self.config_file_path}")
29+
30+
if not self.working_dir_path.exists():
31+
raise RuntimeError(f"Working dir does not exist: {self.working_dir_path}")
32+
2733
self.config = self._load_config(self.config_file_path)
2834
logm.debug("Config: %s", self.config)
2935

@@ -44,10 +50,10 @@ def _load_config(cls, config_path: Path) -> Dict[str, Any]:
4450
return yaml.safe_load(file)
4551

4652
@classmethod
47-
def replace_string_in_file(cls, filepath: Path, text_to_search: str, replacement_text: str):
53+
def replace_string_in_file(cls, filepath: Path, text_to_search: str, replacement_text: str) -> None:
4854
with fileinput.FileInput(filepath, inplace=True) as file:
4955
for line in file:
50-
line.replace(text_to_search, replacement_text)
56+
print(line.replace(text_to_search, replacement_text), end="")
5157

5258
def _update_files(self) -> None:
5359
logm.info("Updating files:")

tests/test_initializer.py

Lines changed: 183 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
# Copyright (C) 2024 twyleg
22
# fmt: off
3+
import os
34
import shutil
45
import sys
5-
from typing import List
6+
from collections import namedtuple
7+
from dataclasses import dataclass
8+
from typing import Dict, Any
9+
610

711
import pygit2
812
import pytest
13+
import yaml
914
from _pytest.monkeypatch import MonkeyPatch
1015

1116
from pathlib import Path
@@ -16,6 +21,13 @@
1621
FILE_DIR = Path(__file__).parent
1722

1823

24+
@dataclass
25+
class TestProject:
26+
path: Path
27+
config: Dict[str, Any]
28+
placeholder_target_pairs: Dict[str, str]
29+
30+
1931
def prepare_test_project_git_repo(test_project_path: Path) -> None:
2032
git_file_from_submodule = test_project_path / ".git"
2133
git_file_from_submodule.unlink()
@@ -26,140 +38,258 @@ def prepare_test_project_git_repo(test_project_path: Path) -> None:
2638
test_project_repo.remotes.create("origin", test_project_dummy_remote_url)
2739

2840

41+
def read_template_config(test_project_config_file_path: Path) -> Dict[str, Any]:
42+
with open(test_project_config_file_path, "r") as file:
43+
return yaml.safe_load(file)
44+
45+
2946
def create_test_project_from_template_and_chdir(template_project_src_dir_path: Path,
3047
template_project_dst_dir_path: Path,
31-
monkeypatch: MonkeyPatch) -> Path:
48+
template_project_config_filename: str,
49+
template_project_placeholder_target_pairs: Dict[str, str],
50+
monkeypatch: MonkeyPatch,
51+
) -> TestProject:
3252
template_project_dst_dir_path = template_project_dst_dir_path / template_project_src_dir_path.name
3353
shutil.copytree(template_project_src_dir_path, template_project_dst_dir_path)
3454
monkeypatch.chdir(template_project_dst_dir_path)
3555
prepare_test_project_git_repo(template_project_dst_dir_path)
36-
return template_project_dst_dir_path
56+
template_project_config = read_template_config(template_project_dst_dir_path / template_project_config_filename)
57+
58+
return TestProject(
59+
path=template_project_dst_dir_path,
60+
config=template_project_config,
61+
placeholder_target_pairs=template_project_placeholder_target_pairs
62+
)
3763

3864

3965
@pytest.fixture
4066
def template_project_cpp_master(tmp_path, monkeypatch):
4167
return create_test_project_from_template_and_chdir(
42-
FILE_DIR / "../external/template_project_cpp_master",
43-
tmp_path,
44-
monkeypatch
68+
template_project_src_dir_path=FILE_DIR / "../external/template_project_cpp_master",
69+
template_project_dst_dir_path=tmp_path,
70+
template_project_config_filename="template_config.yaml",
71+
template_project_placeholder_target_pairs={"template_project_cpp": "test_target_name"},
72+
monkeypatch=monkeypatch
4573
)
4674

4775

4876
@pytest.fixture
4977
def template_project_cpp_usecase_qt_qml_app(tmp_path, monkeypatch):
5078
return create_test_project_from_template_and_chdir(
51-
FILE_DIR / "../external/template_project_cpp_usecase_qt_qml_app",
52-
tmp_path,
53-
monkeypatch
79+
template_project_src_dir_path=FILE_DIR / "../external/template_project_cpp_usecase_qt_qml_app",
80+
template_project_dst_dir_path=tmp_path,
81+
template_project_config_filename="template_config.yaml",
82+
template_project_placeholder_target_pairs={"template_project_cpp": "test_target_name"},
83+
monkeypatch=monkeypatch
5484
)
5585

5686

5787
@pytest.fixture
5888
def template_project_kicad_master(tmp_path, monkeypatch):
5989
return create_test_project_from_template_and_chdir(
60-
FILE_DIR / "../external/template_project_kicad_master",
61-
tmp_path,
62-
monkeypatch
90+
template_project_src_dir_path=FILE_DIR / "../external/template_project_kicad_master",
91+
template_project_dst_dir_path=tmp_path,
92+
template_project_config_filename="template_config.yaml",
93+
template_project_placeholder_target_pairs={"template_project_kicad": "test_target_name"},
94+
monkeypatch=monkeypatch
6395
)
6496

6597

6698
@pytest.fixture
6799
def template_project_python_master(tmp_path, monkeypatch):
68100
return create_test_project_from_template_and_chdir(
69-
FILE_DIR / "../external/template_project_python_master",
70-
tmp_path,
71-
monkeypatch
101+
template_project_src_dir_path=FILE_DIR / "../external/template_project_python_master",
102+
template_project_dst_dir_path=tmp_path,
103+
template_project_config_filename="template_config.yaml",
104+
template_project_placeholder_target_pairs={
105+
"template_project_python": "test_target_name",
106+
"template-project-python": "test-target-name",
107+
},
108+
monkeypatch=monkeypatch
72109
)
73110

74111

112+
@pytest.fixture
113+
def template_project_python_master_with_alternative_config_filename(template_project_python_master):
114+
old_config_file_name = template_project_python_master.path / "template_config.yaml"
115+
new_config_file_name = template_project_python_master.path / "template_config_alt.yaml"
116+
os.rename(old_config_file_name, new_config_file_name)
117+
new_config_file_name.write_text(new_config_file_name.read_text().replace("- template_config.yaml", "- template_config_alt.yaml"))
118+
return template_project_python_master
119+
120+
75121
@pytest.fixture
76122
def template_project_python_usecase_qt_qml_app(tmp_path, monkeypatch):
77123
return create_test_project_from_template_and_chdir(
78-
FILE_DIR / "../external/template_project_python_usecase_qt_qml_app",
79-
tmp_path,
80-
monkeypatch
124+
template_project_src_dir_path=FILE_DIR / "../external/template_project_python_usecase_qt_qml_app",
125+
template_project_dst_dir_path=tmp_path,
126+
template_project_config_filename="template_config.yaml",
127+
template_project_placeholder_target_pairs={"template_project_python": "test_target_name"},
128+
monkeypatch=monkeypatch
81129
)
82130

83131

84-
def is_git_remote_origin_still_available(test_project_path: Path) -> bool:
85-
repo = pygit2.Repository(str(test_project_path))
132+
def is_git_remote_origin_still_existing(test_project: TestProject) -> bool:
133+
repo = pygit2.Repository(str(test_project.path))
86134
remote_collection = pygit2.remotes.RemoteCollection(repo)
87135
return "origin" in remote_collection.names()
88136

89137

90-
def is_any_placeholder_still_available(test_project_path: Path, keywords: List[str]):
138+
def is_any_placeholder_still_existing(test_project: TestProject):
91139
file_name_count = 0
92140
dir_name_count = 0
93141
file_content_count = 0
94142

95-
for path in test_project_path.rglob("*"):
96-
if path.is_relative_to(test_project_path / ".git/"):
143+
for path in test_project.path.rglob("*"):
144+
if path.is_relative_to(test_project.path / ".git/"):
97145
pass # Ignore
98-
elif path.is_relative_to(test_project_path / "venv/"):
146+
elif path.is_relative_to(test_project.path / "venv/"):
99147
pass # Ignore
100-
elif path.is_relative_to(test_project_path / "logs/"):
148+
elif path.is_relative_to(test_project.path / "logs/"):
101149
pass # Ignore
102150
elif path.is_dir():
103-
relative_path = str(path.relative_to(test_project_path))
104-
for keyword in keywords:
105-
if keyword in relative_path:
151+
relative_path = str(path.relative_to(test_project.path))
152+
for placeholder in test_project.placeholder_target_pairs.keys():
153+
if placeholder in relative_path:
106154
dir_name_count += 1
107-
print(f"Error: Keyword \"{keyword}\" found in directory name \"{relative_path}\"", file=sys.stderr)
155+
print(f"Error: Placeholder \"{placeholder}\" found in directory name \"{relative_path}\"", file=sys.stderr)
108156
elif path.is_file():
109-
relative_path = str(path.relative_to(test_project_path))
110-
for keyword in keywords:
111-
if keyword in relative_path:
157+
relative_path = str(path.relative_to(test_project.path))
158+
for placeholder in test_project.placeholder_target_pairs.keys():
159+
if placeholder in relative_path:
112160
file_name_count += 1
113-
print(f"Error: Keyword \"{keyword}\" found in file name \"{relative_path}\"", file=sys.stderr)
161+
print(f"Error: Placeholder \"{placeholder}\" found in file name \"{relative_path}\"", file=sys.stderr)
114162

115163
try:
116164
content = path.read_text()
117-
for keyword in keywords:
118-
count = content.count(keyword)
165+
for placeholder in test_project.placeholder_target_pairs.keys():
166+
count = content.count(placeholder)
119167
file_content_count += count
120168
if count:
121-
print(f"Error: Keyword \"{keyword}\" found {count} times in file \"{path}\"", file=sys.stderr)
169+
print(f"Error: Placeholder \"{placeholder}\" found {count} times in file \"{path}\"", file=sys.stderr)
122170
except UnicodeDecodeError as e:
123171
pass
124172
return file_name_count > 0 or dir_name_count > 0 or file_content_count > 0
125173

126174

127-
def is_project_correctly_initialized(test_project_path: Path, template_project_keywords: List[str]) -> bool:
128-
if is_any_placeholder_still_available(test_project_path, template_project_keywords):
129-
return False
130-
elif is_git_remote_origin_still_available(test_project_path):
131-
return False
132-
return True
175+
def is_any_file_diff_not_plausible(test_project: TestProject) -> bool:
176+
if test_project.config["update_files"]:
177+
for update_file_name in test_project.config["update_files"]:
178+
179+
for placeholder, target in test_project.placeholder_target_pairs.items():
180+
update_file_name = update_file_name.replace(placeholder, target)
181+
182+
update_file_path = test_project.path / update_file_name
183+
if update_file_path.stat().st_size == 0:
184+
return True
185+
return False
186+
187+
188+
def is_any_remove_file_candidate_still_existing(test_project: TestProject) -> bool:
189+
if test_project.config["remove_files"]:
190+
for remove_file_name in test_project.config["remove_files"]:
191+
remove_file_path = test_project.path / remove_file_name
192+
if remove_file_path.exists():
193+
return True
194+
return False
133195

134196

135-
class TestInitializer:
197+
def is_any_remove_dir_candidate_still_existing(test_project: TestProject) -> bool:
198+
if test_project.config["remove_dirs"]:
199+
for remove_dir_name in test_project.config["remove_dirs"]:
200+
remove_dir_path = test_project.path / remove_dir_name
201+
if remove_dir_path.exists():
202+
return True
203+
return False
204+
205+
206+
def is_any_rename_file_candidate_still_existing(test_project: TestProject) -> bool:
207+
if test_project.config["rename_files"]:
208+
for rename_file_name in test_project.config["rename_files"]:
209+
rename_file_path = test_project.path / rename_file_name
210+
if rename_file_path.exists():
211+
return True
212+
return False
213+
214+
215+
def is_any_rename_dir_candidate_still_existing(test_project: TestProject) -> bool:
216+
if test_project.config["rename_dirs"]:
217+
for rename_dir_name in test_project.config["rename_dirs"]:
218+
rename_dir_path = test_project.path / rename_dir_name
219+
if rename_dir_path.exists():
220+
return True
221+
return False
222+
223+
224+
def assert_project_correctly_initialized(test_project: TestProject) -> None:
225+
assert not is_any_placeholder_still_existing(test_project)
226+
assert not is_any_file_diff_not_plausible(test_project)
227+
assert not is_any_rename_file_candidate_still_existing(test_project)
228+
assert not is_any_rename_dir_candidate_still_existing(test_project)
229+
assert not is_any_remove_file_candidate_still_existing(test_project)
230+
assert not is_any_remove_dir_candidate_still_existing(test_project)
231+
assert not is_git_remote_origin_still_existing(test_project)
232+
233+
234+
class TestInitializerForAllTemplateTypes:
136235
def test_ValidTemplateProjectCppMaster_InitializeTemplate_InitializationSuccessful(self, template_project_cpp_master):
137-
template_initializer = TemplateInitializer(template_project_cpp_master / "template_config.yaml")
236+
template_initializer = TemplateInitializer(template_project_cpp_master.path)
138237
template_initializer.init({"template_project_cpp": "test_target_name"})
139-
assert is_project_correctly_initialized(template_project_cpp_master, ["template_project_cpp"])
238+
assert_project_correctly_initialized(template_project_cpp_master)
140239

141240
def test_ValidTemplateProjectCppUsecaseQtQmlApp_InitializeTemplate_InitializationSuccessful(self, template_project_cpp_usecase_qt_qml_app):
142-
template_initializer = TemplateInitializer(template_project_cpp_usecase_qt_qml_app / "template_config.yaml")
241+
template_initializer = TemplateInitializer(template_project_cpp_usecase_qt_qml_app.path)
143242
template_initializer.init({"template_project_cpp": "test_target_name"})
144-
assert is_project_correctly_initialized(template_project_cpp_usecase_qt_qml_app, ["template_project_cpp"])
243+
assert_project_correctly_initialized(template_project_cpp_usecase_qt_qml_app)
145244

146245
def test_ValidTemplateProjectKicadMaster_InitializeTemplate_InitializationSuccessful(self, template_project_kicad_master):
147-
template_initializer = TemplateInitializer(template_project_kicad_master / "template_config.yaml")
246+
template_initializer = TemplateInitializer(template_project_kicad_master.path)
148247
template_initializer.init({"template_project_kicad": "test_target_name"})
149-
assert is_project_correctly_initialized(template_project_kicad_master, ["template_project_kicad"])
248+
assert_project_correctly_initialized(template_project_kicad_master)
150249

151250
def test_ValidTemplateProjectPythonMaster_InitializeTemplate_InitializationSuccessful(self, template_project_python_master):
152-
template_initializer = TemplateInitializer(template_project_python_master / "template_config.yaml")
251+
template_initializer = TemplateInitializer(template_project_python_master.path)
153252
template_initializer.init({
154253
"template_project_python": "test_target_name",
155254
"template-project-python": "test-target-name",
156255
})
157-
assert is_project_correctly_initialized(template_project_python_master, ["template_project_python"])
256+
assert_project_correctly_initialized(template_project_python_master)
158257

159258
def test_ValidTemplateProjectPythonUsecaseQtQmlApp_InitializeTemplate_InitializationSuccessful(self, template_project_python_usecase_qt_qml_app):
160-
template_initializer = TemplateInitializer(template_project_python_usecase_qt_qml_app / "template_config.yaml")
259+
template_initializer = TemplateInitializer(template_project_python_usecase_qt_qml_app.path)
260+
template_initializer.init({
261+
"template_project_python": "test_target_name",
262+
"template-project-python": "test-target-name",
263+
})
264+
assert_project_correctly_initialized(template_project_python_usecase_qt_qml_app)
265+
266+
267+
class TestInitializerDetails:
268+
269+
def test_ValidTemplateProjectPythonMasterWithCustomTemplateConfigFilename_InitializeTemplate_InitializationSuccessful(
270+
self,
271+
template_project_python_master_with_alternative_config_filename
272+
):
273+
template_initializer = TemplateInitializer(template_project_python_master_with_alternative_config_filename.path / "template_config_alt.yaml")
161274
template_initializer.init({
162275
"template_project_python": "test_target_name",
163276
"template-project-python": "test-target-name",
164277
})
165-
assert is_project_correctly_initialized(template_project_python_usecase_qt_qml_app, ["template_project_python"])
278+
assert_project_correctly_initialized(template_project_python_master_with_alternative_config_filename)
279+
280+
def test_ValidTemplateProjectPythonMasterWithInvalidConfigFileNameParameter_InitializeTemplate_RuntimeErrorRaised(
281+
self,
282+
template_project_python_master
283+
):
284+
with pytest.raises(RuntimeError):
285+
TemplateInitializer(template_project_python_master.path / "template_config_not_existing.yaml")
286+
287+
def test_ValidTemplateProjectPythonMasterWithInvalidWorkingDirParameter_InitializeTemplate_RuntimeErrorRaised(
288+
self,
289+
template_project_python_master
290+
):
291+
with pytest.raises(RuntimeError):
292+
TemplateInitializer(
293+
config_file_or_dir_path=template_project_python_master.path / "template_config.yaml",
294+
working_dir_path=Path("/not/existing/working/dir")
295+
)

0 commit comments

Comments
 (0)