7 from abc import ABC, abstractmethod
8 from typing import Iterator, List, Optional
10 from onapsdk.aai.business import Customer, ServiceInstance, ServiceSubscription
11 from onapsdk.configuration import settings
12 from onapsdk.exceptions import SDKException, SettingsError
14 from onaptests.steps.reports_collection import (Report, ReportsCollection,
16 from onaptests.utils.exceptions import (OnapTestException,
17 OnapTestExceptionGroup,
18 SubstepExecutionException,
19 SubstepExecutionExceptionGroup,
20 TestConfigurationException)
22 # pylint: disable=protected-access
23 IF_FORCE_CLEANUP = "PYTHON_SDK_TESTS_FORCE_CLEANUP"
26 class StoreStateHandler(ABC):
27 """Decorator for storing the state of executed test step."""
30 def store_state(cls, fun=None, *, cleanup=False): # noqa
32 return functools.partial(cls.store_state, cleanup=cleanup)
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))
39 self._state_clean = True
41 self._state_execute = True
42 initial_exception = None
44 execution_status: Optional[ReportStepStatus] = ReportStepStatus.FAIL
46 self._start_cleanup_time = time.time()
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
58 execution_status = ReportStepStatus.NOT_EXECUTED
59 except (OnapTestException, SDKException) as test_exc:
60 initial_exception = test_exc
62 self._log_execution_state(execution_status.name, cleanup)
63 self._cleanup_substeps()
65 new_exception = initial_exception
66 initial_exception = None
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
77 execution_status = ReportStepStatus.NOT_EXECUTED
78 except SubstepExecutionException as substep_exc:
80 execution_status = ReportStepStatus.NOT_EXECUTED
82 substep_exc = OnapTestExceptionGroup("Cleanup Exceptions",
83 [initial_exception, substep_exc])
85 except (OnapTestException, SDKException) as test_exc:
87 test_exc = OnapTestExceptionGroup("Cleanup Exceptions",
88 [initial_exception, test_exc])
92 self._log_execution_state(execution_status.name, 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
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",
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
115 wrapper._is_wrapped = True
119 class BaseStep(StoreStateHandler, ABC):
120 """Base step class."""
122 # Indicates that Step has no dedicated cleanup method
123 HAS_NO_CLEANUP = False
125 _logger: logging.Logger = logging.getLogger("")
127 def __init_subclass__(cls):
128 """Subclass initialization.
130 Add _logger property for any BaseStep subclass
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
137 cls.set_proxy(settings.SOCK_HTTP)
138 except SettingsError:
141 def __init__(self, cleanup: bool = False, break_on_error=True) -> None:
142 """Step initialization.
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
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
168 def add_step(self, step: "BaseStep") -> None:
171 Add substep and mark step as a substep parent.
174 step (BaseStep): Step object
176 self._steps.append(step)
177 step._parent: "BaseStep" = self
178 step._update_nesting_level()
180 def _update_nesting_level(self) -> None:
181 """Update step nesting level.
183 Step nesting level allows to display relatino of steps during validation
185 self._nesting_level = 1 + self._parent._nesting_level
186 for step in self._steps:
187 step._update_nesting_level()
190 def parent(self) -> "BaseStep":
193 If parent is not set the step is a root one.
198 def has_substeps(self) -> bool:
199 """Has step substeps.
204 bool: True if step has substeps
207 return len(self._steps) > 0
210 def is_executed(self) -> bool:
213 Step is executed if execute() method was completed without errors
216 bool: True if step is executed, False otherwise
219 return self._executed
222 def is_root(self) -> bool:
225 Step is a root if has no parent
228 bool: True if step is a root step, False otherwise
231 return self._parent is None
234 def reports_collection(self) -> ReportsCollection:
235 """Collection to store step reports.
237 Store there if step result is "PASS" or "FAIL"
240 Queue: Thread safe collection to store reports
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
252 def execution_reports(self) -> Iterator[ReportsCollection]:
253 """Execution reports generator.
255 Steps tree postorder traversal
258 Iterator[ReportsCollection]: Step execution report
261 for step in self._steps:
262 yield from step.execution_reports
263 if self._execution_report:
264 yield self._execution_report
267 def cleanup_reports(self) -> Iterator[ReportsCollection]:
268 """Cleanup reports generator.
270 Steps tree preorder traversal
273 Iterator[ReportsCollection]: Step cleanup report
277 if self._cleanup_report:
278 yield self._cleanup_report
279 for step in reversed(self._steps):
280 yield from step.cleanup_reports
283 def name(self) -> str:
285 return self.__class__.__name__
289 def description(self) -> str:
295 str: Step description
301 def component(self) -> str:
304 Name of component which step is related with.
305 Most is the name of ONAP component.
312 def _component_list(self, components: dict = None):
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)
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}"
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))
333 def check_preconditions(self, cleanup=False) -> bool:
334 """Check preconditions.
336 Check if step preconditions are satisfied. If not, step is skipped
337 without further consequences. If yes, execution is initiated
340 bool: True if preconditions are satisfied, False otherwise
345 def _execute_substeps(self) -> None:
346 """Step's action execution.
348 Run all substeps action before it's own action.
349 Override this method and remember to call `super().execute()` before.
352 substep_error = False
353 for step in self._steps:
356 except (OnapTestException, SDKException) as substep_err:
358 if step._break_on_error:
359 raise SubstepExecutionException from substep_err
360 self._logger.exception(substep_err)
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()
368 def _cleanup_substeps(self) -> None:
369 """Substeps' cleanup.
371 Substeps are cleaned-up in reversed order.
372 We also try to cleanup steps if others failed
375 exceptions_to_raise = []
376 for step in reversed(self._steps):
381 step._default_cleanup_handler()
382 except (OnapTestException, SDKException) as substep_err:
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)
392 def execute(self) -> None:
395 Must be implemented in the steps with store_state decorator
399 def cleanup(self) -> None:
402 Not all steps has to have cleanup method
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()
410 @StoreStateHandler.store_state(cleanup=True)
411 def _default_cleanup_handler(self):
415 def set_proxy(cls, sock_http):
416 """Set sock proxy."""
418 onap_proxy['http'] = sock_http
419 onap_proxy['https'] = sock_http
420 Customer.set_proxy(onap_proxy)
422 def validate_step_implementation(self):
423 """Validate is step addes store_state decorators."""
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()
434 def validate_execution(self):
435 """Validate if each step was executed by decorator."""
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()
445 def validate_cleanup(self):
446 """Validate if each step was cleaned by decorator."""
448 if self._is_validation_only:
449 for step in reversed(self._steps):
450 step.validate_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")
459 class YamlTemplateBaseStep(BaseStep, ABC):
460 """Base YAML template step."""
462 def __init__(self, cleanup: bool):
463 """Initialize step."""
465 super().__init__(cleanup=cleanup)
466 self._service_instance: ServiceInstance = None
467 self._service_subscription: ServiceSubscription = None
468 self._customer: Customer = None
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)
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)
484 def service_name(self) -> str:
487 Get from YAML template if it's a root step, get from parent otherwise.
494 return next(iter(self.yaml_template.keys()))
495 return self.parent.service_name
498 def service_instance_name(self) -> str:
499 """Service instance name.
501 Generate service instance name.
502 If not applicable None is returned
505 str: Service instance name
512 def yaml_template(self) -> dict:
513 """YAML template abstract property.
515 Every YAML template step need to implement that property.
524 def model_yaml_template(self) -> dict:
525 """Model YAML template abstract property.
527 Every YAML template step need to implement that property.