Prevent cleanup of parent when substep has failed
[testsuite/pythonsdk-tests.git] / src / onaptests / steps / base.py
1 import functools
2 import itertools
3 import logging
4 import logging.config
5 import os
6 import time
7 from abc import ABC, abstractmethod
8 from typing import Iterator, List, Optional
9
10 from onapsdk.aai.business import Customer, ServiceInstance, ServiceSubscription
11 from onapsdk.configuration import settings
12 from onapsdk.exceptions import SDKException, SettingsError
13
14 from onaptests.steps.reports_collection import (Report, ReportsCollection,
15                                                 ReportStepStatus)
16 from onaptests.utils.exceptions import (OnapTestException,
17                                         OnapTestExceptionGroup,
18                                         SubstepExecutionException,
19                                         SubstepExecutionExceptionGroup,
20                                         TestConfigurationException)
21
22 # pylint: disable=protected-access
23 IF_FORCE_CLEANUP = "PYTHON_SDK_TESTS_FORCE_CLEANUP"
24
25
26 class StoreStateHandler(ABC):
27     """Decorator for storing the state of executed test step."""
28
29     @classmethod
30     def store_state(cls, fun=None, *, cleanup=False):  # noqa
31         if fun is None:
32             return functools.partial(cls.store_state, cleanup=cleanup)
33
34         @functools.wraps(fun)
35         def wrapper(self, *args, **kwargs):
36             if (cleanup and self._state_clean) or (not cleanup and self._state_execute):
37                 raise RuntimeError("%s step executed twice" % self._step_title(cleanup))
38             if cleanup:
39                 self._state_clean = True
40             else:
41                 self._state_execute = True
42             initial_exception = None
43             try:
44                 execution_status: Optional[ReportStepStatus] = ReportStepStatus.FAIL
45                 if cleanup:
46                     self._start_cleanup_time = time.time()
47                     try:
48                         if (self._cleanup and self._state_execute and
49                                 (not self.has_substeps or self._substeps_executed) and
50                                 (self._is_validation_only or
51                                     self.check_preconditions(cleanup=True))):
52                             self._log_execution_state("START", cleanup)
53                             if not self._is_validation_only or self._is_force_cleanup:
54                                 fun(self, *args, **kwargs)
55                             self._cleaned_up = True
56                             execution_status = ReportStepStatus.PASS
57                         else:
58                             execution_status = ReportStepStatus.NOT_EXECUTED
59                     except (OnapTestException, SDKException) as test_exc:
60                         initial_exception = test_exc
61                     finally:
62                         self._log_execution_state(execution_status.name, cleanup)
63                         self._cleanup_substeps()
64                     if initial_exception:
65                         new_exception = initial_exception
66                         initial_exception = None
67                         raise new_exception
68                 else:
69                     if self._is_validation_only or self.check_preconditions():
70                         self._log_execution_state("START", cleanup)
71                         self._execute_substeps()
72                         if not self._is_validation_only:
73                             fun(self, *args, **kwargs)
74                         execution_status = ReportStepStatus.PASS
75                         self._executed = True
76                     else:
77                         execution_status = ReportStepStatus.NOT_EXECUTED
78             except SubstepExecutionException as substep_exc:
79                 if not cleanup:
80                     execution_status = ReportStepStatus.NOT_EXECUTED
81                 if initial_exception:
82                     substep_exc = OnapTestExceptionGroup("Cleanup Exceptions",
83                                                          [initial_exception, substep_exc])
84                 raise substep_exc
85             except (OnapTestException, SDKException) as test_exc:
86                 if initial_exception:
87                     test_exc = OnapTestExceptionGroup("Cleanup Exceptions",
88                                                       [initial_exception, test_exc])
89                 raise test_exc
90             finally:
91                 if not cleanup:
92                     self._log_execution_state(execution_status.name, cleanup)
93                 if cleanup:
94                     self._cleanup_report = Report(
95                         step_description=self._step_title(cleanup),
96                         step_execution_status=execution_status,
97                         step_execution_duration=time.time() - self._start_cleanup_time,
98                         step_component=self.component
99                     )
100                 else:
101                     if not self._start_execution_time:
102                         if execution_status != ReportStepStatus.NOT_EXECUTED:
103                             self._logger.error("No execution start time saved for %s step. "
104                                                "Fix it by call `super.execute()` "
105                                                "in step class `execute()` method definition",
106                                                self.name)
107                         self._start_execution_time = time.time()
108                     self._execution_report = Report(
109                         step_description=self._step_title(cleanup),
110                         step_execution_status=(execution_status if execution_status else
111                                                ReportStepStatus.FAIL),
112                         step_execution_duration=time.time() - self._start_execution_time,
113                         step_component=self.component
114                     )
115             wrapper._is_wrapped = True
116         return wrapper
117
118
119 class BaseStep(StoreStateHandler, ABC):
120     """Base step class."""
121
122     # Indicates that Step has no dedicated cleanup method
123     HAS_NO_CLEANUP = False
124
125     _logger: logging.Logger = logging.getLogger("")
126
127     def __init_subclass__(cls):
128         """Subclass initialization.
129
130         Add _logger property for any BaseStep subclass
131         """
132         super().__init_subclass__()
133         cls._logger: logging.Logger = logging.getLogger("")
134         logging.config.dictConfig(settings.LOG_CONFIG)
135         # Setup Proxy if SOCK_HTTP is defined in settings
136         try:
137             cls.set_proxy(settings.SOCK_HTTP)
138         except SettingsError:
139             pass
140
141     def __init__(self, cleanup: bool = False, break_on_error=True) -> None:
142         """Step initialization.
143
144         Args:
145             cleanup(bool, optional): Determines if cleanup action should be called.
146             break_on_error(bool, optional): Determines if fail on execution should
147                 result with continuation of further steps
148
149         """
150         self._steps: List["BaseStep"] = []
151         self._cleanup: bool = cleanup
152         self._parent: "BaseStep" = None
153         self._reports_collection: ReportsCollection = None
154         self._start_execution_time: float = None
155         self._start_cleanup_time: float = None
156         self._execution_report: ReportStepStatus = None
157         self._cleanup_report: ReportStepStatus = None
158         self._executed: bool = False
159         self._cleaned_up: bool = False
160         self._state_execute: bool = False
161         self._state_clean: bool = False
162         self._nesting_level: int = 0
163         self._break_on_error: bool = break_on_error
164         self._substeps_executed: bool = False
165         self._is_validation_only = settings.IF_VALIDATION
166         self._is_force_cleanup = os.environ.get(IF_FORCE_CLEANUP) is not None
167
168     def add_step(self, step: "BaseStep") -> None:
169         """Add substep.
170
171         Add substep and mark step as a substep parent.
172
173         Args:
174             step (BaseStep): Step object
175         """
176         self._steps.append(step)
177         step._parent: "BaseStep" = self
178         step._update_nesting_level()
179
180     def _update_nesting_level(self) -> None:
181         """Update step nesting level.
182
183         Step nesting level allows to display relatino of steps during validation
184         """
185         self._nesting_level = 1 + self._parent._nesting_level
186         for step in self._steps:
187             step._update_nesting_level()
188
189     @property
190     def parent(self) -> "BaseStep":
191         """Step parent.
192
193         If parent is not set the step is a root one.
194         """
195         return self._parent
196
197     @property
198     def has_substeps(self) -> bool:
199         """Has step substeps.
200
201         If sdc has substeps.
202
203         Returns:
204             bool: True if step has substeps
205
206         """
207         return len(self._steps) > 0
208
209     @property
210     def is_executed(self) -> bool:
211         """Is step executed.
212
213         Step is executed if execute() method was completed without errors
214
215         Returns:
216             bool: True if step is executed, False otherwise
217
218         """
219         return self._executed
220
221     @property
222     def is_root(self) -> bool:
223         """Is a root step.
224
225         Step is a root if has no parent
226
227         Returns:
228             bool: True if step is a root step, False otherwise
229
230         """
231         return self._parent is None
232
233     @property
234     def reports_collection(self) -> ReportsCollection:
235         """Collection to store step reports.
236
237         Store there if step result is "PASS" or "FAIL"
238
239         Returns:
240             Queue: Thread safe collection to store reports
241
242         """
243         if not self.is_root:
244             return self.parent.reports_collection
245         if not self._reports_collection:
246             self._reports_collection = ReportsCollection(self._component_list())
247             for step_report in itertools.chain(self.execution_reports, self.cleanup_reports):
248                 self._reports_collection.put(step_report)
249         return self._reports_collection
250
251     @property
252     def execution_reports(self) -> Iterator[ReportsCollection]:
253         """Execution reports generator.
254
255         Steps tree postorder traversal
256
257         Yields:
258             Iterator[ReportsCollection]: Step execution report
259
260         """
261         for step in self._steps:
262             yield from step.execution_reports
263         if self._execution_report:
264             yield self._execution_report
265
266     @property
267     def cleanup_reports(self) -> Iterator[ReportsCollection]:
268         """Cleanup reports generator.
269
270         Steps tree preorder traversal
271
272         Yields:
273             Iterator[ReportsCollection]: Step cleanup report
274
275         """
276         if self._cleanup:
277             if self._cleanup_report:
278                 yield self._cleanup_report
279         for step in reversed(self._steps):
280             yield from step.cleanup_reports
281
282     @property
283     def name(self) -> str:
284         """Step name."""
285         return self.__class__.__name__
286
287     @property
288     @abstractmethod
289     def description(self) -> str:
290         """Step description.
291
292         Used for reports
293
294         Returns:
295             str: Step description
296
297         """
298
299     @property
300     @abstractmethod
301     def component(self) -> str:
302         """Component name.
303
304         Name of component which step is related with.
305             Most is the name of ONAP component.
306
307         Returns:
308             str: Component name
309
310         """
311
312     def _component_list(self, components: dict = None):
313         if not components:
314             components = {}
315         for step in self._steps:
316             components[step.component] = step.component
317             step._component_list(components)
318         if not self.is_root or not components:
319             components[self.component] = self.component
320         return list(components)
321
322     def _step_title(self, cleanup=False):
323         cleanup_label = " Cleanup:" if cleanup else ":"
324         return f"[{self.component}] {self.name}{cleanup_label} {self.description}"
325
326     def _log_execution_state(self, state: str, cleanup=False):
327         nesting_label = "" + "  " * self._nesting_level
328         description = f"| {state} {self._step_title(cleanup)} |"
329         self._logger.info(nesting_label + "*" * len(description))
330         self._logger.info(nesting_label + description)
331         self._logger.info(nesting_label + "*" * len(description))
332
333     def check_preconditions(self, cleanup=False) -> bool:
334         """Check preconditions.
335
336         Check if step preconditions are satisfied. If not, step is skipped
337         without further consequences. If yes, execution is initiated
338
339         Returns:
340             bool: True if preconditions are satisfied, False otherwise
341
342         """
343         return True
344
345     def _execute_substeps(self) -> None:
346         """Step's action execution.
347
348         Run all substeps action before it's own action.
349         Override this method and remember to call `super().execute()` before.
350
351         """
352         substep_error = False
353         for step in self._steps:
354             try:
355                 step.execute()
356             except (OnapTestException, SDKException) as substep_err:
357                 substep_error = True
358                 if step._break_on_error:
359                     raise SubstepExecutionException from substep_err
360                 self._logger.exception(substep_err)
361         if self._steps:
362             if substep_error and self._break_on_error:
363                 raise SubstepExecutionException("Cannot continue due to failed substeps")
364             self._log_execution_state("CONTINUE")
365         self._substeps_executed = True
366         self._start_execution_time = time.time()
367
368     def _cleanup_substeps(self) -> None:
369         """Substeps' cleanup.
370
371         Substeps are cleaned-up in reversed order.
372         We also try to cleanup steps if others failed
373
374         """
375         exceptions_to_raise = []
376         for step in reversed(self._steps):
377             try:
378                 if step._cleanup:
379                     step.cleanup()
380                 else:
381                     step._default_cleanup_handler()
382             except (OnapTestException, SDKException) as substep_err:
383                 try:
384                     raise SubstepExecutionException from substep_err
385                 except Exception as e:
386                     exceptions_to_raise.append(e)
387         if len(exceptions_to_raise) > 0:
388             if len(exceptions_to_raise) == 1:
389                 raise exceptions_to_raise[0]
390             raise SubstepExecutionExceptionGroup("Substep Exceptions", exceptions_to_raise)
391
392     def execute(self) -> None:
393         """Step's execute.
394
395         Must be implemented in the steps with store_state decorator
396
397         """
398
399     def cleanup(self) -> None:
400         """Step's cleanup.
401
402         Not all steps has to have cleanup method
403
404         """
405         # Step itself was cleaned-up, now time for children
406         if not self._cleanup:
407             # in this case we just make sure that store_state is run
408             self._default_cleanup_handler()
409
410     @StoreStateHandler.store_state(cleanup=True)
411     def _default_cleanup_handler(self):
412         pass
413
414     @classmethod
415     def set_proxy(cls, sock_http):
416         """Set sock proxy."""
417         onap_proxy = {}
418         onap_proxy['http'] = sock_http
419         onap_proxy['https'] = sock_http
420         Customer.set_proxy(onap_proxy)
421
422     def validate_step_implementation(self):
423         """Validate is step addes store_state decorators."""
424
425         if not getattr(self.execute, "_is_wrapped", False):
426             raise TestConfigurationException(
427                 f"{self._step_title()} - store_state decorator not present in execute() method")
428         if self._cleanup and not getattr(self.cleanup, "_is_wrapped", False):
429             raise TestConfigurationException(
430                 f"{self._step_title()} - store_state decorator not present in cleanup() method")
431         for step in self._steps:
432             step.validate_step_implementation()
433
434     def validate_execution(self):
435         """Validate if each step was executed by decorator."""
436
437         if self._is_validation_only:
438             self._log_execution_state(f"VALIDATE EXECUTION [{self._state_execute}]")
439             if not self._state_execute:
440                 raise TestConfigurationException(
441                     f"{self._step_title()} - Execute decorator was not called")
442             for step in self._steps:
443                 step.validate_execution()
444
445     def validate_cleanup(self):
446         """Validate if each step was cleaned by decorator."""
447
448         if self._is_validation_only:
449             for step in reversed(self._steps):
450                 step.validate_cleanup()
451             if self._cleanup:
452                 self._log_execution_state(
453                     f"VALIDATE CLEANUP [{self._state_clean}, {self._cleanup}]")
454                 if not self._state_clean:
455                     raise TestConfigurationException(
456                         f"{self._step_title()} - Cleanup decorator was not called")
457
458
459 class YamlTemplateBaseStep(BaseStep, ABC):
460     """Base YAML template step."""
461
462     def __init__(self, cleanup: bool):
463         """Initialize step."""
464
465         super().__init__(cleanup=cleanup)
466         self._service_instance: ServiceInstance = None
467         self._service_subscription: ServiceSubscription = None
468         self._customer: Customer = None
469
470     def _load_customer_and_subscription(self, reload: bool = False):
471         if self._customer is None:
472             self._customer: Customer = \
473                 Customer.get_by_global_customer_id(settings.GLOBAL_CUSTOMER_ID)
474         if self._service_subscription is None or reload:
475             self._service_subscription: ServiceSubscription = \
476                 self._customer.get_service_subscription_by_service_type(self.service_name)
477
478     def _load_service_instance(self):
479         if self._service_instance is None:
480             self._service_instance: ServiceInstance = \
481                 self._service_subscription.get_service_instance_by_name(self.service_instance_name)
482
483     @property
484     def service_name(self) -> str:
485         """Service name.
486
487         Get from YAML template if it's a root step, get from parent otherwise.
488
489         Returns:
490             str: Service name
491
492         """
493         if self.is_root:
494             return next(iter(self.yaml_template.keys()))
495         return self.parent.service_name
496
497     @property
498     def service_instance_name(self) -> str:
499         """Service instance name.
500
501         Generate service instance name.
502         If not applicable None is returned
503
504         Returns:
505             str: Service instance name
506
507         """
508         return None
509
510     @property
511     @abstractmethod
512     def yaml_template(self) -> dict:
513         """YAML template abstract property.
514
515         Every YAML template step need to implement that property.
516
517         Returns:
518             dict: YAML template
519
520         """
521
522     @property
523     @abstractmethod
524     def model_yaml_template(self) -> dict:
525         """Model YAML template abstract property.
526
527         Every YAML template step need to implement that property.
528
529         Returns:
530             dict: YAML template
531
532         """