ScriptCollection 4.0.11__py3-none-any.whl → 4.0.13__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 (31) hide show
  1. ScriptCollection/AnionBuildPlatform.py +2 -0
  2. ScriptCollection/ScriptCollectionCore.py +1 -1
  3. ScriptCollection/TFCPS/Docker/TFCPS_CodeUnitSpecific_Docker.py +88 -0
  4. ScriptCollection/TFCPS/Docker/__init__.py +0 -0
  5. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationBase.py +8 -0
  6. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationGenerate.py +6 -0
  7. ScriptCollection/TFCPS/DotNet/CertificateGeneratorInformationNoGenerate.py +7 -0
  8. ScriptCollection/TFCPS/DotNet/TFCPS_CodeUnitSpecific_DotNet.py +479 -0
  9. ScriptCollection/TFCPS/DotNet/__init__.py +0 -0
  10. ScriptCollection/TFCPS/Flutter/TFCPS_CodeUnitSpecific_Flutter.py +43 -0
  11. ScriptCollection/TFCPS/Flutter/__init__.py +0 -0
  12. ScriptCollection/TFCPS/NodeJS/TFCPS_CodeUnitSpecific_NodeJS.py +123 -0
  13. ScriptCollection/TFCPS/NodeJS/__init__.py +0 -0
  14. ScriptCollection/TFCPS/Python/TFCPS_CodeUnitSpecific_Python.py +114 -0
  15. ScriptCollection/TFCPS/Python/__init__.py +0 -0
  16. ScriptCollection/TFCPS/TFCPS_CodeUnitSpecific_Base.py +417 -0
  17. ScriptCollection/TFCPS/TFCPS_CodeUnit_BuildCodeUnit.py +120 -0
  18. ScriptCollection/TFCPS/TFCPS_CodeUnit_BuildCodeUnits.py +80 -0
  19. ScriptCollection/TFCPS/TFCPS_CreateRelease.py +97 -0
  20. ScriptCollection/TFCPS/TFCPS_Generic.py +43 -0
  21. ScriptCollection/TFCPS/TFCPS_MergeToMain.py +125 -0
  22. ScriptCollection/TFCPS/TFCPS_MergeToStable.py +361 -0
  23. ScriptCollection/TFCPS/TFCPS_Tools_Dependencies.py +16 -0
  24. ScriptCollection/TFCPS/TFCPS_Tools_General.py +1076 -0
  25. ScriptCollection/TFCPS/__init__.py +0 -0
  26. {scriptcollection-4.0.11.dist-info → scriptcollection-4.0.13.dist-info}/METADATA +1 -1
  27. scriptcollection-4.0.13.dist-info/RECORD +41 -0
  28. scriptcollection-4.0.11.dist-info/RECORD +0 -17
  29. {scriptcollection-4.0.11.dist-info → scriptcollection-4.0.13.dist-info}/WHEEL +0 -0
  30. {scriptcollection-4.0.11.dist-info → scriptcollection-4.0.13.dist-info}/entry_points.txt +0 -0
  31. {scriptcollection-4.0.11.dist-info → scriptcollection-4.0.13.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,417 @@
1
+ import os
2
+ import traceback
3
+ from pathlib import Path
4
+ import shutil
5
+ import re
6
+ import json
7
+ import argparse
8
+ from abc import ABC
9
+ import xmlschema
10
+ from lxml import etree
11
+ from ..GeneralUtilities import GeneralUtilities
12
+ from ..ScriptCollectionCore import ScriptCollectionCore
13
+ from ..SCLog import LogLevel
14
+ from .TFCPS_Tools_General import TFCPS_Tools_General
15
+ from .TFCPS_Tools_Dependencies import TFCPS_Tools_Dependencies,Dependency
16
+
17
+ class TFCPS_CodeUnitSpecific_Base(ABC):
18
+
19
+ __current_file:str=None
20
+ __target_environment_type:str
21
+ __repository_folder:str=None
22
+ __codeunit_folder:str=None
23
+ __current_folder:str=None
24
+ __verbosity:LogLevel=None
25
+ __use_cache:bool=None
26
+ tfcps_Tools_General:TFCPS_Tools_General
27
+ _protected_sc:ScriptCollectionCore
28
+ __is_pre_merge:bool=False#TODO must be setable to true
29
+ __validate_developers_of_repository:bool=True#TODO must be setable to false
30
+
31
+ def __init__(self,current_file:str,verbosity:LogLevel,target_envionment_type:str,use_cache:bool):
32
+ self.__verbosity=verbosity
33
+ self.__use_cache=use_cache
34
+ self.__target_environment_type=target_envionment_type
35
+ self.__current_file = str(Path(current_file).absolute())
36
+ self.__current_folder = os.path.dirname(self.__current_file)
37
+ self.__codeunit_folder=self.__search_codeunit_folder()
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
+ def update_dependencies_default(self):
59
+ d:TFCPS_Tools_Dependencies=TFCPS_Tools_Dependencies()
60
+ dependencies:list[Dependency]=d.get_dependencies()
61
+ for dependency in dependencies:
62
+ if dependency.current_version!=dependency.latest_version:
63
+ pass#TODO update dependency
64
+
65
+ def get_version_of_project(self)->str:
66
+ return self.tfcps_Tools_General.get_version_of_project(self.get_repository_folder())
67
+
68
+ @GeneralUtilities.check_arguments
69
+ def do_common_tasks_base(self,current_codeunit_version:str):
70
+
71
+ repository_folder: str =self.get_repository_folder()
72
+ self._protected_sc.assert_is_git_repository(repository_folder)
73
+ codeunit_name: str = self.get_codeunit_name()
74
+ project_version = self.tfcps_Tools_General.get_version_of_project(repository_folder)
75
+ if current_codeunit_version is None:
76
+ current_codeunit_version=project_version
77
+ codeunit_folder = os.path.join(repository_folder, codeunit_name)
78
+
79
+ # check codeunit-conformity
80
+ # TODO check if foldername=="<codeunitname>[.codeunit.xml]" == <codeunitname> in file
81
+ supported_codeunitspecificationversion = "2.9.4" # should always be the latest version of the ProjectTemplates-repository
82
+ codeunit_file = os.path.join(codeunit_folder, f"{codeunit_name}.codeunit.xml")
83
+ if not os.path.isfile(codeunit_file):
84
+ raise ValueError(f'Codeunitfile "{codeunit_file}" does not exist.')
85
+ # TODO implement usage of self.reference_latest_version_of_xsd_when_generating_xml
86
+ namespaces = {'cps': 'https://projects.aniondev.de/PublicProjects/Common/ProjectTemplates/-/tree/main/Conventions/RepositoryStructure/CommonProjectStructure', 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}
87
+ root: etree._ElementTree = etree.parse(codeunit_file)
88
+
89
+ # check codeunit-spcecification-version
90
+ try:
91
+ codeunit_file_version = root.xpath('//cps:codeunit/@codeunitspecificationversion', namespaces=namespaces)[0]
92
+ if codeunit_file_version != supported_codeunitspecificationversion:
93
+ raise ValueError(f"ScriptCollection only supports processing codeunits with codeunit-specification-version={supported_codeunitspecificationversion}.")
94
+ schemaLocation = root.xpath('//cps:codeunit/@xsi:schemaLocation', namespaces=namespaces)[0]
95
+ xmlschema.validate(codeunit_file, schemaLocation)
96
+ # TODO check if the properties codeunithastestablesourcecode, codeunithasupdatabledependencies, throwexceptionifcodeunitfilecannotbevalidated, developmentState and description exist and the values are valid
97
+ except Exception as exception:
98
+ self._protected_sc.log.log_exception(f'Codeunitfile "{codeunit_file}" can not be validated due to the following exception:', exception,traceback,LogLevel.Warning)
99
+
100
+ # check codeunit-name
101
+ codeunit_name_in_codeunit_file = root.xpath('//cps:codeunit/cps:name/text()', namespaces=namespaces)[0]
102
+ if codeunit_name != codeunit_name_in_codeunit_file:
103
+ raise ValueError(f"The folder-name ('{codeunit_name}') is not equal to the codeunit-name ('{codeunit_name_in_codeunit_file}').")
104
+
105
+ # check owner-name
106
+ codeunit_ownername_in_codeunit_file = self.tfcps_Tools_General. get_codeunit_owner_name(self.get_codeunit_file())
107
+ GeneralUtilities.assert_condition(GeneralUtilities.string_has_content(codeunit_ownername_in_codeunit_file), "No valid name for codeunitowner given.")
108
+
109
+ # check owner-emailaddress
110
+ codeunit_owneremailaddress_in_codeunit_file = self.tfcps_Tools_General.get_codeunit_owner_emailaddress(self.get_codeunit_file())
111
+ GeneralUtilities.assert_condition(GeneralUtilities.string_has_content(codeunit_owneremailaddress_in_codeunit_file), "No valid email-address for codeunitowner given.")
112
+
113
+ # check development-state
114
+ developmentstate = root.xpath('//cps:properties/@developmentstate', namespaces=namespaces)[0]
115
+ developmentstate_active = "Active development"
116
+ developmentstate_maintenance = "Maintenance-updates only"
117
+ developmentstate_inactive = "Inactive"
118
+ 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}'.")
119
+
120
+ # check for mandatory files
121
+ files = ["Other/Build/Build.py", "Other/QualityCheck/Linting.py", "Other/Reference/GenerateReference.py"]
122
+ if self.tfcps_Tools_General.codeunit_has_testable_sourcecode(self.get_codeunit_file()):
123
+ # TODO check if the testsettings-section appears in the codeunit-file
124
+ files.append("Other/QualityCheck/RunTestcases.py")
125
+ if self.tfcps_Tools_General.codeunit_has_updatable_dependencies(self.get_codeunit_file()):
126
+ # TODO check if the updatesettings-section appears in the codeunit-file
127
+ files.append("Other/UpdateDependencies.py")
128
+ for file in files:
129
+ combined_file = os.path.join(codeunit_folder, file)
130
+ if not os.path.isfile(combined_file):
131
+ raise ValueError(f'The mandatory file "{file}" does not exist in the codeunit-folder.')
132
+
133
+ if os.path.isfile(os.path.join(codeunit_folder, "Other", "requirements.txt")):
134
+ self.install_requirementstxt_for_codeunit()
135
+
136
+ # check developer
137
+ if self.__validate_developers_of_repository:
138
+ expected_authors: list[tuple[str, str]] = []
139
+ expected_authors_in_xml = root.xpath('//cps:codeunit/cps:developerteam/cps:developer', namespaces=namespaces)
140
+ for expected_author in expected_authors_in_xml:
141
+ author_name = expected_author.xpath('./cps:developername/text()', namespaces=namespaces)[0]
142
+ author_emailaddress = expected_author.xpath('./cps:developeremailaddress/text()', namespaces=namespaces)[0]
143
+ expected_authors.append((author_name, author_emailaddress))
144
+ actual_authors: list[tuple[str, str]] = self.tfcps_Tools_General.get_all_authors_and_committers_of_repository(repository_folder, codeunit_name)
145
+ # TODO refactor this check to only check commits which are behind this but which are not already on main
146
+ # TODO verify also if the commit is signed by a valid key of the author
147
+ for actual_author in actual_authors:
148
+ if not (actual_author) in expected_authors:
149
+ actual_author_formatted = f"{actual_author[0]} <{actual_author[1]}>"
150
+ 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.')
151
+
152
+ dependent_codeunits = self.tfcps_Tools_General.get_dependent_code_units(codeunit_file)
153
+ for dependent_codeunit in dependent_codeunits:
154
+ if not self.tfcps_Tools_General.dependent_codeunit_exists(repository_folder, dependent_codeunit):
155
+ raise ValueError(f"Codeunit {codeunit_name} does have dependent codeunit {dependent_codeunit} which does not exist.")
156
+
157
+ # TODO implement cycle-check for dependent codeunits
158
+
159
+ artifacts_folder = os.path.join(codeunit_folder, "Other", "Artifacts")
160
+ GeneralUtilities.ensure_directory_does_not_exist(artifacts_folder)
161
+
162
+ # get artifacts from dependent codeunits
163
+ self.tfcps_Tools_General.copy_artifacts_from_dependent_code_units(repository_folder, codeunit_name)
164
+
165
+ # update codeunit-version
166
+ self.tfcps_Tools_General.write_version_to_codeunit_file(self.get_codeunit_file(), current_codeunit_version)
167
+
168
+ # set project version
169
+ package_json_file = os.path.join(repository_folder, "package.json") # TDOO move this to a general project-specific (and codeunit-independent-script)
170
+ if os.path.isfile(package_json_file):
171
+ package_json_data: str = None
172
+ with open(package_json_file, "r", encoding="utf-8") as f1:
173
+ package_json_data = json.load(f1)
174
+ package_json_data["version"] = project_version
175
+ with open(package_json_file, "w", encoding="utf-8") as f2:
176
+ json.dump(package_json_data, f2, indent=2)
177
+ GeneralUtilities.write_text_to_file(package_json_file, GeneralUtilities.read_text_from_file(package_json_file).replace("\r", ""))
178
+
179
+ # set default constants
180
+ self.tfcps_Tools_General.set_default_constants(os.path.join(codeunit_folder))
181
+
182
+ # Copy changelog-file
183
+ changelog_folder = os.path.join(repository_folder, "Other", "Resources", "Changelog")
184
+ changelog_file = os.path.join(changelog_folder, f"v{project_version}.md")
185
+ target_folder = os.path.join(codeunit_folder, "Other", "Artifacts", "Changelog")
186
+ GeneralUtilities.ensure_directory_exists(target_folder)
187
+ shutil.copy(changelog_file, target_folder)
188
+
189
+ # Hints-file
190
+ hints_file = os.path.join(codeunit_folder, "Other", "Reference", "ReferenceContent", "Hints.md")
191
+ if not os.path.isfile(hints_file):
192
+ raise ValueError(f"Hints-file '{hints_file}' does not exist.")
193
+
194
+ # Copy license-file
195
+ self.tfcps_Tools_General.copy_licence_file(self.get_codeunit_folder())
196
+
197
+ # Generate diff-report
198
+ self.tfcps_Tools_General.generate_diff_report(repository_folder, codeunit_name, self.tfcps_Tools_General.get_version_of_codeunit(self.get_codeunit_file()))
199
+
200
+ # TODO check for secrets using TruffleHog
201
+ # TODO run static code analysis tool to search for vulnerabilities
202
+
203
+ d:TFCPS_Tools_Dependencies=TFCPS_Tools_Dependencies()
204
+ dependencies:list[Dependency]=d.get_dependencies()
205
+ for dependency in dependencies:
206
+ #TODO show warning if the latest version of dependency is too old
207
+ if dependency.current_version!=dependency.latest_version:
208
+ dependency_is_disabled_for_update=False#TODO read this value from codeunit-file
209
+ if not dependency_is_disabled_for_update:
210
+ self._protected_sc.log.log(f"Dependency \"{dependency.name}\" is used in the outdated version v{dependency.current_version} and can be upudated to v{dependency.latest_version}",LogLevel.Warning)
211
+
212
+
213
+
214
+
215
+ @GeneralUtilities.check_arguments
216
+ def generate_reference_using_docfx(self=None):
217
+ reference_folder =os.path.join( self.get_codeunit_folder(),"Other","Reference")
218
+ generated_reference_folder = GeneralUtilities.resolve_relative_path("../Artifacts/Reference", reference_folder)
219
+ GeneralUtilities.ensure_directory_does_not_exist(generated_reference_folder)
220
+ GeneralUtilities.ensure_directory_exists(generated_reference_folder)
221
+ obj_folder = os.path.join(reference_folder, "obj")
222
+ GeneralUtilities.ensure_folder_exists_and_is_empty(obj_folder)
223
+ self._protected_sc.run_program("docfx", "-t default,templates/darkfx docfx.json", reference_folder)
224
+ GeneralUtilities.ensure_directory_does_not_exist(obj_folder)
225
+
226
+ @GeneralUtilities.check_arguments
227
+ def use_cache(self)->bool:
228
+ return self.__use_cache
229
+
230
+ @GeneralUtilities.check_arguments
231
+ def update_dependencies_base(self):
232
+ self.update_dependencies_default()
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 testcoderage-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
+ return parser
@@ -0,0 +1,120 @@
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
+
20
+ def __init__(self, codeunit_folder: str, verbosity: LogLevel, target_environment_type: str, additionalargumentsfile: str, use_cache: bool):
21
+ self.sc = ScriptCollectionCore()
22
+ self.sc.log.loglevel = verbosity
23
+ self.tFCPS_Tools = TFCPS_Tools_General(self.sc)
24
+ self.tFCPS_Tools.assert_is_codeunit_folder(codeunit_folder)
25
+ self.codeunit_folder = codeunit_folder
26
+ self.codeunit_name = os.path.basename(self.codeunit_folder)
27
+ self.target_environment_type = target_environment_type
28
+ self.additionalargumentsfile = additionalargumentsfile
29
+ self.use_cache = use_cache
30
+
31
+ @GeneralUtilities.check_arguments
32
+ def build_codeunit(self) -> None:
33
+ codeunit_file: str = str(os.path.join(self.codeunit_folder, f"{self.codeunit_name}.codeunit.xml"))
34
+
35
+ if not self.tFCPS_Tools.codeunit_is_enabled(codeunit_file):
36
+ self.sc.log.log(f"Codeunit {self.codeunit_name} is disabled.", LogLevel.Warning)
37
+ return
38
+
39
+ self.sc.log.log(f"Build codeunit {self.codeunit_name}...")
40
+
41
+ GeneralUtilities.ensure_folder_exists_and_is_empty(self.codeunit_folder+"/Other/Artifacts")
42
+
43
+ arguments: str = f"--targetenvironmenttype {self.target_environment_type} --additionalargumentsfile {self.additionalargumentsfile} --verbosity {int(self.sc.log.loglevel)}"
44
+ if not self.use_cache:
45
+ arguments = f"{arguments} --nocache"
46
+
47
+ self.sc.log.log("Do common tasks...")
48
+ self.sc.run_program("python", f"CommonTasks.py {arguments}", os.path.join(self.codeunit_folder, "Other"), print_live_output=True)
49
+ self.verify_artifact_exists(self.codeunit_folder, dict[str, bool]({"Changelog": False, "License": True, "DiffReport": True}))
50
+
51
+ self.sc.log.log("Build...")
52
+ self.sc.run_program("python", f"Build.py {arguments}", os.path.join(self.codeunit_folder, "Other", "Build"), print_live_output=True)
53
+ artifacts = {"BuildResult_.+": True, "BOM": False, "SourceCode": self.tFCPS_Tools.codeunit_has_testable_sourcecode(codeunit_file)}
54
+ self.verify_artifact_exists(self.codeunit_folder, dict[str, bool](artifacts))
55
+
56
+ if self.tFCPS_Tools.codeunit_has_testable_sourcecode(codeunit_file):
57
+ self.sc.log.log("Run testcases...")
58
+ self.sc.run_program("python", f"RunTestcases.py {arguments}", os.path.join(self.codeunit_folder, "Other", "QualityCheck"), print_live_output=True)
59
+ self.verify_artifact_exists(self.codeunit_folder, dict[str, bool]({"TestCoverage": True, "TestCoverageReport": False}))
60
+
61
+ self.sc.log.log("Check for linting-issues...")
62
+ 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)
63
+ if linting_result[0] != 0:
64
+ self.sc.log.log("Linting-issues were found.", LogLevel.Warning)
65
+
66
+ self.sc.log.log("Generate reference...")
67
+ self.sc.run_program("python", "GenerateReference.py", os.path.join(self.codeunit_folder, "Other", "Reference"), print_live_output=True)
68
+ self.verify_artifact_exists(self.codeunit_folder, dict[str, bool]({"Reference": True}))
69
+
70
+ if os.path.isfile(os.path.join(self.codeunit_folder, "Other", "OnBuildingFinished.py")):
71
+ self.sc.log.log('Run "OnBuildingFinished.py"...')
72
+ self.sc.run_program("python", f"OnBuildingFinished.py {arguments}", os.path.join(self.codeunit_folder, "Other"), print_live_output=True)
73
+
74
+ artifacts_folder = os.path.join(self.codeunit_folder, "Other", "Artifacts")
75
+ artifactsinformation_file = os.path.join(artifacts_folder, f"{self.codeunit_name}.artifactsinformation.xml")
76
+ codeunit_version = self.tFCPS_Tools.get_version_of_codeunit(codeunit_file)
77
+ GeneralUtilities.ensure_file_exists(artifactsinformation_file)
78
+ artifacts_list = []
79
+ for artifact_folder in GeneralUtilities.get_direct_folders_of_folder(artifacts_folder):
80
+ artifact_name = os.path.basename(artifact_folder)
81
+ artifacts_list.append(f" <cps:artifact>{artifact_name}<cps:artifact>")
82
+ artifacts = '\n'.join(artifacts_list)
83
+ moment = GeneralUtilities.datetime_to_string(GeneralUtilities.get_now())
84
+ # TODO implement usage of reference_latest_version_of_xsd_when_generating_xml
85
+ GeneralUtilities.write_text_to_file(artifactsinformation_file, f"""<?xml version="1.0" encoding="UTF-8" ?>
86
+ <cps:artifactsinformation xmlns:cps="https://projects.aniondev.de/PublicProjects/Common/ProjectTemplates/-/tree/main/Conventions/RepositoryStructure/CommonProjectStructure" artifactsinformationspecificationversion="1.0.0"
87
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://raw.githubusercontent.com/anionDev/ProjectTemplates/main/Templates/Conventions/RepositoryStructure/CommonProjectStructure/artifactsinformation.xsd">
88
+ <cps:name>{self.codeunit_name}</cps:name>
89
+ <cps:version>{codeunit_version}</cps:version>
90
+ <cps:timestamp>{moment}</cps:timestamp>
91
+ <cps:targetenvironmenttype>{self.target_environment_type}</cps:targetenvironmenttype>
92
+ <cps:artifacts>
93
+ {artifacts}
94
+ </cps:artifacts>
95
+ </cps:artifactsinformation>""")
96
+ # TODO validate artifactsinformation_file against xsd
97
+ self.sc.log.log(f"Finished building codeunit {self.codeunit_name} without errors.")
98
+
99
+ @GeneralUtilities.check_arguments
100
+ def verify_artifact_exists(self, codeunit_folder: str, artifact_name_regexes: dict[str, bool]) -> None:
101
+ codeunit_name: str = os.path.basename(codeunit_folder)
102
+ artifacts_folder = os.path.join(codeunit_folder, "Other/Artifacts")
103
+ existing_artifacts = [os.path.basename(x) for x in GeneralUtilities.get_direct_folders_of_folder(artifacts_folder)]
104
+ for artifact_name_regex, required in artifact_name_regexes.items():
105
+ artifact_exists = False
106
+ for existing_artifact in existing_artifacts:
107
+ pattern = re.compile(artifact_name_regex)
108
+ if pattern.match(existing_artifact):
109
+ artifact_exists = True
110
+ if not artifact_exists:
111
+ message = f"Codeunit {codeunit_name} does not contain an artifact which matches the name '{artifact_name_regex}'."
112
+ if required:
113
+ raise ValueError(message)
114
+ else:
115
+ self.sc.log.log(message, LogLevel.Warning)
116
+
117
+ @GeneralUtilities.check_arguments
118
+ def update_dependencies(self) -> None:
119
+ self.sc.log.log("Update dependencies...")
120
+ self.sc.run_program("python", "UpdateDependencies.py", os.path.join(self.codeunit_folder, "Other"))
@@ -0,0 +1,80 @@
1
+ import os
2
+ from ..GeneralUtilities import GeneralUtilities
3
+ from ..ScriptCollectionCore import ScriptCollectionCore
4
+ from ..SCLog import LogLevel
5
+ from .TFCPS_CodeUnit_BuildCodeUnit import TFCPS_CodeUnit_BuildCodeUnit
6
+ from .TFCPS_Tools_General import TFCPS_Tools_General
7
+
8
+ class TFCPS_CodeUnit_BuildCodeUnits:
9
+ repository:str=None
10
+ tFCPS_Other:TFCPS_Tools_General=None
11
+ sc:ScriptCollectionCore=None
12
+ target_environment_type:str=None
13
+ additionalargumentsfile:str=None
14
+ __use_cache:bool
15
+ __is_pre_merge:bool
16
+
17
+ def __init__(self,repository:str,loglevel:LogLevel,target_environment_type:str,additionalargumentsfile:str,use_cache:bool,is_pre_merge:bool):
18
+ self.sc=ScriptCollectionCore()
19
+ self.sc.log.loglevel=loglevel
20
+ self.__use_cache=use_cache
21
+ self.sc.assert_is_git_repository(repository)
22
+ self.repository=repository
23
+ self.tFCPS_Other:TFCPS_Tools_General=TFCPS_Tools_General(self.sc)
24
+ allowed_target_environment_types=["Development","QualityCheck","Productive"]
25
+ GeneralUtilities.assert_condition(target_environment_type in allowed_target_environment_types,"Unknown target-environment-type. Allowed values are: "+", ".join(allowed_target_environment_types))
26
+ self.target_environment_type=target_environment_type
27
+ self.additionalargumentsfile=additionalargumentsfile
28
+ self.__is_pre_merge=is_pre_merge
29
+
30
+ @GeneralUtilities.check_arguments
31
+ def build_codeunits(self) -> None:
32
+ self.sc.log.log(GeneralUtilities.get_line())
33
+ self.sc.log.log(f"Start building codeunits. (Target environment-type: {self.target_environment_type})")
34
+ changelog_file=os.path.join(self.repository,"Other","Resources","Changelog",f"v{self.tFCPS_Other.get_version_of_project(self.repository)}.md")
35
+ GeneralUtilities.assert_file_exists(changelog_file,f"Changelogfile \"{changelog_file}\" does not exist. Try to create it for example using \"sccreatechangelogentry -m ...\".")
36
+ if os.path.isfile( os.path.join(self.repository,"Other","Scripts","PrepareBuildCodeunits.py")):
37
+ arguments:str=f"--targetenvironmenttype {self.target_environment_type} --additionalargumentsfile {self.additionalargumentsfile} --verbosity {int(self.sc.log.loglevel)}"
38
+ if not self.__use_cache:
39
+ arguments=f"{arguments} --nocache"
40
+ if self.sc.git_repository_has_uncommitted_changes(self.repository):
41
+ self.sc.log.log("No-cache-option can not be applied because there are uncommited changes in the repository.",LogLevel.Warning)
42
+ else:
43
+ self.sc.run_program("git","clean -dfx",self.repository)
44
+ self.sc.log.log("Prepare build codeunits...")
45
+ self.sc.run_program("python", f"PrepareBuildCodeunits.py {arguments}", os.path.join(self.repository,"Other","Scripts"),print_live_output=True)
46
+ codeunits:list[str]=self.tFCPS_Other.get_codeunits(self.repository)
47
+ self.sc.log.log("Codeunits will be built in the following order:")
48
+ for codeunit_name in codeunits:
49
+ self.sc.log.log(" - "+codeunit_name)
50
+ for codeunit_name in codeunits:
51
+ tFCPS_CodeUnit_BuildCodeUnit:TFCPS_CodeUnit_BuildCodeUnit = TFCPS_CodeUnit_BuildCodeUnit(os.path.join(self.repository,codeunit_name),self.sc.log.loglevel,self.target_environment_type,self.additionalargumentsfile,self.use_cache())
52
+ self.sc.log.log(GeneralUtilities.get_line())
53
+ tFCPS_CodeUnit_BuildCodeUnit.build_codeunit()
54
+ self.sc.log.log(GeneralUtilities.get_line())
55
+ self.sc.log.log("Finished building codeunits.")
56
+ self.sc.log.log(GeneralUtilities.get_line())
57
+
58
+
59
+ @GeneralUtilities.check_arguments
60
+ def use_cache(self) -> bool:
61
+ return self.__use_cache
62
+
63
+
64
+ @GeneralUtilities.check_arguments
65
+ def is_pre_merge(self) -> bool:
66
+ return self.__is_pre_merge
67
+
68
+ @GeneralUtilities.check_arguments
69
+ def update_dependencies(self) -> None:
70
+ self.update_year_in_license_file()
71
+
72
+ #TODO update project-wide-dependencies here
73
+ codeunits:list[str]=self.tFCPS_Other.get_codeunits(self.repository)
74
+ for codeunit_name in codeunits:
75
+ tFCPS_CodeUnit_BuildCodeUnit:TFCPS_CodeUnit_BuildCodeUnit = TFCPS_CodeUnit_BuildCodeUnit(os.path.join(self.repository,codeunit_name),self.sc.log.loglevel,self.target_environment_type,self.additionalargumentsfile,self.use_cache())
76
+ tFCPS_CodeUnit_BuildCodeUnit.update_dependencies()
77
+
78
+ @GeneralUtilities.check_arguments
79
+ def update_year_in_license_file(self) -> None:
80
+ self.sc.update_year_in_first_line_of_file(os.path.join(self.repository, "License.txt"))