1
1
#
2
2
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3
3
#
4
- import html
5
- import re
6
4
from abc import ABC
7
- from typing import Any , ClassVar , List , Optional , Tuple
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any , ClassVar , List , Optional , Tuple , cast
8
8
9
9
import pipelines .dagger .actions .system .docker
10
- import xmltodict
11
10
from dagger import CacheSharingMode , CacheVolume , Container , QueryError
12
11
from pipelines .airbyte_ci .connectors .context import ConnectorContext
13
12
from pipelines .consts import AMAZONCORRETTO_IMAGE
@@ -37,8 +36,18 @@ class GradleTask(Step, ABC):
37
36
bind_to_docker_host : ClassVar [bool ] = False
38
37
mount_connector_secrets : ClassVar [bool ] = False
39
38
with_test_report : ClassVar [bool ] = False
39
+ with_logs : ClassVar [bool ] = False
40
40
accept_extra_params = True
41
41
42
+ @property
43
+ def airbyte_logs_subdir (self ) -> str :
44
+ return datetime .fromtimestamp (cast (float , self .context .pipeline_start_timestamp )).isoformat () + "-" + self .gradle_task_name
45
+
46
+ @property
47
+ def test_artifacts_path (self ) -> Path :
48
+ test_artifacts_path = f"{ self .context .connector .code_directory } /build/test-artifacts/{ self .airbyte_logs_subdir } "
49
+ return Path (test_artifacts_path )
50
+
42
51
@property
43
52
def gradle_task_options (self ) -> Tuple [str , ...]:
44
53
return self .STATIC_GRADLE_OPTIONS + (f"-Ds3BuildCachePrefix={ self .context .connector .technical_name } " ,)
@@ -68,22 +77,22 @@ def _get_gradle_command(self, task: str, *args: Any, task_options: Optional[List
68
77
69
78
async def _run (self , * args : Any , ** kwargs : Any ) -> StepResult :
70
79
include = [
71
- ".root" ,
72
- ".env" ,
73
- "build.gradle" ,
74
- "deps.toml" ,
75
- "gradle.properties" ,
76
- "gradle" ,
77
- "gradlew" ,
78
- "settings.gradle" ,
79
- "build.gradle" ,
80
- "tools/gradle" ,
81
- "spotbugs-exclude-filter-file.xml" ,
82
- "buildSrc" ,
83
- "tools/bin/build_image.sh" ,
84
- "tools/lib/lib.sh" ,
85
- "pyproject.toml" ,
86
- ] + self .build_include
80
+ ".root" ,
81
+ ".env" ,
82
+ "build.gradle" ,
83
+ "deps.toml" ,
84
+ "gradle.properties" ,
85
+ "gradle" ,
86
+ "gradlew" ,
87
+ "settings.gradle" ,
88
+ "build.gradle" ,
89
+ "tools/gradle" ,
90
+ "spotbugs-exclude-filter-file.xml" ,
91
+ "buildSrc" ,
92
+ "tools/bin/build_image.sh" ,
93
+ "tools/lib/lib.sh" ,
94
+ "pyproject.toml" ,
95
+ ] + self .build_include
87
96
88
97
yum_packages_to_install = [
89
98
"docker" , # required by :integrationTestJava.
@@ -102,7 +111,7 @@ async def _run(self, *args: Any, **kwargs: Any) -> StepResult:
102
111
# Set GRADLE_HOME to the directory which will be rsync-ed with the gradle cache volume.
103
112
.with_env_variable ("GRADLE_HOME" , self .GRADLE_HOME_PATH )
104
113
# Same for GRADLE_USER_HOME.
105
- .with_env_variable ("GRADLE_USER_HOME" , self .GRADLE_HOME_PATH )
114
+ .with_env_variable ("GRADLE_USER_HOME" , self .GRADLE_HOME_PATH ). with_env_variable ( "AIRBYTE_LOG_SUBDIR" , self . airbyte_logs_subdir )
106
115
# Install a bunch of packages as early as possible.
107
116
.with_exec (
108
117
sh_dash_c (
@@ -193,6 +202,8 @@ async def _run(self, *args: Any, **kwargs: Any) -> StepResult:
193
202
connector_gradle_task = f":airbyte-integrations:connectors:{ self .context .connector .technical_name } :{ self .gradle_task_name } "
194
203
gradle_command = self ._get_gradle_command (connector_gradle_task , task_options = self .params_as_cli_options )
195
204
gradle_container = gradle_container .with_ (never_fail_exec ([gradle_command ]))
205
+ await self ._collect_logs (gradle_container )
206
+ await self ._collect_test_report (gradle_container )
196
207
return await self .get_step_result (gradle_container )
197
208
198
209
async def get_step_result (self , container : Container ) -> StepResult :
@@ -203,76 +214,38 @@ async def get_step_result(self, container: Container) -> StepResult:
203
214
status = step_result .status ,
204
215
stdout = step_result .stdout ,
205
216
stderr = step_result .stderr ,
206
- report = await self ._collect_test_report (container ),
207
217
output_artifact = step_result .output_artifact ,
218
+ test_artifacts_path = self .test_artifacts_path ,
208
219
)
209
220
210
- async def _collect_test_report (self , gradle_container : Container ) -> Optional [str ]:
221
+ async def _collect_logs (self , gradle_container : Container ) -> None :
222
+ if not self .with_logs :
223
+ return None
224
+ logs_dir_path = f"{ self .context .connector .code_directory } /build/test-logs/{ self .airbyte_logs_subdir } "
225
+ try :
226
+ container_logs_dir = await gradle_container .directory (logs_dir_path )
227
+ # the gradle task didn't create any logs.
228
+ if not container_logs_dir :
229
+ return None
230
+
231
+ self .test_artifacts_path .mkdir (parents = True , exist_ok = True )
232
+ if not await container_logs_dir .export (str (self .test_artifacts_path )):
233
+ self .context .logger .error ("Error when trying to export log files from container" )
234
+ except QueryError as e :
235
+ self .context .logger .error (str (e ))
236
+ self .context .logger .warn (f"Failed to retrieve junit test results from { logs_dir_path } gradle container." )
237
+ return None
238
+
239
+ async def _collect_test_report (self , gradle_container : Container ) -> None :
211
240
if not self .with_test_report :
212
241
return None
213
242
214
243
junit_xml_path = f"{ self .context .connector .code_directory } /build/test-results/{ self .gradle_task_name } "
215
- testsuites = []
216
244
try :
217
245
junit_xml_dir = await gradle_container .directory (junit_xml_path )
218
246
for file_name in await junit_xml_dir .entries ():
219
247
if file_name .endswith (".xml" ):
220
- junit_xml = await junit_xml_dir .file (file_name ).contents ()
221
- # This will be embedded in the HTML report in a <pre lang="xml"> block.
222
- # The java logging backend will have already taken care of masking any secrets.
223
- # Nothing to do in that regard.
224
- try :
225
- if testsuite := xmltodict .parse (junit_xml ):
226
- testsuites .append (testsuite )
227
- except Exception as e :
228
- self .context .logger .error (str (e ))
229
- self .context .logger .warn (f"Failed to parse junit xml file { file_name } ." )
248
+ export_succeeded = await junit_xml_dir .file (file_name ).export (str (self .test_artifacts_path ), allow_parent_dir_path = True )
230
249
except QueryError as e :
231
250
self .context .logger .error (str (e ))
232
- self .context .logger .warn (f"Failed to retrieve junit test results from { junit_xml_path } gradle container." )
233
251
return None
234
- return render_junit_xml (testsuites )
235
-
236
-
237
- MAYBE_STARTS_WITH_XML_TAG = re .compile ("^ *<" )
238
- ESCAPED_ANSI_COLOR_PATTERN = re .compile (r"\?\[0?m|\?\[[34][0-9]m" )
239
-
240
-
241
- def render_junit_xml (testsuites : List [Any ]) -> str :
242
- """Renders the JUnit XML report as something readable in the HTML test report."""
243
- # Transform the dict contents.
244
- indent = " "
245
- for testsuite in testsuites :
246
- testsuite = testsuite .get ("testsuite" )
247
- massage_system_out_and_err (testsuite , indent , 4 )
248
- if testcases := testsuite .get ("testcase" ):
249
- if not isinstance (testcases , list ):
250
- testcases = [testcases ]
251
- for testcase in testcases :
252
- massage_system_out_and_err (testcase , indent , 5 )
253
- # Transform back to XML string.
254
- # Try to respect the JUnit XML test result schema.
255
- root = {"testsuites" : {"testsuite" : testsuites }}
256
- xml = xmltodict .unparse (root , pretty = True , short_empty_elements = True , indent = indent )
257
- # Escape < and > and so forth to make them render properly, but not in the log messages.
258
- # These lines will already have been escaped by xmltodict.unparse.
259
- lines = xml .splitlines ()
260
- for idx , line in enumerate (lines ):
261
- if MAYBE_STARTS_WITH_XML_TAG .match (line ):
262
- lines [idx ] = html .escape (line )
263
- return "\n " .join (lines )
264
-
265
-
266
- def massage_system_out_and_err (d : dict , indent : str , indent_levels : int ) -> None :
267
- """Makes the system-out and system-err text prettier."""
268
- if d :
269
- for key in ["system-out" , "system-err" ]:
270
- if s := d .get (key ):
271
- lines = s .splitlines ()
272
- s = ""
273
- for line in lines :
274
- stripped = line .strip ()
275
- if stripped :
276
- s += "\n " + indent * indent_levels + ESCAPED_ANSI_COLOR_PATTERN .sub ("" , line .strip ())
277
- s = s + "\n " + indent * (indent_levels - 1 ) if s else None
278
- d [key ] = s
0 commit comments