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