ScriptCollection 3.5.16__py3-none-any.whl → 4.0.78__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. ScriptCollection/AnionBuildPlatform.py +206 -0
  2. ScriptCollection/{UpdateCertificates.py → CertificateUpdater.py} +69 -46
  3. ScriptCollection/Executables.py +515 -18
  4. ScriptCollection/GeneralUtilities.py +1272 -873
  5. ScriptCollection/ImageUpdater.py +648 -0
  6. ScriptCollection/ProgramRunnerBase.py +10 -10
  7. ScriptCollection/ProgramRunnerMock.py +2 -0
  8. ScriptCollection/ProgramRunnerPopen.py +7 -1
  9. ScriptCollection/ProgramRunnerSudo.py +108 -0
  10. ScriptCollection/SCLog.py +115 -0
  11. ScriptCollection/ScriptCollectionCore.py +942 -266
  12. ScriptCollection/TFCPS/Docker/TFCPS_CodeUnitSpecific_Docker.py +95 -0
  13. ScriptCollection/TFCPS/Docker/__init__.py +0 -0
  14. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationBase.py +8 -0
  15. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationGenerate.py +6 -0
  16. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationNoGenerate.py +7 -0
  17. ScriptCollection/TFCPS/DotNet/TFCPS_CodeUnitSpecific_DotNet.py +485 -0
  18. ScriptCollection/TFCPS/DotNet/__init__.py +0 -0
  19. ScriptCollection/TFCPS/Flutter/TFCPS_CodeUnitSpecific_Flutter.py +130 -0
  20. ScriptCollection/TFCPS/Flutter/__init__.py +0 -0
  21. ScriptCollection/TFCPS/Go/TFCPS_CodeUnitSpecific_Go.py +74 -0
  22. ScriptCollection/TFCPS/Go/__init__.py +0 -0
  23. ScriptCollection/TFCPS/NodeJS/TFCPS_CodeUnitSpecific_NodeJS.py +131 -0
  24. ScriptCollection/TFCPS/NodeJS/__init__.py +0 -0
  25. ScriptCollection/TFCPS/Python/TFCPS_CodeUnitSpecific_Python.py +227 -0
  26. ScriptCollection/TFCPS/Python/__init__.py +0 -0
  27. ScriptCollection/TFCPS/TFCPS_CodeUnitSpecific_Base.py +418 -0
  28. ScriptCollection/TFCPS/TFCPS_CodeUnit_BuildCodeUnit.py +128 -0
  29. ScriptCollection/TFCPS/TFCPS_CodeUnit_BuildCodeUnits.py +136 -0
  30. ScriptCollection/TFCPS/TFCPS_CreateRelease.py +95 -0
  31. ScriptCollection/TFCPS/TFCPS_Generic.py +43 -0
  32. ScriptCollection/TFCPS/TFCPS_MergeToMain.py +122 -0
  33. ScriptCollection/TFCPS/TFCPS_MergeToStable.py +350 -0
  34. ScriptCollection/TFCPS/TFCPS_PreBuildCodeunitsScript.py +47 -0
  35. ScriptCollection/TFCPS/TFCPS_Tools_General.py +1356 -0
  36. ScriptCollection/TFCPS/__init__.py +0 -0
  37. {ScriptCollection-3.5.16.dist-info → scriptcollection-4.0.78.dist-info}/METADATA +23 -22
  38. scriptcollection-4.0.78.dist-info/RECORD +43 -0
  39. {ScriptCollection-3.5.16.dist-info → scriptcollection-4.0.78.dist-info}/WHEEL +1 -1
  40. {ScriptCollection-3.5.16.dist-info → scriptcollection-4.0.78.dist-info}/entry_points.txt +32 -0
  41. ScriptCollection/ProgramRunnerEpew.py +0 -122
  42. ScriptCollection/RPStream.py +0 -42
  43. ScriptCollection/TasksForCommonProjectStructure.py +0 -2625
  44. ScriptCollection-3.5.16.dist-info/RECORD +0 -16
  45. {ScriptCollection-3.5.16.dist-info → scriptcollection-4.0.78.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,418 @@
1
+ import os
2
+ from pathlib import Path
3
+ import shutil
4
+ import re
5
+ import json
6
+ import argparse
7
+ from abc import ABC, abstractmethod
8
+ import xmlschema
9
+ from lxml import etree
10
+ from ..GeneralUtilities import GeneralUtilities, VersionEcholon
11
+ from ..ScriptCollectionCore import ScriptCollectionCore
12
+ from ..SCLog import LogLevel
13
+ from .TFCPS_Tools_General import TFCPS_Tools_General
14
+
15
+
16
+ class TFCPS_CodeUnitSpecific_Base(ABC):
17
+
18
+ __current_file:str=None
19
+ __target_environment_type:str
20
+ __repository_folder:str=None
21
+ __codeunit_folder:str=None
22
+ __current_folder:str=None
23
+ __verbosity:LogLevel=None
24
+ __use_cache:bool=None
25
+ tfcps_Tools_General:TFCPS_Tools_General
26
+ _protected_sc:ScriptCollectionCore
27
+ __is_pre_merge:bool=False#TODO must be setable to true
28
+ __validate_developers_of_repository:bool=True#TODO must be setable to false
29
+
30
+ def __init__(self,current_file:str,verbosity:LogLevel,target_envionment_type:str,use_cache:bool,is_pre_merge:bool):
31
+ self.__verbosity=verbosity
32
+ self.__use_cache=use_cache
33
+ self.__target_environment_type=target_envionment_type
34
+ self.__current_file = str(Path(current_file).absolute())
35
+ self.__current_folder = os.path.dirname(self.__current_file)
36
+ self.__codeunit_folder=self.__search_codeunit_folder()
37
+ self.__is_pre_merge=is_pre_merge
38
+ self._protected_sc=ScriptCollectionCore()#TODO set loglevel
39
+ self.tfcps_Tools_General=TFCPS_Tools_General(self._protected_sc)
40
+ self.tfcps_Tools_General.assert_is_codeunit_folder(self.__codeunit_folder)
41
+ self.__repository_folder=GeneralUtilities.resolve_relative_path("..",self.__codeunit_folder)
42
+ self._protected_sc.assert_is_git_repository(self.__repository_folder)
43
+
44
+ def __search_codeunit_folder(self)->str:
45
+ current_path:str=os.path.dirname(self.__current_file)
46
+ enabled:bool=True
47
+ while enabled:
48
+ try:
49
+ current_path=GeneralUtilities.resolve_relative_path("..",current_path)
50
+ foldername=os.path.basename(current_path)
51
+ codeunit_file:str=os.path.join(current_path,f"{foldername}.codeunit.xml")
52
+ if os.path.isfile(codeunit_file):
53
+ return current_path
54
+ except:
55
+ enabled=False
56
+ raise ValueError(f"Can not find codeunit-folder for folder \"{self.__current_file}\".")
57
+
58
+ @abstractmethod
59
+ def get_dependencies(self)->dict[str,set[str]]:
60
+ raise ValueError(f"Operation is abstract.")
61
+
62
+ @abstractmethod
63
+ def get_available_versions(self,dependencyname:str)->list[str]:
64
+ raise ValueError(f"Operation is abstract.")
65
+
66
+ @abstractmethod
67
+ def set_dependency_version(self,name:str,new_version:str)->None:
68
+ raise ValueError(f"Operation is abstract.")
69
+
70
+ def update_dependencies(self):
71
+ self.update_dependencies_with_specific_echolon(VersionEcholon.LatestPatchOrLatestMinor)
72
+
73
+ def update_dependencies_with_specific_echolon(self, echolon: VersionEcholon):
74
+ ignored_dependencies=self.tfcps_Tools_General.get_dependencies_which_are_ignored_from_updates(self.get_codeunit_folder())
75
+ for ignored_dependency in ignored_dependencies:
76
+ self._protected_sc.log.log(f"Codeunit {self.get_codeunit_name()} ignores the dependency {ignored_dependency} in update-checks.", LogLevel.Warning)
77
+
78
+ dependencies_dict:dict[str,set[str]]=self.get_dependencies()
79
+ for dependencyname,dependency_versions in dependencies_dict.items():
80
+ latest_currently_used_version=GeneralUtilities.get_latest_version(dependency_versions)
81
+ if dependencyname not in ignored_dependencies:
82
+ try:
83
+ available_versions:list[str]=self.get_available_versions(dependencyname)
84
+ for available_version in available_versions:
85
+ GeneralUtilities.assert_condition(re.match(r"^(\d+).(\d+).(\d+)$", available_version) is not None,f"Invalid-version-string: {available_version}")
86
+ desired_version=GeneralUtilities.choose_version(available_versions,latest_currently_used_version,echolon)
87
+ self.set_dependency_version(dependencyname,desired_version)
88
+ except Exception:
89
+ GeneralUtilities.write_exception_to_stderr(f"Error while updating {dependencyname}.")
90
+ raise
91
+
92
+ def get_version_of_project(self)->str:
93
+ return self.tfcps_Tools_General.get_version_of_project(self.get_repository_folder())
94
+
95
+ @GeneralUtilities.check_arguments
96
+ def do_common_tasks_base(self,current_codeunit_version:str):
97
+ repository_folder: str =self.get_repository_folder()
98
+ self._protected_sc.assert_is_git_repository(repository_folder)
99
+ codeunit_name: str = self.get_codeunit_name()
100
+ project_version = self.tfcps_Tools_General.get_version_of_project(repository_folder)
101
+ if current_codeunit_version is None:
102
+ current_codeunit_version=project_version
103
+ codeunit_folder = os.path.join(repository_folder, codeunit_name)
104
+
105
+ # check codeunit-conformity
106
+ # TODO check if foldername=="<codeunitname>[.codeunit.xml]" == <codeunitname> in file
107
+ supported_codeunitspecificationversion = "2.9.4" # should always be the latest version of the ProjectTemplates-repository
108
+ codeunit_file = os.path.join(codeunit_folder, f"{codeunit_name}.codeunit.xml")
109
+ if not os.path.isfile(codeunit_file):
110
+ raise ValueError(f'Codeunitfile "{codeunit_file}" does not exist.')
111
+ # TODO implement usage of self.reference_latest_version_of_xsd_when_generating_xml
112
+ namespaces = {'cps': 'https://projects.aniondev.de/PublicProjects/Common/ProjectTemplates/-/tree/main/Conventions/RepositoryStructure/CommonProjectStructure', 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}
113
+ root: etree._ElementTree = etree.parse(codeunit_file)
114
+
115
+ # check codeunit-spcecification-version
116
+ try:
117
+ codeunit_file_version = root.xpath('//cps:codeunit/@codeunitspecificationversion', namespaces=namespaces)[0]
118
+ if codeunit_file_version != supported_codeunitspecificationversion:
119
+ raise ValueError(f"ScriptCollection only supports processing codeunits with codeunit-specification-version={supported_codeunitspecificationversion}.")
120
+ schemaLocation = root.xpath('//cps:codeunit/@xsi:schemaLocation', namespaces=namespaces)[0]
121
+ xmlschema.validate(codeunit_file, schemaLocation)
122
+ # TODO check if the properties codeunithastestablesourcecode, codeunithasupdatabledependencies, throwexceptionifcodeunitfilecannotbevalidated, developmentState and description exist and the values are valid
123
+ except Exception as exception:
124
+ self._protected_sc.log.log_exception(f'Codeunitfile "{codeunit_file}" can not be validated due to the following exception:', exception,LogLevel.Warning)
125
+
126
+ # check codeunit-name
127
+ codeunit_name_in_codeunit_file = root.xpath('//cps:codeunit/cps:name/text()', namespaces=namespaces)[0]
128
+ if codeunit_name != codeunit_name_in_codeunit_file:
129
+ raise ValueError(f"The folder-name ('{codeunit_name}') is not equal to the codeunit-name ('{codeunit_name_in_codeunit_file}').")
130
+
131
+ # check owner-name
132
+ codeunit_ownername_in_codeunit_file = self.tfcps_Tools_General. get_codeunit_owner_name(self.get_codeunit_file())
133
+ GeneralUtilities.assert_condition(GeneralUtilities.string_has_content(codeunit_ownername_in_codeunit_file), "No valid name for codeunitowner given.")
134
+
135
+ # check owner-emailaddress
136
+ codeunit_owneremailaddress_in_codeunit_file = self.tfcps_Tools_General.get_codeunit_owner_emailaddress(self.get_codeunit_file())
137
+ GeneralUtilities.assert_condition(GeneralUtilities.string_has_content(codeunit_owneremailaddress_in_codeunit_file), "No valid email-address for codeunitowner given.")
138
+
139
+ # check development-state
140
+ developmentstate = root.xpath('//cps:properties/@developmentstate', namespaces=namespaces)[0]
141
+ developmentstate_active = "Active development"
142
+ developmentstate_maintenance = "Maintenance-updates only"
143
+ developmentstate_inactive = "Inactive"
144
+ GeneralUtilities.assert_condition(developmentstate in (developmentstate_active, developmentstate_maintenance, developmentstate_inactive), f"Invalid development-state. Must be '{developmentstate_active}' or '{developmentstate_maintenance}' or '{developmentstate_inactive}' but was '{developmentstate}'.")
145
+
146
+ # check for mandatory files
147
+ files = ["Other/Build/Build.py", "Other/QualityCheck/Linting.py", "Other/Reference/GenerateReference.py"]
148
+ if self.tfcps_Tools_General.codeunit_has_testable_sourcecode(self.get_codeunit_file()):
149
+ # TODO check if the testsettings-section appears in the codeunit-file
150
+ files.append("Other/QualityCheck/RunTestcases.py")
151
+ if self.tfcps_Tools_General.codeunit_has_updatable_dependencies(self.get_codeunit_file()):
152
+ # TODO check if the updatesettings-section appears in the codeunit-file
153
+ files.append("Other/UpdateDependencies.py")
154
+ for file in files:
155
+ combined_file = os.path.join(codeunit_folder, file)
156
+ if not os.path.isfile(combined_file):
157
+ raise ValueError(f'The mandatory file "{file}" does not exist in the codeunit-folder.')
158
+
159
+ if os.path.isfile(os.path.join(codeunit_folder, "Other", "requirements.txt")):
160
+ self.install_requirementstxt_for_codeunit()
161
+
162
+ # check developer
163
+ if self.__validate_developers_of_repository:
164
+ expected_authors: list[tuple[str, str]] = []
165
+ expected_authors_in_xml = root.xpath('//cps:codeunit/cps:developerteam/cps:developer', namespaces=namespaces)
166
+ for expected_author in expected_authors_in_xml:
167
+ author_name = expected_author.xpath('./cps:developername/text()', namespaces=namespaces)[0]
168
+ author_emailaddress = expected_author.xpath('./cps:developeremailaddress/text()', namespaces=namespaces)[0]
169
+ expected_authors.append((author_name, author_emailaddress))
170
+ actual_authors: list[tuple[str, str]] = self.tfcps_Tools_General.get_all_authors_and_committers_of_repository(repository_folder, codeunit_name)
171
+ # TODO refactor this check to only check commits which are behind this but which are not already on main
172
+ # TODO verify also if the commit is signed by a valid key of the author
173
+ for actual_author in actual_authors:
174
+ if not (actual_author) in expected_authors:
175
+ actual_author_formatted = f"{actual_author[0]} <{actual_author[1]}>"
176
+ raise ValueError(f'Author/Comitter "{actual_author_formatted}" is not in the codeunit-developer-team. If {actual_author} is a authorized developer for this codeunit you should consider defining this in the codeunit-file or adapting the name using a .mailmap-file (see https://git-scm.com/docs/gitmailmap). The developer-team-check can also be disabled using the property validate_developers_of_repository.')
177
+
178
+ dependent_codeunits = self.tfcps_Tools_General.get_dependent_code_units(codeunit_file)
179
+ for dependent_codeunit in dependent_codeunits:
180
+ if not self.tfcps_Tools_General.dependent_codeunit_exists(repository_folder, dependent_codeunit):
181
+ raise ValueError(f"Codeunit {codeunit_name} does have dependent codeunit {dependent_codeunit} which does not exist.")
182
+
183
+ # TODO implement cycle-check for dependent codeunits
184
+
185
+ artifacts_folder = os.path.join(codeunit_folder, "Other", "Artifacts")
186
+ GeneralUtilities.ensure_directory_does_not_exist(artifacts_folder)
187
+
188
+ # get artifacts from dependent codeunits
189
+ self.tfcps_Tools_General.copy_artifacts_from_dependent_code_units(repository_folder, codeunit_name)
190
+
191
+ # update codeunit-version
192
+ self.tfcps_Tools_General.write_version_to_codeunit_file(self.get_codeunit_file(), current_codeunit_version)
193
+
194
+ # set project version
195
+ package_json_file = os.path.join(repository_folder, "package.json") # TDOO move this to a general project-specific (and codeunit-independent-script)
196
+ if os.path.isfile(package_json_file):
197
+ package_json_data: str = None
198
+ with open(package_json_file, "r", encoding="utf-8") as f1:
199
+ package_json_data = json.load(f1)
200
+ package_json_data["version"] = project_version
201
+ with open(package_json_file, "w", encoding="utf-8") as f2:
202
+ json.dump(package_json_data, f2, indent=2)
203
+ GeneralUtilities.write_text_to_file(package_json_file, GeneralUtilities.read_text_from_file(package_json_file).replace("\r", ""))
204
+
205
+ # set default constants
206
+ self.tfcps_Tools_General.set_default_constants(os.path.join(codeunit_folder))
207
+
208
+ # Hints-file
209
+ hints_file = os.path.join(codeunit_folder, "Other", "Reference", "ReferenceContent", "Hints.md")
210
+ if not os.path.isfile(hints_file):
211
+ raise ValueError(f"Hints-file '{hints_file}' does not exist.")
212
+
213
+ # Copy license-file
214
+ self.tfcps_Tools_General.copy_licence_file(self.get_codeunit_folder())
215
+
216
+ # Generate diff-report
217
+ self.tfcps_Tools_General.generate_diff_report(repository_folder, codeunit_name, self.tfcps_Tools_General.get_version_of_codeunit(self.get_codeunit_file()))
218
+
219
+ @GeneralUtilities.check_arguments
220
+ def generate_reference_using_docfx(self=None):
221
+ reference_folder =os.path.join( self.get_codeunit_folder(),"Other","Reference")
222
+ generated_reference_folder = GeneralUtilities.resolve_relative_path("../Artifacts/Reference", reference_folder)
223
+ GeneralUtilities.ensure_directory_does_not_exist(generated_reference_folder)
224
+ GeneralUtilities.ensure_directory_exists(generated_reference_folder)
225
+ obj_folder = os.path.join(reference_folder, "obj")
226
+ GeneralUtilities.ensure_folder_exists_and_is_empty(obj_folder)
227
+ self._protected_sc.run_program("docfx", "-t default,templates/darkfx docfx.json", reference_folder)
228
+ GeneralUtilities.ensure_directory_does_not_exist(obj_folder)
229
+
230
+ @GeneralUtilities.check_arguments
231
+ def use_cache(self)->bool:
232
+ return self.__use_cache
233
+
234
+ @GeneralUtilities.check_arguments
235
+ def get_codeunit_folder(self)->str:
236
+ return self.__codeunit_folder
237
+
238
+ @GeneralUtilities.check_arguments
239
+ def get_codeunit_name(self)->str:
240
+ return os.path.basename(self.__codeunit_folder)
241
+
242
+ @GeneralUtilities.check_arguments
243
+ def get_repository_folder(self)->str:
244
+ return self.__repository_folder
245
+
246
+ @GeneralUtilities.check_arguments
247
+ def get_current_folder(self)->str:
248
+ return self.__current_folder
249
+
250
+ @GeneralUtilities.check_arguments
251
+ def get_verbosity(self)->LogLevel:
252
+ return self.__verbosity
253
+
254
+ @GeneralUtilities.check_arguments
255
+ def get_artifacts_folder(self) -> str:
256
+ return os.path.join(self.get_codeunit_folder(), "Other", "Artifacts")
257
+
258
+ @GeneralUtilities.check_arguments
259
+ def get_codeunit_file(self) -> str:
260
+ return os.path.join(self.get_codeunit_folder(), f"{self.get_codeunit_name()}.codeunit.xml")
261
+
262
+ def get_type_environment_type(self)->str:
263
+ return self.__target_environment_type
264
+
265
+ def get_target_environment_type(self)->str:
266
+ return self.__target_environment_type
267
+
268
+ @GeneralUtilities.check_arguments
269
+ def copy_source_files_to_output_directory(self) -> None:
270
+ self._protected_sc.log.log("Copy sourcecode...")
271
+ codeunit_folder =self.get_codeunit_folder()
272
+ result = self._protected_sc.run_program_argsasarray("git", ["ls-tree", "-r", "HEAD", "--name-only"], codeunit_folder)
273
+ files = [f for f in result[1].split('\n') if len(f) > 0]
274
+ for file in files:
275
+ full_source_file = os.path.join(codeunit_folder, file)
276
+ if os.path.isfile(full_source_file):
277
+ # Reson of isdir-check:
278
+ # Prevent trying to copy files which are not exist.
279
+ # Otherwise exceptions occurr because uncommitted deletions of files will result in an error here.
280
+ target_file = os.path.join(codeunit_folder, "Other", "Artifacts", "SourceCode", file)
281
+ target_folder = os.path.dirname(target_file)
282
+ GeneralUtilities.ensure_directory_exists(target_folder)
283
+ shutil.copyfile(full_source_file, target_file)
284
+
285
+ @GeneralUtilities.check_arguments
286
+ def run_testcases_common_post_task(self, repository_folder: str, codeunit_name: str, generate_badges: bool, targetenvironmenttype: str) -> None:
287
+ self._protected_sc.assert_is_git_repository(repository_folder)
288
+ coverage_file_folder = os.path.join(repository_folder, codeunit_name, "Other/Artifacts/TestCoverage")
289
+ coveragefiletarget = os.path.join(coverage_file_folder, "TestCoverage.xml")
290
+ self.__update_path_of_source_in_testcoverage_file(repository_folder, codeunit_name)
291
+ self.__standardized_tasks_generate_coverage_report(repository_folder, codeunit_name, generate_badges, targetenvironmenttype)
292
+ self.__check_testcoverage(coveragefiletarget, repository_folder, codeunit_name)
293
+ self.__format_xml_file(coveragefiletarget)
294
+
295
+ @GeneralUtilities.check_arguments
296
+ def __format_xml_file(self, xmlfile:str) -> None:
297
+ GeneralUtilities.write_text_to_file(xmlfile,self.__format_xml_content( GeneralUtilities.read_text_from_file(xmlfile)))
298
+
299
+ @GeneralUtilities.check_arguments
300
+ def __format_xml_content(self, xml:str) -> None:
301
+ root = etree.fromstring(xml)
302
+ return etree.tostring(root, pretty_print=True, encoding="unicode")
303
+
304
+ @GeneralUtilities.check_arguments
305
+ def __standardized_tasks_generate_coverage_report(self, repository_folder: str, codeunitname: str, generate_badges: bool, targetenvironmenttype: str, add_testcoverage_history_entry: bool = None) -> None:
306
+ """This function expects that the file '<repositorybasefolder>/<codeunitname>/Other/Artifacts/TestCoverage/TestCoverage.xml'
307
+ which contains a test-coverage-report in the cobertura-format exists.
308
+ This script expectes that the testcoverage-reportfolder is '<repositorybasefolder>/<codeunitname>/Other/Artifacts/TestCoverageReport'.
309
+ This script expectes that a test-coverage-badges should be added to '<repositorybasefolder>/<codeunitname>/Other/Resources/Badges'."""
310
+ self._protected_sc.log.log("Generate testcoverage report..")
311
+ self._protected_sc.assert_is_git_repository(repository_folder)
312
+ codeunit_version = self.tfcps_Tools_General.get_version_of_codeunit(self.get_codeunit_file())
313
+ verbosity=0#TODO use loglevel-value here
314
+ if verbosity == 0:
315
+ verbose_argument_for_reportgenerator = "Off"
316
+ elif verbosity == 1:
317
+ verbose_argument_for_reportgenerator = "Error"
318
+ elif verbosity == 2:
319
+ verbose_argument_for_reportgenerator = "Info"
320
+ elif verbosity == 3:
321
+ verbose_argument_for_reportgenerator = "Verbose"
322
+ else:
323
+ raise ValueError(f"Unknown value for verbosity: {GeneralUtilities.str_none_safe(verbosity)}")
324
+
325
+ # Generating report
326
+ GeneralUtilities.ensure_directory_does_not_exist(os.path.join(repository_folder, codeunitname, f"{codeunitname}/Other/Artifacts/TestCoverageReport"))
327
+ GeneralUtilities.ensure_directory_exists(os.path.join(repository_folder, codeunitname, "Other/Artifacts/TestCoverageReport"))
328
+
329
+ if add_testcoverage_history_entry is None:
330
+ add_testcoverage_history_entry = self.__is_pre_merge
331
+
332
+ history_folder = f"{codeunitname}/Other/Resources/TestCoverageHistory"
333
+ history_folder_full = os.path.join(repository_folder, history_folder)
334
+ GeneralUtilities.ensure_directory_exists(history_folder_full)
335
+ history_argument = f" -historydir:{history_folder}"
336
+ argument = f"-reports:{codeunitname}/Other/Artifacts/TestCoverage/TestCoverage.xml -targetdir:{codeunitname}/Other/Artifacts/TestCoverageReport --verbosity:{verbose_argument_for_reportgenerator}{history_argument} -title:{codeunitname} -tag:v{codeunit_version}"
337
+ self._protected_sc.run_program("reportgenerator", argument, repository_folder)
338
+ if not add_testcoverage_history_entry:
339
+ os.remove(GeneralUtilities.get_direct_files_of_folder(history_folder_full)[-1])
340
+
341
+ # Generating badges
342
+ if generate_badges:
343
+ testcoverageubfolger = "Other/Resources/TestCoverageBadges"
344
+ fulltestcoverageubfolger = os.path.join(repository_folder, codeunitname, testcoverageubfolger)
345
+ GeneralUtilities.ensure_directory_does_not_exist(fulltestcoverageubfolger)
346
+ GeneralUtilities.ensure_directory_exists(fulltestcoverageubfolger)
347
+ self._protected_sc.run_program("reportgenerator", f"-reports:Other/Artifacts/TestCoverage/TestCoverage.xml -targetdir:{testcoverageubfolger} -reporttypes:Badges --verbosity:{verbose_argument_for_reportgenerator}", os.path.join(repository_folder, codeunitname))
348
+
349
+ @GeneralUtilities.check_arguments
350
+ def __update_path_of_source_in_testcoverage_file(self, repository_folder: str, codeunitname: str) -> None:
351
+ self._protected_sc.assert_is_git_repository(repository_folder)
352
+ self._protected_sc.log.log("Update paths of source files in testcoverage files..")
353
+ folder = f"{repository_folder}/{codeunitname}/Other/Artifacts/TestCoverage"
354
+ filename = "TestCoverage.xml"
355
+ full_file = os.path.join(folder, filename)
356
+ GeneralUtilities.write_text_to_file(full_file, re.sub("<source>.+<\\/source>", f"<source><!--[repository]/-->./{codeunitname}/</source>", GeneralUtilities.read_text_from_file(full_file)))
357
+ self.__remove_not_existing_files_from_testcoverage_file(full_file, repository_folder, codeunitname)
358
+
359
+ @GeneralUtilities.check_arguments
360
+ def __remove_not_existing_files_from_testcoverage_file(self, testcoveragefile: str, repository_folder: str, codeunit_name: str) -> None:
361
+ self._protected_sc.assert_is_git_repository(repository_folder)
362
+ root: etree._ElementTree = etree.parse(testcoveragefile)
363
+ codeunit_folder = os.path.join(repository_folder, codeunit_name)
364
+ xpath = f"//coverage/packages/package[@name='{codeunit_name}']/classes/class"
365
+ coverage_report_classes = root.xpath(xpath)
366
+ found_existing_files = False
367
+ for coverage_report_class in coverage_report_classes:
368
+ filename = coverage_report_class.attrib['filename']
369
+ file = os.path.join(codeunit_folder, filename)
370
+ if os.path.isfile(file):
371
+ found_existing_files = True
372
+ else:
373
+ coverage_report_class.getparent().remove(coverage_report_class)
374
+ GeneralUtilities.assert_condition(found_existing_files, f"No existing files in testcoverage-report-file \"{testcoveragefile}\".")
375
+ result = etree.tostring(root).decode("utf-8")
376
+ GeneralUtilities.write_text_to_file(testcoveragefile, result)
377
+
378
+ @GeneralUtilities.check_arguments
379
+ def __check_testcoverage(self, testcoverage_file_in_cobertura_format: str, repository_folder: str, codeunitname: str) -> None:
380
+ self._protected_sc.assert_is_git_repository(repository_folder)
381
+ self._protected_sc.log.log("Check testcoverage..")
382
+ root: etree._ElementTree = etree.parse(testcoverage_file_in_cobertura_format)
383
+ if len(root.xpath('//coverage/packages/package')) != 1:
384
+ raise ValueError(f"'{testcoverage_file_in_cobertura_format}' must contain exactly 1 package.")
385
+ if root.xpath('//coverage/packages/package[1]/@name')[0] != codeunitname:
386
+ raise ValueError(f"The package name of the tested package in '{testcoverage_file_in_cobertura_format}' must be '{codeunitname}'.")
387
+ rates=root.xpath('//coverage/packages/package[1]/@line-rate')
388
+ coverage_in_percent = round(float(str(rates[0]))*100, 2)
389
+ technicalminimalrequiredtestcoverageinpercent = 0
390
+ if not technicalminimalrequiredtestcoverageinpercent < coverage_in_percent:
391
+ raise ValueError(f"The test-coverage of package '{codeunitname}' must be greater than {technicalminimalrequiredtestcoverageinpercent}%.")
392
+ minimalrequiredtestcoverageinpercent = self.get_testcoverage_threshold_from_codeunit_file()
393
+ if (coverage_in_percent < minimalrequiredtestcoverageinpercent):
394
+ raise ValueError(f"The testcoverage for codeunit {codeunitname} must be {minimalrequiredtestcoverageinpercent}% or more but is {coverage_in_percent}%.")
395
+
396
+ @GeneralUtilities.check_arguments
397
+ def get_testcoverage_threshold_from_codeunit_file(self):
398
+ root: etree._ElementTree = etree.parse(self.get_codeunit_file())
399
+ return float(str(root.xpath('//cps:properties/cps:testsettings/@minimalcodecoverageinpercent', namespaces={'cps': 'https://projects.aniondev.de/PublicProjects/Common/ProjectTemplates/-/tree/main/Conventions/RepositoryStructure/CommonProjectStructure'})[0]))
400
+
401
+
402
+ @GeneralUtilities.check_arguments
403
+ def install_requirementstxt_for_codeunit(self):
404
+ self._protected_sc.install_requirementstxt_file(self.get_codeunit_folder()+"/Other/requirements.txt")
405
+
406
+ class TFCPS_CodeUnitSpecific_Base_CLI():
407
+
408
+ @staticmethod
409
+ @GeneralUtilities.check_arguments
410
+ def get_base_parser()->argparse.ArgumentParser:
411
+ parser = argparse.ArgumentParser()
412
+ verbosity_values = ", ".join(f"{lvl.value}={lvl.name}" for lvl in LogLevel)
413
+ parser.add_argument('-e', '--targetenvironmenttype', required=False, default="QualityCheck")
414
+ parser.add_argument('-a', '--additionalargumentsfile', required=False, default=None)
415
+ parser.add_argument('-v', '--verbosity', required=False, default=3, help=f"Sets the loglevel. Possible values: {verbosity_values}")
416
+ parser.add_argument('-c', '--nocache', action='store_true', required=False, default=False)
417
+ parser.add_argument('-p', '--ispremerge', action='store_true', required=False, default=False)
418
+ return parser
@@ -0,0 +1,128 @@
1
+ import os
2
+ import re
3
+ from ..GeneralUtilities import GeneralUtilities
4
+ from ..ScriptCollectionCore import ScriptCollectionCore
5
+ from ..SCLog import LogLevel
6
+ from .TFCPS_Tools_General import TFCPS_Tools_General
7
+
8
+
9
+ class TFCPS_CodeUnit_BuildCodeUnit:
10
+
11
+ codeunit_folder: str
12
+ repository_folder: str
13
+ sc: ScriptCollectionCore = ScriptCollectionCore()
14
+ codeunit_name: str
15
+ tFCPS_Tools: TFCPS_Tools_General
16
+ target_environment_type: str
17
+ additionalargumentsfile: str
18
+ use_cache: bool
19
+ is_pre_merge: bool
20
+
21
+ def __init__(self, codeunit_folder: str, verbosity: LogLevel, target_environment_type: str, additionalargumentsfile: str, use_cache: bool,is_pre_merge:bool):
22
+ self.sc = ScriptCollectionCore()
23
+ self.sc.log.loglevel = verbosity
24
+ self.tFCPS_Tools = TFCPS_Tools_General(self.sc)
25
+ self.tFCPS_Tools.assert_is_codeunit_folder(codeunit_folder)
26
+ self.codeunit_folder = codeunit_folder
27
+ self.codeunit_name = os.path.basename(self.codeunit_folder)
28
+ self.target_environment_type = target_environment_type
29
+ self.additionalargumentsfile = additionalargumentsfile
30
+ self.use_cache = use_cache
31
+ self.is_pre_merge=is_pre_merge
32
+
33
+ @GeneralUtilities.check_arguments
34
+ def build_codeunit(self) -> None:
35
+ codeunit_file: str = str(os.path.join(self.codeunit_folder, f"{self.codeunit_name}.codeunit.xml"))
36
+
37
+ if not self.tFCPS_Tools.codeunit_is_enabled(codeunit_file):
38
+ self.sc.log.log(f"Codeunit {self.codeunit_name} is disabled.", LogLevel.Warning)
39
+ return
40
+
41
+ self.sc.log.log(f"Build codeunit {self.codeunit_name}...")
42
+
43
+ GeneralUtilities.ensure_folder_exists_and_is_empty(self.codeunit_folder+"/Other/Artifacts")
44
+
45
+ arguments: str = f"--targetenvironmenttype {self.target_environment_type} --verbosity {int(self.sc.log.loglevel)}"
46
+ if self.additionalargumentsfile is not None:
47
+ arguments=arguments+f" --additionalargumentsfile {self.additionalargumentsfile}"
48
+ if not self.use_cache:
49
+ arguments = f"{arguments} --nocache"
50
+
51
+ if self.is_pre_merge:
52
+ arguments = f"{arguments} --ispremerge"
53
+
54
+ self.sc.log.log("Do common tasks...")
55
+ self.sc.run_program("python", f"CommonTasks.py {arguments}", os.path.join(self.codeunit_folder, "Other"), print_live_output=True)
56
+ self.verify_artifact_exists(self.codeunit_folder, dict[str, bool]({"License": True, "DiffReport": True}))
57
+
58
+ self.sc.log.log("Build...")
59
+ self.sc.run_program("python", f"Build.py {arguments}", os.path.join(self.codeunit_folder, "Other", "Build"), print_live_output=True)
60
+ artifacts = {"BuildResult_.+": True, "BOM": False, "SourceCode": self.tFCPS_Tools.codeunit_has_testable_sourcecode(codeunit_file)}
61
+ self.verify_artifact_exists(self.codeunit_folder, dict[str, bool](artifacts))
62
+
63
+ if self.tFCPS_Tools.codeunit_has_testable_sourcecode(codeunit_file):
64
+ self.sc.log.log("Run testcases...")
65
+ self.sc.run_program("python", f"RunTestcases.py {arguments}", os.path.join(self.codeunit_folder, "Other", "QualityCheck"), print_live_output=True)
66
+ self.verify_artifact_exists(self.codeunit_folder, dict[str, bool]({"TestCoverage": True, "TestCoverageReport": False}))
67
+
68
+ self.sc.log.log("Check for linting-issues...")
69
+ linting_result = self.sc.run_program("python", f"Linting.py {arguments}", os.path.join(self.codeunit_folder, "Other", "QualityCheck"), print_live_output=True, throw_exception_if_exitcode_is_not_zero=False)
70
+ if linting_result[0] != 0:
71
+ self.sc.log.log("Linting-issues were found.", LogLevel.Warning)
72
+
73
+ self.sc.log.log("Generate reference...")
74
+ self.sc.run_program("python", "GenerateReference.py", os.path.join(self.codeunit_folder, "Other", "Reference"), print_live_output=True)
75
+ self.verify_artifact_exists(self.codeunit_folder, dict[str, bool]({"Reference": True}))
76
+
77
+ if os.path.isfile(os.path.join(self.codeunit_folder, "Other", "OnBuildingFinished.py")):
78
+ self.sc.log.log('Run "OnBuildingFinished.py"...')
79
+ self.sc.run_program("python", f"OnBuildingFinished.py {arguments}", os.path.join(self.codeunit_folder, "Other"), print_live_output=True)
80
+
81
+ artifacts_folder = os.path.join(self.codeunit_folder, "Other", "Artifacts")
82
+ artifactsinformation_file = os.path.join(artifacts_folder, f"{self.codeunit_name}.artifactsinformation.xml")
83
+ codeunit_version = self.tFCPS_Tools.get_version_of_codeunit(codeunit_file)
84
+ GeneralUtilities.ensure_file_exists(artifactsinformation_file)
85
+ artifacts_list = []
86
+ for artifact_folder in GeneralUtilities.get_direct_folders_of_folder(artifacts_folder):
87
+ artifact_name = os.path.basename(artifact_folder)
88
+ artifacts_list.append(f" <cps:artifact>{artifact_name}<cps:artifact>")
89
+ artifacts = '\n'.join(artifacts_list)
90
+ moment = GeneralUtilities.datetime_to_string(GeneralUtilities.get_now())
91
+ # TODO implement usage of reference_latest_version_of_xsd_when_generating_xml
92
+ GeneralUtilities.write_text_to_file(artifactsinformation_file, f"""<?xml version="1.0" encoding="UTF-8" ?>
93
+ <cps:artifactsinformation xmlns:cps="https://projects.aniondev.de/PublicProjects/Common/ProjectTemplates/-/tree/main/Conventions/RepositoryStructure/CommonProjectStructure" artifactsinformationspecificationversion="1.0.0"
94
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://raw.githubusercontent.com/anionDev/ProjectTemplates/main/Templates/Conventions/RepositoryStructure/CommonProjectStructure/artifactsinformation.xsd">
95
+ <cps:name>{self.codeunit_name}</cps:name>
96
+ <cps:version>{codeunit_version}</cps:version>
97
+ <cps:timestamp>{moment}</cps:timestamp>
98
+ <cps:targetenvironmenttype>{self.target_environment_type}</cps:targetenvironmenttype>
99
+ <cps:artifacts>
100
+ {artifacts}
101
+ </cps:artifacts>
102
+ </cps:artifactsinformation>""")
103
+ # TODO validate artifactsinformation_file against xsd
104
+ self.sc.log.log(f"Finished building codeunit {self.codeunit_name} without errors.")
105
+
106
+
107
+ @GeneralUtilities.check_arguments
108
+ def verify_artifact_exists(self, codeunit_folder: str, artifact_name_regexes: dict[str, bool]) -> None:
109
+ codeunit_name: str = os.path.basename(codeunit_folder)
110
+ artifacts_folder = os.path.join(codeunit_folder, "Other/Artifacts")
111
+ existing_artifacts = [os.path.basename(x) for x in GeneralUtilities.get_direct_folders_of_folder(artifacts_folder)]
112
+ for artifact_name_regex, required in artifact_name_regexes.items():
113
+ artifact_exists = False
114
+ for existing_artifact in existing_artifacts:
115
+ pattern = re.compile(artifact_name_regex)
116
+ if pattern.match(existing_artifact):
117
+ artifact_exists = True
118
+ if not artifact_exists:
119
+ message = f"Codeunit {codeunit_name} does not contain an artifact which matches the name '{artifact_name_regex}'."
120
+ if required:
121
+ raise ValueError(message)
122
+ else:
123
+ self.sc.log.log(message, LogLevel.Warning)
124
+
125
+ @GeneralUtilities.check_arguments
126
+ def update_dependencies(self) -> None:
127
+ self.sc.log.log("Update dependencies...")
128
+ self.sc.run_program("python", "UpdateDependencies.py", os.path.join(self.codeunit_folder, "Other"))