Skip to content
Snippets Groups Projects
data_model.py 80.34 KiB
"""
Data model of a test specification.
A instance of a test case consists out of

    * test case information
    * the steps

The numeration of the steps consists out ouf a primary counter and a secondary counter, separated by a dot (e.g. 2.1).
Although this approach may look like a grouping, it is not intended to be. It is more a history preservation if the test
altered after an official delivery.

Features:

    * adding a step

        * adding a step above (:py:meth:`~test_specification.TestSequence.add_step_above`)
        * adding a step below (:py:meth:`~test_specification.TestSequence.add_step_below`)

    * removing a step (:py:meth:`~test_specification.TestSequence.remove_step`)
    * renumber the steps (decrease_step_numbers, increase_step_numbers)

        * :py:func:`~test_specification.TestSequence.decrease_step_numbers`
        * :py:func:`~test_specification.TestSequence.increase_step_numbers`

    * Lock-mechanism for the numeration of the steps.

        * If a test is locked:

            * the primary counter of a step is "frozen", new steps get numerated using the secondary counter
            * the behaviour of adding a step is changed
            * deleting a step is not possible anymore

        * if a test is unlocked:

            * re-numeration for all steps should be possible only manually

    * generating a JSON string out of the data (encode_to_json, serialize)

        * :py:func:`~test_specification.TestSequence.encode_to_json`
        * :py:func:`~test_specification.TestSequence.serialize`

    * creating a instance from a JSON string (decode_to_json)

        * :py:func:`~test_specification.TestSequence.decode_to_json`

Parallel processes:
A test can have parallel processes. To mark if a step is has the attribute 'sequence'. The different sequences are
numbered, starting at 0. To start a sequence a step should be made and a function should be called.
"""
import json
import copy
import logging
import os
import gettext
import unittest
import confignator
import toolbox

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.WARNING)
console_hdlr = toolbox.create_console_handler(hdlr_lvl=logging.WARNING)
logger.addHandler(hdlr=console_hdlr)

# using gettext for internationalization (i18n)
localedir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'locale')
translate = gettext.translation('handroll', localedir, fallback=True)
_ = translate.gettext


def create_step_number(primary_counter: int, secondary_counter: int) -> str:
    """
    Using integers to build a string consisting of primary counter and secondary counter, which are separated by a dot.

    :param int primary_counter: the primary counter of the step
    :param int secondary_counter: the secondary counter of the step
    :return: the step number as a string (<primary_counter>.<secondary_counter>
    :rtype: str
    """
    assert isinstance(primary_counter, int)
    assert isinstance(secondary_counter, int)
    step_number_string = str(primary_counter) + '.' + str(secondary_counter)
    return step_number_string


def parse_step_number(step_number: (str, int, float)):
    """
    Parses the input value into a tuple of primary counter and secondary counter.

    :param str, int, float step_number: the value which should be a step number
    :return: a tuple consisting out of the primary counter and secondary counter of a step
    :rtype: (int, int)
    """
    assert isinstance(step_number, (str, int, float))
    step_number_string = str(step_number)
    separator = step_number_string.find('.')
    if separator != -1:  # found the separator in the string
        primary_counter = int(step_number_string[:separator])
        secondary_counter = int(step_number_string[separator+1:])
    else:  # did not find the separator in the string
        primary_counter = int(step_number_string)
        secondary_counter = 0

    return primary_counter, secondary_counter


class Step:
    def __init__(self, test_seq=None, step_num=None, prim_counter=None, seco_counter=None, step=None, logger=logger):
        self.logger = logger
        # self._sequence = 0
        self._primary_counter = 0
        self._secondary_counter = 0
        self._step_number = ''
        self._description = ''
        self._command_code = ''
        self._step_comment = ''
        self._verification_code = ''
        self._verification_description = ''
        self._is_active = True
        self._verified_item = []
        self._start_sequence = None
        self._stop_sequence = None
        self.test = test_seq

        # set the step number
        if step_num is not None:
            self.primary_counter = parse_step_number(step_num)[0]
            self.secondary_counter = parse_step_number(step_num)[1]
        if step_num is None and prim_counter is not None:
            self.primary_counter = prim_counter
            if seco_counter is not None:
                self.secondary_counter = seco_counter
            else:
                self.secondary_counter = 0

        # if a json is provided, read the step attributes from it
        if step is not None:
            self.set_attributes_from_json(step)

    def __deepcopy__(self, memodict={}):
        new_step = Step()
        # new_step.sequence = copy.copy(self.sequence)
        new_step.primary_counter = copy.copy(self.primary_counter)
        new_step.secondary_counter = copy.copy(self.secondary_counter)
        new_step.description = copy.copy(self.description)
        new_step.command_code = copy.copy(self.command_code)
        new_step.step_comment = copy.copy(self.step_comment)
        new_step.verification_code = copy.copy(self.verification_code)
        new_step.verification_description = copy.copy(self.verification_description)
        new_step.is_active = copy.copy(self.is_active)
        new_step.start_sequence = copy.copy(self.start_sequence)
        new_step.stop_sequence = copy.copy(self.stop_sequence)
        # new_step._verified_item = copy.copy(self._verified_item)
        return new_step

    # @property
    # def sequence(self):
    #     return self._sequence
    #
    # @sequence.setter
    # def sequence(self, value: int):
    #     assert isinstance(value, int)
    #     self._sequence = value

    @property
    def primary_counter(self):
        return self._primary_counter

    @primary_counter.setter
    def primary_counter(self, value: int):
        assert isinstance(value, int)
        self._primary_counter = value
        self._step_number = str(self.primary_counter) + '.' + str(self.secondary_counter)

    @property
    def secondary_counter(self):
        return self._secondary_counter

    @secondary_counter.setter
    def secondary_counter(self, value: int):
        assert isinstance(value, int)
        self._secondary_counter = value
        self._step_number = str(self.primary_counter) + '.' + str(self.secondary_counter)

    # Do not use if a test file should be generated, Output is always '1.0' and not '1_0'
    # A Point is not supported in a function/method name in python, use 'step_number_test_format' function instead
    @property
    def step_number(self):
        return self._step_number

    @step_number.setter
    def step_number(self, value: str):
        assert isinstance(value, (str, int))
        if isinstance(value, str):
            # verify, that the format of primary.secondary (e.g.: 1.1, 1.2, ...) is fulfilled
            primary, secondary = parse_step_number(value)
            self._step_number = value
        if isinstance(value, int):
            self._step_number = str(value)

    @property
    def description(self):
        return self._description

    @description.setter
    def description(self, value: str):
        assert isinstance(value, str)
        self._description = value

    @property
    def command_code(self):
        return self._command_code

    @command_code.setter
    def command_code(self, value: str):
        assert isinstance(value, str)
        self._command_code = value

    @property
    def step_comment(self):
        return self._step_comment

    @step_comment.setter
    def step_comment(self, value: str):
        assert isinstance(value, str)
        self._step_comment = value

    @property
    def verification_code(self):
        return self._verification_code

    @verification_code.setter
    def verification_code(self, value: str):
        assert isinstance(value, str)
        self._verification_code = value

    @property
    def verification_description(self):
        return self._verification_description

    @verification_description.setter
    def verification_description(self, value: str):
        assert isinstance(value, str)
        self._verification_description = value

    @property
    def is_active(self):
        return self._is_active

    @is_active.setter
    def is_active(self, value: bool):
        assert isinstance(value, bool)
        self._is_active = value

    @property
    def start_sequence(self):
        return self._start_sequence

    @start_sequence.setter
    def start_sequence(self, value: int):
        assert isinstance(value, int) or value is None
        self._start_sequence = value

    @property
    def stop_sequence(self):
        return self._stop_sequence

    @stop_sequence.setter
    def stop_sequence(self, value: int):
        assert isinstance(value, int) or value is None
        self._stop_sequence = value

    @property
    def step_number_test_format(self):
        primary = parse_step_number(self._step_number)[0]
        secondary = parse_step_number(self._step_number)[1]
        return str(primary) + '_' + str(secondary)

    def set_attributes_from_json(self, step):
        """
        Loading the step attributes from a parsed JSON file.

        :param: dict step: a dictionary parsed from a JSON file containing the information of a step
        """
        try:
            # self.sequence = step['_sequence']
            self.primary_counter = step['_primary_counter']
            self.secondary_counter = step['_secondary_counter']
            self.description = step['_description']
            self.command_code = step['_command_code']
            self.step_comment = step['_step_comment']
            self.verification_code = step['_verification_code']
            self.verification_description = step['_verification_description']
            self.is_active = step['_is_active']
        except KeyError as error:
            self.logger.error('KeyError: no {} could be found in the loaded data'.format(error))
        # load optional attributes
        try:
            self.start_sequence = step['_start_sequence']
            self.stop_sequence = step['_stop_sequence']
        except KeyError:
            pass
        return

    def increase_primary_counter(self):
        current_step_number = self.step_number
        self.primary_counter = self.primary_counter + 1
        self.logger.debug('increased step number: {} -> {}'.format(current_step_number, self.step_number))
        return

    def decrease_primary_counter(self):
        current_step_number = self.step_number
        self.primary_counter = self.primary_counter - 1
        self.logger.debug('decreased step number: {} -> {}'.format(current_step_number, self.step_number))
        return

    def increase_secondary_counter(self):
        current_step_number = self.step_number
        self.secondary_counter = self.secondary_counter + 1
        own_list_index = self.test.get_step_index(self.step_number)
        self.logger.debug('increased step number: {} -> {} (index: {})'.format(current_step_number, self.step_number, own_list_index))
        return

    def decrease_secondary_counter(self):
        current_step_number = self.step_number
        self.secondary_counter = self.secondary_counter - 1
        self.logger.debug('decreased step number: {} -> {}'.format(current_step_number, self.step_number))
        return


class TestSequence:
    def __init__(self, sequence=0, json_data=None, logger=logger):
        """
        Creates a new instance of TestSequence. If a JSON object was provided it will be decoded and used otherwise
        an empty instance is created.
        For every attribute there is a function to set its value.
        :param json_data: the JSON object which is used to build the test specification
        """
        self.logger = logger
        self._sequence = sequence
        self._name = ''
        self._description = ''
        self._spec_version = ''
        self._primary_counter_locked = False
        self.steps = []

        if json_data is not None:
            # load from a JSON and then sort the steps by their step number and verify the numbering
            self.decode_from_json(json_data)
            self.resort_step_list_by_step_numbers()
            self.verify_step_list_consistency()

    def __deepcopy__(self):
        new_test_seq = TestSequence()
        new_test_seq.sequence = copy.copy(self.sequence)
        new_test_seq.name = copy.copy(self.name)
        new_test_seq.description = copy.copy(self.description)
        new_test_seq.spec_version = copy.copy(self.spec_version)
        new_test_seq.primary_counter_locked = copy.copy(self.primary_counter_locked)
        new_test_seq.steps = copy.deepcopy(self.steps)

        return new_test_seq

    @property
    def primary_counter_locked(self):
        return self._primary_counter_locked

    @primary_counter_locked.setter
    def primary_counter_locked(self, value: bool):
        assert isinstance(value, bool)
        self._primary_counter_locked = value

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value: str):
        assert isinstance(value, str)
        self._name = value

    @property
    def sequence(self):
        return self._sequence

    @sequence.setter
    def sequence(self, value: int):
        assert isinstance(value, int)
        self._sequence = value

    @property
    def description(self):
        return self._description

    @description.setter
    def description(self, value: str):
        assert isinstance(value, str)
        self._description = value

    @property
    def spec_version(self):
        return self._spec_version

    @spec_version.setter
    def spec_version(self, value: str):
        assert isinstance(value, str)
        self._spec_version = value

    def verify_step_list_consistency(self) -> bool:
        """
        Testing the step list for integrity. The step numbers have to increase steadily without gaps.

        :return: If the step list is consistent, True is returned
        :rtype: bool
        """
        try:
            for idx, stp in enumerate(self.steps):
                if idx == 0:
                    # the first item in the list has to be the step 1.0
                    assert(stp.primary_counter == 1)
                    assert(stp.secondary_counter == 0)
                else:
                    last_stp = self.steps[idx - 1]
                    # verify that the step numbers are steadily increasing and are uninterrupted
                    if last_stp.primary_counter == stp.primary_counter:
                        # the secondary counter is greater than the last one
                        assert(stp.secondary_counter > last_stp.secondary_counter)
                        # the difference is 1
                        assert(stp.secondary_counter - last_stp.secondary_counter == 1)
                    else:
                        # the primary counter is greater than the last one
                        assert(stp.primary_counter > last_stp.primary_counter)
                        # the difference is 1
                        assert(stp.primary_counter - last_stp.primary_counter == 1)
            return True
        except Exception as e:
            self.logger.exception(e)
            return False

    def resort_step_list_by_step_numbers(self):
        """
        The list of the steps is sorted by the step number.
        """
        # sort by secondary counter
        s = sorted(self.steps, key=lambda step: step.secondary_counter)
        # sort by primary counter
        p = sorted(s, key=lambda step: step.primary_counter)
        # assign the sorted step list
        self.steps = p

    def highest_step_number(self) -> str:
        """
        Returns the step number with the highest step number. This is achieved by first sorting the step list, then
        return the last element in the list
        :return: step number of the last step
        :rtype: str
        """
        self.resort_step_list_by_step_numbers()
        return self.steps[-1].step_number

    def log_list_of_steps(self):
        self.logger.debug('--------------------------------------------')
        self.logger.debug('List of steps:')
        if len(self.steps) > 0:
            for index, step in enumerate(self.steps):
                self.logger.debug('Step {},{} (index: {}, id:{})'.format(step.primary_counter, step.secondary_counter, index, id(step)))
        else:
            self.logger.debug('list of steps is empty')
        self.logger.debug('--------------------------------------------')

    def get_step_index(self, step_number: (str, int, float)):
        """
        Retrieves, for a given step_number, the index of the step in the step list.
        Raises LookupError if the step_number has no matching step in the list.

        :param str, int, float step_number: number of the desired step
        :return: index of the desired step
        :rtype: int
        """
        assert isinstance(step_number, (str, int, float))
        # find the index within the list of the step
        index_ref_step = None
        primary, secondary = parse_step_number(step_number)
        for ndx, step in enumerate(self.steps):
            if step.primary_counter == primary:
                if step.secondary_counter == secondary:
                    index_ref_step = ndx
        if index_ref_step is None:
            raise LookupError
        else:
            return index_ref_step

    def get_step(self, step_number: (str, int, float)):
        """
        Retrieves, for a given step_number, the step object

        :param str, int, float step_number: number of the desired step
        :return: The step instance.
        :rtype: data_model.Step
        """
        step_index = self.get_step_index(step_number)
        step = self.steps[step_index]
        return step

    def add_step_above(self, reference_step_position):
        """
        Determines the step number of the step to add.  then adds a step above the step provided as argument
        :param reference_step_position: the step which is the reference for adding a new step above
        :return:
        """
        # figure out the primary and secondary counter of the new step
        try:
            # get index of the reference step
            index_ref_step = self.get_step_index(reference_step_position)
            ref_step = self.steps[index_ref_step]
            index_new_step = index_ref_step
            try:
                # get the step before the reference step
                before_step = self.steps[index_ref_step - 1]
            except IndexError:
                before_step = None

            # determine the primary and secondary counter of the new step
            if before_step is None or (before_step.secondary_counter == 0 and not self.primary_counter_locked):
                new_step_primary = ref_step.primary_counter
                new_step_secondary = 0
            else:
                new_step_primary = before_step.primary_counter
                new_step_secondary = before_step.secondary_counter + 1
        except LookupError:
            if len(self.steps) == 0:  # edge-case: the step list is empty
                index_new_step = 0
                new_step_primary = 1
                new_step_secondary = 0
            else:
                self.logger.error('step {} could not be found, thus no step is added below'.format(reference_step_position))
                return

        # create the new step
        number_new_step = create_step_number(new_step_primary, new_step_secondary)
        new_step = Step(step_num=number_new_step, test_seq=self)

        # add the new step to the list - the new step takes the place below the reference step

        self.steps.insert(index_new_step, new_step)
        self.logger.debug(
            'added Step {}.{} (index: {}, id:{})'.format(new_step.primary_counter, new_step.secondary_counter, index_new_step, id(new_step)))

        # increase the step numbers of the following steps
        for step in self.steps[index_new_step + 1:]:
            if step.primary_counter == new_step.primary_counter and new_step.secondary_counter > 0:
                step.increase_secondary_counter()
            elif not self.primary_counter_locked:
                step.increase_primary_counter()

        return new_step

    def add_step_below(self, reference_step_position=None, step_instance=None):
        # figure out the primary and secondary counter of the new step
        if reference_step_position is None and len(self.steps) == 0:
            # edge-case: the step list is empty
            index_new_step = 0
            new_step_primary = 1
            new_step_secondary = 0
        else:
            # the step list is not empty, thus find the reference step and the following
            if reference_step_position is None:   # append it
                index_ref_step = -1
            else:
                try:
                    # get the reference step
                    index_ref_step = self.get_step_index(reference_step_position)
                except LookupError:
                    # Although the list of steps is not empty, no reference step could be found (for whatever reason).
                    raise Exception
            ref_step = self.steps[index_ref_step]
            index_ref_step = self.get_step_index(ref_step.step_number)  # if the index was -1, get the actual index
            index_new_step = index_ref_step + 1

            # get the next step
            if index_ref_step is not None:
                try:
                    # get the following step of reference step
                    next_step = self.steps[index_ref_step + 1]
                except IndexError:
                    next_step = None
            else:
                next_step = None

            # determine the primary and secondary counter of the new step
            if next_step is None or (next_step.secondary_counter == 0 and not self.primary_counter_locked):
                new_step_primary = ref_step.primary_counter + 1
                new_step_secondary = 0
            else:
                new_step_primary = ref_step.primary_counter
                new_step_secondary = ref_step.secondary_counter + 1

        # create the new step
        number_new_step = create_step_number(new_step_primary, new_step_secondary)
        if step_instance is None:
            # make a new Step
            new_step = Step(step_num=number_new_step, test_seq=self)
        else:
            # use the provided Step
            assert isinstance(step_instance, Step)
            new_step = step_instance
            new_step.primary_counter = new_step_primary
            new_step.secondary_counter = new_step_secondary

        # add the new step to the list - the new step takes the place below the reference step
        self.steps.insert(index_new_step, new_step)
        self.logger.debug('added Step {}.{} (index: {}, id:{})'.format(new_step.primary_counter, new_step.secondary_counter, index_new_step, id(new_step)))

        # increase the step numbers of the following steps
        for step in self.steps[index_new_step + 1:]:
            if step.primary_counter == new_step.primary_counter and new_step.secondary_counter > 0:
                step.increase_secondary_counter()
            elif not self.primary_counter_locked:
                step.increase_primary_counter()

        return new_step

    def remove_step(self, step_number: (str, int, float)):
        """
        Deletes a step out of the dictionary for steps. If the step is found, decrease all the following step numbers,
        then delete the step.
        :param str, int, float step_number: The step which will be deleted
        """
        try:
            # get index within the step list of the step
            index_of_step = self.get_step_index(step_number)
        except LookupError:
            self.logger.error('step {} could not be found, thus no step is added below'.format(step_number))
            return

        # decrease step numbers if step has following steps
        self.decrease_step_numbers(reference_step_number=step_number)

        # delete the step in the list
        self.steps.pop(index_of_step)
        self.logger.debug('deleted Step {} (index: {})'.format(step_number, index_of_step))

        return

    def move_step(self, step_to_move_index: int, desired_position_index: int):
        """
        Moving a step within the data model. A step can be moved upward or downward.
        Cases:

        * step_to_move < desired_position


        * step_to_move == desired_position

            * do nothing

        * step_to_move > desired_position

        :param int step_to_move_index: index in the list of the step which should be moved
        :param int desired_position_index: index in the list of the moved step after the move
        """
        assert isinstance(step_to_move_index, int)
        assert isinstance(desired_position_index, int)
        if desired_position_index == -1:
            # prevent unexpected behavior if the desired position is before the first item (which has index 0)
            desired_position_index = 0
        self.logger.debug('list of steps before moving:')
        self.log_list_of_steps()

        if step_to_move_index < desired_position_index:  # move the step downwards
            # get the step, which should be moved
            step_to_move = self.steps[step_to_move_index]
            assert isinstance(step_to_move, Step)
            # decrease the step counters, since the step gets removed
            self.decrease_step_numbers(reference_step_index=step_to_move_index)
            # delete the step from the list
            logger.debug('delete the step which is moved from the list')
            self.steps.pop(step_to_move_index)

            # figure out the which step number to reassign to the step (it was moved, thus it gets a new step number)
            stored_step_number = step_to_move.step_number
            if desired_position_index > 0:
                try:
                    step_before = self.steps[desired_position_index-1]
                except IndexError:
                    step_before = self.steps[-1]
                assert isinstance(step_before, Step)
                if self.primary_counter_locked:
                    step_to_move.primary_counter = step_before.primary_counter
                    step_to_move.secondary_counter = step_before.secondary_counter + 1
                else:
                    step_to_move.primary_counter = step_before.primary_counter + 1
                    step_to_move.secondary_counter = 0
            else:
                # it is moved to the top position
                step_to_move.primary_counter = 1
                step_to_move.secondary_counter = 0
            logger.debug('changed step number: {} -> {}'.format(stored_step_number, step_to_move.step_number))
            # insert it again at the desired position (-1, because the indices got decreased when the step was deleted)
            logger.debug('insert the moved step again')
            self.steps.insert(desired_position_index, step_to_move)
            # increase the step numbers, since a step was added
            self.increase_step_numbers(reference_step_index=desired_position_index-1)

        elif step_to_move_index == desired_position_index:
            pass
        elif step_to_move_index > desired_position_index:  # move the step upwards
            # get the step, which should be moved
            step_to_move = self.steps[step_to_move_index]
            assert isinstance(step_to_move, Step)
            # decrease the step counters, since the step is going to be removed
            self.decrease_step_numbers(reference_step_index=step_to_move_index)
            # delete the step from the list
            logger.debug('delete the step which is moved from the list')
            self.steps.pop(step_to_move_index)

            # figure out the which step number to reassign to the step (it was moved, thus it gets a new step number)
            stored_step_number = step_to_move.step_number
            if desired_position_index > 0:
                try:
                    step_before = self.steps[desired_position_index - 1]
                except IndexError:
                    step_before = self.steps[-1]
                assert isinstance(step_before, Step)
                if self.primary_counter_locked:
                    step_to_move.primary_counter = step_before.primary_counter
                    step_to_move.secondary_counter = step_before.secondary_counter + 1
                else:
                    step_to_move.primary_counter = step_before.primary_counter + 1
                    step_to_move.secondary_counter = 0
            else:
                # it is moved to the top position
                step_to_move.primary_counter = 1
                step_to_move.secondary_counter = 0
            logger.debug('changed step number: {} -> {}'.format(stored_step_number, step_to_move.step_number))
            # insert it again at the desired position
            logger.debug('insert the moved step again')
            self.steps.insert(desired_position_index, step_to_move)
            # increase the step numbers, since a step was added
            self.increase_step_numbers(reference_step_index=desired_position_index)

        self.logger.debug('list of steps after moving:')
        self.log_list_of_steps()
        self.verify_step_list_consistency()

    def renumber_steps(self):
        """
        Renumbering all steps. The steps are numerated by only using the primary counter.
        All secondary counters will be set to 0.
        """
        for index, step in enumerate(self.steps):
            if index == 0:
                step.primary_counter = 1
                step.secondary_counter = 0
            else:
                last_step = self.steps[index-1]
                step.primary_counter = last_step.primary_counter + 1
                step.secondary_counter = 0
        return

    def decrease_step_numbers(self, reference_step_number: (str, int, float) = '', reference_step_index: int = None):
        """
        Decreases the step numbers of all steps following the reference step, excluding it.
        This function is used to reassign the step numbers after
        * a step was deleted from the data model
        * a step was moved within the data model

        :param (str, int, float) reference_step_number: the number of the step to start the decreasing (EXCLUDING)
        :param int reference_step_index: the index within the step list of the reference step
        """
        # get the index
        if reference_step_index is None:
            try:
                index_ref_step = self.get_step_index(reference_step_number)
            except LookupError:
                index_ref_step = None
                self.logger.error('could not find the reference step, thus no step numbers where increased')
            except ValueError:
                index_ref_step = None
                self.logger.error('{} is not a valid step number'.format(reference_step_number))
        else:
            index_ref_step = reference_step_index
        # get the step
        try:
            ref_step = self.steps[index_ref_step]
            assert isinstance(ref_step, Step)
        except IndexError or TypeError:
            ref_step = None
            self.logger.error('could not find the reference step, thus no step numbers where increased')
        # decrease the step numbers of the following steps
        for step in self.steps[index_ref_step + 1:]:
            if ref_step.secondary_counter == 0:
                # decrease all following primary counter
                step.decrease_primary_counter()
            else:
                # decrease all following secondary counter for steps,
                # which have the same primary counter as the reference step
                if step.primary_counter == ref_step.primary_counter:
                    step.decrease_secondary_counter()
        return

    def increase_step_numbers(self, reference_step_number: (str, int, float) = '', reference_step_index: int = None):
        """
        Increases the step numbers of all steps following the reference step, excluding it.
        This function is used to reassign the step numbers after
        * a step was added to the data model
        * a step was moved within the data model

        :param (str, int, float) reference_step_number: the number of the step to start the increasing (EXCLUDING)
        :param int reference_step_index: the index within the step list of the reference step
        """
        # get the index
        if reference_step_index is None:
            try:
                index_ref_step = self.get_step_index(reference_step_number)
            except LookupError:
                index_ref_step = None
                self.logger.error('could not find the reference step, thus no step numbers where increased')
            except ValueError:
                index_ref_step = None
                self.logger.error('{} is not a valid step number'.format(reference_step_number))
        else:
            index_ref_step = reference_step_index
        # get the step
        try:
            ref_step = self.steps[index_ref_step]
            assert isinstance(ref_step, Step)
        except IndexError or TypeError:
            ref_step = None
            self.logger.error('could not find the reference step, thus no step numbers where increased')
        # increase the step numbers of the following steps
        if ref_step is not None:
            last_step = ref_step
            for index, step in enumerate(self.steps[index_ref_step + 1:]):
                if step.primary_counter == last_step.primary_counter:
                    if step.secondary_counter != 0 and step.primary_counter == ref_step.primary_counter:
                        step.increase_secondary_counter()
                    if step.secondary_counter == 0:
                        step.increase_primary_counter()
                elif step.secondary_counter != 0:
                    step.increase_primary_counter()
                last_step = step
        return

    def verified_items(self):
        """
        Returns a list of all verified items. The verified items are stored in each step.
        """
        # ToDo
        return

    def encode_to_json(self, *args):
        """
        Makes out of the TestSequence a JSON object.
        """
        json_string = None
        json_string = json.dumps(self, default=self.serialize)
        return json_string

    def decode_from_json(self, json_data: dict):
        """
        Create a TestSequence instance from a JSON object.
        """
        try:
            self.name = json_data['_name']
            self.description = json_data['_description']
            self.spec_version = json_data['_spec_version']
            self.sequence = json_data['_sequence']
        except KeyError as keyerror:
            self.logger.error('KeyError: no {} could be found in the loaded data'.format(keyerror))

        self.steps = []
        try:
            for index, step in enumerate(json_data['steps']):
                self.steps.append(Step(test_seq=self, step=step))
        except KeyError as keyerror:
            self.logger.error('KeyError: no {} could be found in the loaded data'.format(keyerror))
        return

    @staticmethod
    def serialize(obj):
        """
        JSON serializer for objects which are not serializable by default.
        Uses the overwritten __deepcopy__ methods of the classes, to make copies. Then takes the dictionarys of them
        and deletes the objects, which can not be serialized.
        """
        if isinstance(obj, TestSequence):
            obj_copy = obj.__deepcopy__()
            obj_dict = obj_copy.__dict__
            del obj_dict['logger']
            return obj_dict
        if isinstance(obj, Step):
            obj_copy = obj.__deepcopy__()
            obj_dict = obj_copy.__dict__
            del obj_dict['test']
            del obj_dict['logger']
            return obj_dict

class TestSpecification:
    def __init__(self, json_data=None, logger=logger):
        self.logger = logger
        self._name = ''
        self._description = ''
        self._spec_version = ''
        self._iasw_version = ''
        self._primary_counter_locked = False
        self._precon_name = ''
        self._precon_code = ''
        self._precon_descr = ''
        self._postcon_name = ''
        self._postcon_code = ''
        self._postcon_descr = ''
        self._comment = ''
        self._custom_imports = ''
        self.sequences = []

        if json_data is not None:
            # load from a JSON and then sort the steps by their step number and verify the numbering
            self.decode_from_json(json_data)

    def __deepcopy__(self):
        new_testspec = TestSpecification()
        new_testspec.sequences = copy.copy(self.sequences)
        new_testspec.name = copy.copy(self.name)
        new_testspec.description = copy.copy(self.description)
        new_testspec.spec_version = copy.copy(self.spec_version)
        new_testspec.iasw_version = copy.copy(self.iasw_version)
        new_testspec.primary_counter_locked = copy.copy(self.primary_counter_locked)
        new_testspec.precon_name = copy.copy(self.precon_name)
        new_testspec.precon_code = copy.copy(self.precon_code)
        new_testspec.precon_descr = copy.copy(self.precon_descr)
        new_testspec.postcon_name = copy.copy(self.postcon_name)
        new_testspec.postcon_code = copy.copy(self.postcon_code)
        new_testspec.postcon_descr = copy.copy(self.postcon_descr)
        new_testspec.comment = copy.copy(self.comment)
        new_testspec.custom_imports = copy.copy(self.custom_imports)

        return new_testspec

    @property
    def primary_counter_locked(self):
        return self._primary_counter_locked

    @primary_counter_locked.setter
    def primary_counter_locked(self, value: bool):
        assert isinstance(value, bool)
        self._primary_counter_locked = value
        # set the primary_counter_locked for all sequences
        for seq in self.sequences:
            assert isinstance(seq, TestSequence)
            seq.primary_counter_locked = value

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value: str):
        assert isinstance(value, str)
        self._name = value

    @property
    def description(self):
        return self._description

    @description.setter
    def description(self, value: str):
        assert isinstance(value, str)
        self._description = value

    @property
    def spec_version(self):
        return self._spec_version

    @spec_version.setter
    def spec_version(self, value: str):
        assert isinstance(value, str)
        self._spec_version = value

    @property
    def iasw_version(self):
        return self._iasw_version

    @iasw_version.setter
    def iasw_version(self, value: str):
        assert isinstance(value, str)
        self._iasw_version = value

    @property
    def precon_name(self):
        return self._precon_name

    @precon_name.setter
    def precon_name(self, value: str):
        if value == None:
            value = "None"
        else:
            pass
        assert isinstance(value, str)
        self._precon_name = value

    @property
    def precon_code(self):
        return self._precon_code

    @precon_code.setter
    def precon_code(self, value: str):
        assert isinstance(value, str)
        self._precon_code = value

    @property
    def precon_descr(self):
        return self._precon_descr

    @precon_descr.setter
    def precon_descr(self, value: str):
        assert isinstance(value, str)
        self._precon_descr = value

    @property
    def postcon_name(self):
        return self._postcon_name

    @postcon_name.setter
    def postcon_name(self, value: str):
        if value == None:
            value = "None"
        else:
            pass
        assert isinstance(value, str)
        self._postcon_name = value

    @property
    def postcon_code(self):
        return self._postcon_code

    @postcon_code.setter
    def postcon_code(self, value: str):
        assert isinstance(value, str)
        self._postcon_code = value

    @property
    def postcon_descr(self):
        return self._postcon_descr

    @postcon_descr.setter
    def postcon_descr(self, value: str):
        assert isinstance(value, str)
        self._postcon_descr = value

    @property
    def comment(self):
        return self._comment

    @comment.setter
    def comment(self, value: str):
        assert isinstance(value, str)
        self._comment = value

    @property
    def custom_imports(self):
        return self._custom_imports

    @custom_imports.setter
    def custom_imports(self, value: str):
        assert isinstance(value, str)
        self._custom_imports = value

    def encode_to_json(self, *args):
        """
        Makes out of the TestSequence a JSON object.
        """
        json_string = None
        json_string = json.dumps(self, default=self.serialize)
        return json_string

    def decode_from_json(self, json_data: dict):
        """
        Create a TestSpecification instance from a JSON object.
        """
        try:
            self.name = json_data['_name']
            self.description = json_data['_description']
            self.spec_version = json_data['_spec_version']
            self.iasw_version = json_data['_iasw_version']
            self.primary_counter_locked = json_data['_primary_counter_locked']
            self.precon_name = json_data['_precon_name']
            self.precon_code = json_data['_precon_code']
            self.precon_descr = json_data['_precon_descr']
            self.postcon_name = json_data['_postcon_name']
            self.postcon_code = json_data['_postcon_code']
            self.postcon_descr = json_data['_postcon_descr']
            self.comment = json_data['_comment']
            self.custom_imports = json_data['_custom_imports']
        except KeyError as keyerror:
            self.logger.error('KeyError: no {} could be found in the loaded data'.format(keyerror))

        self.sequences = []
        try:
            for index, seq in enumerate(json_data['sequences']):
                self.sequences.append(TestSequence(json_data=seq, logger=logger))
        except KeyError as keyerror:
            self.logger.error('KeyError: no {} could be found in the loaded data'.format(keyerror))
        return

    @staticmethod
    def serialize(obj):
        """
        JSON serializer for objects which are not serializable by default.
        Uses the overwritten __deepcopy__ methods of the classes, to make copies. Then takes the dictionarys of them
        and deletes the objects, which can not be serialized.
        """
        if isinstance(obj, TestSpecification):
            obj_copy = obj.__deepcopy__()
            obj_dict = obj_copy.__dict__
            del obj_dict['logger']
            return obj_dict
        if isinstance(obj, TestSequence):
            obj_copy = obj.__deepcopy__()
            obj_dict = obj_copy.__dict__
            del obj_dict['logger']
            return obj_dict
        if isinstance(obj, Step):
            obj_copy = obj.__deepcopy__()
            obj_dict = obj_copy.__dict__
            del obj_dict['test']
            del obj_dict['logger']
            return obj_dict

    def add_sequence(self):
        # find out the sequence number for the new one
        number_of_sequence = len(self.sequences)
        # append it to the list
        self.sequences.append(TestSequence(sequence=number_of_sequence, logger=logger))
        return number_of_sequence

    def get_sequence(self, sequence_number):
        for testseq in self.sequences:
            if testseq.sequence == sequence_number:
                return testseq


# @unittest.skip("demonstrating skipping")
class TestParseStepNumber(unittest.TestCase):
    """
    Test the function parse_step_number.
    """

    def test_datatype_str(self):
        # valid input
        self.assertEqual(parse_step_number('1'), (1, 0))
        self.assertEqual(parse_step_number('1.0'), (1, 0))
        self.assertEqual(parse_step_number('1.1'), (1, 1))
        self.assertEqual(parse_step_number('1.2'), (1, 2))

        # invalid input
        self.assertRaises(ValueError, parse_step_number, 'a')
        self.assertRaises(ValueError, parse_step_number, 'a.a')
        self.assertRaises(ValueError, parse_step_number, '1a.1')

    def test_datatype_int(self):
        self.assertEqual(parse_step_number(1), (1, 0))
        self.assertEqual(parse_step_number(2), (2, 0))

    def test_datatype_float(self):
        self.assertEqual(parse_step_number(1.0), (1, 0))
        self.assertEqual(parse_step_number(1.1), (1, 1))
        self.assertEqual(parse_step_number(1.2), (1, 2))

    def test_datatype_list(self):
        self.assertRaises(AssertionError, parse_step_number, [])


# @unittest.skip("demonstrating skipping")
class TestResortStepListByStepNumbers(unittest.TestCase):
    """
    Test the function resort_step_list_by_step_numbers. A method to verify the list consistency is implemented.
    """
    @classmethod
    def setUpClass(self):
        logger.debug('create a test specification and create step objects (not assigned to the test specification yet)')
        self.tstspc = TestSequence()
        self.step_1 = Step(step_num=1.0)
        self.step_1_1 = Step(step_num=1.1)
        self.step_1_2 = Step(step_num=1.2)
        self.step_2 = Step(step_num=2.0)
        self.step_3 = Step(step_num=3.0)
        self.step_4 = Step(step_num=4.0)
        self.step_4_1 = Step(step_num=4.1)
        self.step_5 = Step(step_num=5.0)
        self.step_6 = Step(step_num=6.0)
        self.step_6_1 = Step(step_num=6.1)
        self.step_6_2 = Step(step_num=6.2)
        self.step_6_3 = Step(step_num=6.3)
        self.step_7 = Step(step_num=7.0)

    def test_list_needs_sorting(self):
        logger.info('Running: {}'.format(self._testMethodName))
        self.tstspc.steps = [
            self.step_1,
            self.step_1_1,
            self.step_2,
            self.step_1_2,
            self.step_3,
            self.step_4,
            self.step_4_1,
            self.step_5,
            self.step_7,
            self.step_6,
            self.step_6_1,
            self.step_6_3,
            self.step_6_2,
        ]
        self.tstspc.log_list_of_steps()
        logger.debug('sorting the list')
        self.tstspc.resort_step_list_by_step_numbers()
        self.tstspc.log_list_of_steps()
        self.assertTrue(self.tstspc.verify_step_list_consistency())

    def test_nothing_to_sort(self):
        logger.info('Running: {}'.format(self._testMethodName))
        self.tstspc.steps = [
            self.step_1,
            self.step_1_1,
            self.step_1_2,
            self.step_2,
            self.step_3,
            self.step_4,
            self.step_4_1,
            self.step_5,
            self.step_6,
            self.step_6_1,
            self.step_6_2,
            self.step_6_3,
            self.step_7
        ]
        self.tstspc.log_list_of_steps()
        logger.debug('sorting the list')
        self.tstspc.resort_step_list_by_step_numbers()
        self.tstspc.log_list_of_steps()
        self.assertTrue(self.tstspc.verify_step_list_consistency())


# @unittest.skip("demonstrating skipping")
class TestAddStepBelow(unittest.TestCase):
    """
    Test if the function add_step_below does what it should.
    """

    def setUp(self) -> None:
        logger.info('setUp: {}'.format(self._testMethodName))
        logger.debug('create a test specification and add three steps')
        self.tstspc = TestSequence()
        logger.debug('Python object ID of the test specification: {}'.format(id(self.tstspc)))
        self.tstspc.log_list_of_steps()

        # create a new step and append it to the steps list
        number_new_step = create_step_number(1, 0)
        step_1 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_1)
        self.stp1 = id(step_1)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(2, 0)
        step_2 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_2)
        self.stp2 = id(step_2)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 0)
        step_3 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3)
        self.stp3 = id(step_3)

        self.tstspc.log_list_of_steps()

    def tearDown(self) -> None:
        logger.info('tearDown: after running test method: {}'.format(self._testMethodName))
        # save the test specification as JSON file
        json_file = os.path.join(confignator.get_option('logging', 'log-dir'), 'tst/data_model/' + self._testMethodName + '.json')
        with open(json_file, 'w') as fileobject:
            data = self.tstspc.encode_to_json()
            fileobject.write(data)
            logger.debug('Test specification was dumped as JSON: {}'.format(json_file))

    def test_add_step_below_unlocked(self):
        """
        The test specification is unlocked.
        add a step below step 2, the new step becomes (the new) step 3,
        the step which was number 3 till now, becomes step 4
        """
        logger.info('Running {}'.format(self._testMethodName))
        self.tstspc.primary_counter_locked = False

        # verify the numeration of the steps before adding a new step
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')

        # add the new step
        logger.info('going to add a step below Step 2.0')
        new_step = self.tstspc.add_step_below(2.0)
        nw_stp = id(new_step)

        self.tstspc.log_list_of_steps()

        # verify the numeration of the steps
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '4.0')

        # verify the order of the steps, by comparing the IDs of the Python objects
        self.assertEqual(id(self.tstspc.steps[0]), self.stp1)
        self.assertEqual(id(self.tstspc.steps[1]), self.stp2)
        self.assertEqual(id(self.tstspc.steps[2]), nw_stp)
        self.assertEqual(id(self.tstspc.steps[3]), self.stp3)

    def test_add_step_below_locked(self):
        """
        The test specification is locked.
        add a step below step 2, the new step becomes (the new) step 3,
        the step which was number 3 till now, becomes step 4
        """
        logger.info('Running {}'.format(self._testMethodName))
        self.tstspc.primary_counter_locked = True

        # verify the numeration of the steps before adding a new step
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')

        # add the new step
        logger.info('going to add a step below Step 2.0')
        new_step = self.tstspc.add_step_below(2.0)
        nw_stp = id(new_step)

        self.tstspc.log_list_of_steps()

        # verify the numeration of the steps
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '2.1')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.0')

        # verify the order of the steps, by comparing the IDs of the Python objects
        self.assertEqual(id(self.tstspc.steps[0]), self.stp1)
        self.assertEqual(id(self.tstspc.steps[1]), self.stp2)
        self.assertEqual(id(self.tstspc.steps[2]), nw_stp)
        self.assertEqual(id(self.tstspc.steps[3]), self.stp3)


# @unittest.skip("demonstrating skipping")
class TestAddStepAbove(unittest.TestCase):
    """
    Test if the function add_step_above does what it should.
    """

    def setUp(self) -> None:
        logger.info('setUp: {}'.format(self._testMethodName))
        logger.debug('create a test specification and add three steps')
        self.tstspc = TestSequence()
        logger.debug('Python object ID of the test specification: {}'.format(id(self.tstspc)))
        self.tstspc.log_list_of_steps()

        # create a new step and append it to the steps list
        number_new_step = create_step_number(1, 0)
        step_1 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_1)
        self.stp1 = id(step_1)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(2, 0)
        step_2 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_2)
        self.stp2 = id(step_2)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 0)
        step_3 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3)
        self.stp3 = id(step_3)

        self.tstspc.log_list_of_steps()

    def tearDown(self) -> None:
        logger.info('tear down after running test method: {}'.format(self._testMethodName))
        # save the test specification as JSON file
        json_file = os.path.join(confignator.get_option('logging', 'log-dir'), 'tst/data_model/' + self._testMethodName + '.json')
        with open(json_file, 'w') as fileobject:
            data = self.tstspc.encode_to_json()
            fileobject.write(data)
            logger.debug('Test specification was dumped as JSON: {}'.format(json_file))

    def test_add_step_above_unlocked(self):
        """
        The test specification is unlocked.
        add a step above step 2, the new step becomes (the new) step 2,
        the step which was number 2 till now, becomes step 3
        """
        logger.info('Running {}'.format(self._testMethodName))
        self.tstspc.primary_counter_locked = False

        # verify the numeration of the steps before adding a new step
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')

        # add the new step
        logger.info('going to add a step above Step 2.0')
        new_step = self.tstspc.add_step_above(2.0)
        nw_stp = id(new_step)

        self.tstspc.log_list_of_steps()

        # verify the numeration of the steps
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '4.0')

        # verify the order of the steps, by comparing the IDs of the Python objects
        self.assertEqual(id(self.tstspc.steps[0]), self.stp1)
        self.assertEqual(id(self.tstspc.steps[1]), nw_stp)
        self.assertEqual(id(self.tstspc.steps[2]), self.stp2)
        self.assertEqual(id(self.tstspc.steps[3]), self.stp3)

    def test_add_step_above_locked(self):
        """
        The test specification is locked.
        add a step above step 2, the new step becomes (the new) step 1.1,
        the step which was number 2 till now, stays step 2
        """
        logger.info('Running {}'.format(self._testMethodName))
        self.tstspc.primary_counter_locked = True

        # verify the numeration of the steps before adding a new step
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')

        # add the new step
        logger.info('going to add a step above Step 2.0')
        new_step = self.tstspc.add_step_above(2.0)
        nw_stp = id(new_step)

        self.tstspc.log_list_of_steps()

        # verify the numeration of the steps
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '1.1')
        self.assertEqual(self.tstspc.steps[2].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.0')

        # verify the order of the steps, by comparing the IDs of the Python objects
        self.assertEqual(id(self.tstspc.steps[0]), self.stp1)
        self.assertEqual(id(self.tstspc.steps[1]), nw_stp)
        self.assertEqual(id(self.tstspc.steps[2]), self.stp2)
        self.assertEqual(id(self.tstspc.steps[3]), self.stp3)


# @unittest.skip("demonstrating skipping")
class TestMoveStep(unittest.TestCase):
    """
    Test if moving of a step upwards or downwards works
    """

    def setUp(self) -> None:
        logger.info('setUp: {}'.format(self._testMethodName))
        logger.debug('create a test specification and add steps')
        self.tstspc = TestSequence()
        logger.debug('Python object ID of the test specification: {}'.format(id(self.tstspc)))
        self.tstspc.log_list_of_steps()

        # create a new step and append it to the steps list
        number_new_step = create_step_number(1, 0)
        step_1 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_1)
        self.stp1 = id(step_1)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(2, 0)
        step_2 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_2)
        self.stp2 = id(step_2)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 0)
        step_3 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3)
        self.stp3 = id(step_3)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 1)
        step_3_1 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3_1)
        self.stp3_1 = id(step_3_1)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 2)
        step_3_2 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3_2)
        self.stp3_2 = id(step_3_2)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 3)
        step_3_3 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3_3)
        self.stp3_3 = id(step_3_3)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 4)
        step_3_4 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3_4)
        self.stp3_4 = id(step_3_4)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 5)
        step_3_5 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3_5)
        self.stp3_5 = id(step_3_5)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(4, 0)
        step_4 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_4)
        self.stp4 = id(step_4)

    def tearDown(self) -> None:
        logger.info('tearDown: after running test method: {}'.format(self._testMethodName))
        # save the test specification as JSON file
        json_file = os.path.join(confignator.get_option('logging', 'log-dir'), 'tst/data_model/' + self._testMethodName + '.json')
        with open(json_file, 'w') as fileobject:
            data = self.tstspc.encode_to_json()
            fileobject.write(data)
            logger.debug('Test specification was dumped as JSON: {}'.format(json_file))

    def test_move_step_to_top_unlocked(self):
        """
        It should not matter if the test is unlocked, if moving to the top.
        Move step 3.2 to the top.
        """
        logger.info('Running {}'.format(self._testMethodName))
        self.tstspc.primary_counter_locked = False

        # verify the numeration of the steps before adding a new step
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.1')
        self.assertEqual(self.tstspc.steps[4].step_number, '3.2')
        self.assertEqual(self.tstspc.steps[5].step_number, '3.3')
        self.assertEqual(self.tstspc.steps[6].step_number, '3.4')
        self.assertEqual(self.tstspc.steps[7].step_number, '3.5')
        self.assertEqual(self.tstspc.steps[8].step_number, '4.0')

        logger.info('moving step 3.2 to the top')
        step_to_move_index = self.tstspc.get_step_index('3.2')
        self.tstspc.move_step(step_to_move_index, 0)

        # verify the numeration of the steps
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '4.0')
        self.assertEqual(self.tstspc.steps[4].step_number, '4.1')
        self.assertEqual(self.tstspc.steps[5].step_number, '4.2')
        self.assertEqual(self.tstspc.steps[6].step_number, '4.3')
        self.assertEqual(self.tstspc.steps[7].step_number, '4.4')
        self.assertEqual(self.tstspc.steps[8].step_number, '5.0')

        # verify the order of the steps, by comparing the IDs of the Python objects
        self.assertEqual(id(self.tstspc.steps[0]), self.stp3_2)
        self.assertEqual(id(self.tstspc.steps[1]), self.stp1)
        self.assertEqual(id(self.tstspc.steps[2]), self.stp2)
        self.assertEqual(id(self.tstspc.steps[3]), self.stp3)
        self.assertEqual(id(self.tstspc.steps[4]), self.stp3_1)
        self.assertEqual(id(self.tstspc.steps[5]), self.stp3_3)
        self.assertEqual(id(self.tstspc.steps[6]), self.stp3_4)
        self.assertEqual(id(self.tstspc.steps[7]), self.stp3_5)
        self.assertEqual(id(self.tstspc.steps[8]), self.stp4)

    def test_move_step_to_top_locked(self):
        """
        It should not matter if the test is unlocked, if moving to the top.
        Move step 3.2 to the top.
        """
        logger.info('Running {}'.format(self._testMethodName))
        self.tstspc.primary_counter_locked = True

        # verify the numeration of the steps before adding a new step
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.1')
        self.assertEqual(self.tstspc.steps[4].step_number, '3.2')
        self.assertEqual(self.tstspc.steps[5].step_number, '3.3')
        self.assertEqual(self.tstspc.steps[6].step_number, '3.4')
        self.assertEqual(self.tstspc.steps[7].step_number, '3.5')
        self.assertEqual(self.tstspc.steps[8].step_number, '4.0')

        logger.info('moving step 3.2 to the top')
        step_to_move_index = self.tstspc.get_step_index('3.2')
        self.tstspc.move_step(step_to_move_index, 0)

        # verify the numeration of the steps
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '4.0')
        self.assertEqual(self.tstspc.steps[4].step_number, '4.1')
        self.assertEqual(self.tstspc.steps[5].step_number, '4.2')
        self.assertEqual(self.tstspc.steps[6].step_number, '4.3')
        self.assertEqual(self.tstspc.steps[7].step_number, '4.4')
        self.assertEqual(self.tstspc.steps[8].step_number, '5.0')

        # verify the order of the steps, by comparing the IDs of the Python objects
        self.assertEqual(id(self.tstspc.steps[0]), self.stp3_2)
        self.assertEqual(id(self.tstspc.steps[1]), self.stp1)
        self.assertEqual(id(self.tstspc.steps[2]), self.stp2)
        self.assertEqual(id(self.tstspc.steps[3]), self.stp3)
        self.assertEqual(id(self.tstspc.steps[4]), self.stp3_1)
        self.assertEqual(id(self.tstspc.steps[5]), self.stp3_3)
        self.assertEqual(id(self.tstspc.steps[6]), self.stp3_4)
        self.assertEqual(id(self.tstspc.steps[7]), self.stp3_5)
        self.assertEqual(id(self.tstspc.steps[8]), self.stp4)

    def test_move_step_upwards_unlocked(self):
        """
        Move step 3.2 to above step 3.0.
        """
        logger.info('Running {}'.format(self._testMethodName))
        self.tstspc.primary_counter_locked = False

        # verify the numeration of the steps before adding a new step
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.1')
        self.assertEqual(self.tstspc.steps[4].step_number, '3.2')
        self.assertEqual(self.tstspc.steps[5].step_number, '3.3')
        self.assertEqual(self.tstspc.steps[6].step_number, '3.4')
        self.assertEqual(self.tstspc.steps[7].step_number, '3.5')
        self.assertEqual(self.tstspc.steps[8].step_number, '4.0')

        logger.info('move step 3.2 to above step 3.0.')
        step_to_move_index = self.tstspc.get_step_index('3.2')
        self.tstspc.move_step(step_to_move_index, 2)

        # verify the numeration of the steps
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '4.0')
        self.assertEqual(self.tstspc.steps[4].step_number, '4.1')
        self.assertEqual(self.tstspc.steps[5].step_number, '4.2')
        self.assertEqual(self.tstspc.steps[6].step_number, '4.3')
        self.assertEqual(self.tstspc.steps[7].step_number, '4.4')
        self.assertEqual(self.tstspc.steps[8].step_number, '5.0')

        # verify the order of the steps, by comparing the IDs of the Python objects
        self.assertEqual(id(self.tstspc.steps[0]), self.stp1)
        self.assertEqual(id(self.tstspc.steps[1]), self.stp2)
        self.assertEqual(id(self.tstspc.steps[2]), self.stp3_2)
        self.assertEqual(id(self.tstspc.steps[3]), self.stp3)
        self.assertEqual(id(self.tstspc.steps[4]), self.stp3_1)
        self.assertEqual(id(self.tstspc.steps[5]), self.stp3_3)
        self.assertEqual(id(self.tstspc.steps[6]), self.stp3_4)
        self.assertEqual(id(self.tstspc.steps[7]), self.stp3_5)
        self.assertEqual(id(self.tstspc.steps[8]), self.stp4)

    def test_move_step_upwards_locked(self):
        """
        Move step 3.2 to above step 3.0.
        """
        logger.info('Running {}'.format(self._testMethodName))
        self.tstspc.primary_counter_locked = True

        # verify the numeration of the steps before adding a new step
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.1')
        self.assertEqual(self.tstspc.steps[4].step_number, '3.2')
        self.assertEqual(self.tstspc.steps[5].step_number, '3.3')
        self.assertEqual(self.tstspc.steps[6].step_number, '3.4')
        self.assertEqual(self.tstspc.steps[7].step_number, '3.5')
        self.assertEqual(self.tstspc.steps[8].step_number, '4.0')

        logger.info('move step 3.2 to above step 3.0.')
        step_to_move_index = self.tstspc.get_step_index('3.2')
        self.tstspc.move_step(step_to_move_index, 2)

        # verify the numeration of the steps
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '2.1')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[4].step_number, '3.1')
        self.assertEqual(self.tstspc.steps[5].step_number, '3.2')
        self.assertEqual(self.tstspc.steps[6].step_number, '3.3')
        self.assertEqual(self.tstspc.steps[7].step_number, '3.4')
        self.assertEqual(self.tstspc.steps[8].step_number, '4.0')

        # verify the order of the steps, by comparing the IDs of the Python objects
        self.assertEqual(id(self.tstspc.steps[0]), self.stp1)
        self.assertEqual(id(self.tstspc.steps[1]), self.stp2)
        self.assertEqual(id(self.tstspc.steps[2]), self.stp3_2)
        self.assertEqual(id(self.tstspc.steps[3]), self.stp3)
        self.assertEqual(id(self.tstspc.steps[4]), self.stp3_1)
        self.assertEqual(id(self.tstspc.steps[5]), self.stp3_3)
        self.assertEqual(id(self.tstspc.steps[6]), self.stp3_4)
        self.assertEqual(id(self.tstspc.steps[7]), self.stp3_5)
        self.assertEqual(id(self.tstspc.steps[8]), self.stp4)

    def test_move_step_downwards_unlocked(self):
        """
        Move step 3.2 to below step 4.0.
        """
        logger.info('Running {}'.format(self._testMethodName))
        self.tstspc.primary_counter_locked = False

        # verify the numeration of the steps before adding a new step
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.1')
        self.assertEqual(self.tstspc.steps[4].step_number, '3.2')
        self.assertEqual(self.tstspc.steps[5].step_number, '3.3')
        self.assertEqual(self.tstspc.steps[6].step_number, '3.4')
        self.assertEqual(self.tstspc.steps[7].step_number, '3.5')
        self.assertEqual(self.tstspc.steps[8].step_number, '4.0')

        logger.info('Move step 3.2 to below step 4.0.')
        step_to_move_index = self.tstspc.get_step_index('3.2')
        self.tstspc.move_step(step_to_move_index, 9)

        # verify the numeration of the steps
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.1')
        self.assertEqual(self.tstspc.steps[4].step_number, '3.2')
        self.assertEqual(self.tstspc.steps[5].step_number, '3.3')
        self.assertEqual(self.tstspc.steps[6].step_number, '3.4')
        self.assertEqual(self.tstspc.steps[7].step_number, '4.0')
        self.assertEqual(self.tstspc.steps[8].step_number, '5.0')

        # verify the order of the steps, by comparing the IDs of the Python objects
        self.assertEqual(id(self.tstspc.steps[0]), self.stp1)
        self.assertEqual(id(self.tstspc.steps[1]), self.stp2)
        self.assertEqual(id(self.tstspc.steps[2]), self.stp3)
        self.assertEqual(id(self.tstspc.steps[3]), self.stp3_1)
        self.assertEqual(id(self.tstspc.steps[4]), self.stp3_3)
        self.assertEqual(id(self.tstspc.steps[5]), self.stp3_4)
        self.assertEqual(id(self.tstspc.steps[6]), self.stp3_5)
        self.assertEqual(id(self.tstspc.steps[7]), self.stp4)
        self.assertEqual(id(self.tstspc.steps[8]), self.stp3_2)

    def test_move_step_downwards_locked(self):
        """
        Move step 3.2 to below step 4.0.
        """
        logger.info('Running {}'.format(self._testMethodName))
        self.tstspc.primary_counter_locked = True

        # verify the numeration of the steps before adding a new step
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.1')
        self.assertEqual(self.tstspc.steps[4].step_number, '3.2')
        self.assertEqual(self.tstspc.steps[5].step_number, '3.3')
        self.assertEqual(self.tstspc.steps[6].step_number, '3.4')
        self.assertEqual(self.tstspc.steps[7].step_number, '3.5')
        self.assertEqual(self.tstspc.steps[8].step_number, '4.0')

        logger.info('Move step 3.2 to below step 4.0.')
        step_to_move_index = self.tstspc.get_step_index('3.2')
        self.tstspc.move_step(step_to_move_index, 9)

        # verify the numeration of the steps
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.1')
        self.assertEqual(self.tstspc.steps[4].step_number, '3.2')
        self.assertEqual(self.tstspc.steps[5].step_number, '3.3')
        self.assertEqual(self.tstspc.steps[6].step_number, '3.4')
        self.assertEqual(self.tstspc.steps[7].step_number, '4.0')
        self.assertEqual(self.tstspc.steps[8].step_number, '4.1')

        # verify the order of the steps, by comparing the IDs of the Python objects
        self.assertEqual(id(self.tstspc.steps[0]), self.stp1)
        self.assertEqual(id(self.tstspc.steps[1]), self.stp2)
        self.assertEqual(id(self.tstspc.steps[2]), self.stp3)
        self.assertEqual(id(self.tstspc.steps[3]), self.stp3_1)
        self.assertEqual(id(self.tstspc.steps[4]), self.stp3_3)
        self.assertEqual(id(self.tstspc.steps[5]), self.stp3_4)
        self.assertEqual(id(self.tstspc.steps[6]), self.stp3_5)
        self.assertEqual(id(self.tstspc.steps[7]), self.stp4)
        self.assertEqual(id(self.tstspc.steps[8]), self.stp3_2)


# @unittest.skip("demonstrating skipping")
class TestRenumberSteps(unittest.TestCase):
    """
    Test if the method to renumber all steps works
    """

    def setUp(self) -> None:
        logger.info('setUp: {}'.format(self._testMethodName))
        logger.debug('create a test specification and add steps')
        self.tstspc = TestSequence()
        logger.debug('Python object ID of the test specification: {}'.format(id(self.tstspc)))

        # create a new step and append it to the steps list
        number_new_step = create_step_number(1, 0)
        step_1 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_1)
        self.stp1 = id(step_1)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(2, 0)
        step_2 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_2)
        self.stp2 = id(step_2)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 0)
        step_3 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3)
        self.stp3 = id(step_3)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 1)
        step_3_1 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3_1)
        self.stp3_1 = id(step_3_1)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 2)
        step_3_2 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3_2)
        self.stp3_2 = id(step_3_2)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 3)
        step_3_3 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3_3)
        self.stp3_3 = id(step_3_3)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 4)
        step_3_4 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3_4)
        self.stp3_4 = id(step_3_4)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(3, 5)
        step_3_5 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_3_5)
        self.stp3_5 = id(step_3_5)

        # create a new step and append it to the steps list
        number_new_step = create_step_number(4, 0)
        step_4 = Step(step_num=number_new_step, test_seq=self.tstspc)
        self.tstspc.steps.append(step_4)
        self.stp4 = id(step_4)

    def tearDown(self) -> None:
        logger.info('tearDown: after running test method: {}'.format(self._testMethodName))
        # save the test specification as JSON file
        json_file = os.path.join(confignator.get_option('logging', 'log-dir'), 'tst/data_model/' + self._testMethodName + '.json')
        with open(json_file, 'w') as fileobject:
            data = self.tstspc.encode_to_json()
            fileobject.write(data)
            logger.debug('Test specification was dumped as JSON: {}'.format(json_file))

    def test_move_step_to_top_unlocked(self):
        """
        It should not matter if the test is unlocked, if moving to the top.
        Move step 3.2 to the top.
        """
        logger.info('Running {}'.format(self._testMethodName))
        self.tstspc.primary_counter_locked = False

        # verify the numeration of the steps before adding a new step
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '3.1')
        self.assertEqual(self.tstspc.steps[4].step_number, '3.2')
        self.assertEqual(self.tstspc.steps[5].step_number, '3.3')
        self.assertEqual(self.tstspc.steps[6].step_number, '3.4')
        self.assertEqual(self.tstspc.steps[7].step_number, '3.5')
        self.assertEqual(self.tstspc.steps[8].step_number, '4.0')

        # renumber the steps
        logger.info('renumber the steps')
        self.tstspc.renumber_steps()

        # verify the numeration of the steps
        self.assertEqual(self.tstspc.steps[0].step_number, '1.0')
        self.assertEqual(self.tstspc.steps[1].step_number, '2.0')
        self.assertEqual(self.tstspc.steps[2].step_number, '3.0')
        self.assertEqual(self.tstspc.steps[3].step_number, '4.0')
        self.assertEqual(self.tstspc.steps[4].step_number, '5.0')
        self.assertEqual(self.tstspc.steps[5].step_number, '6.0')
        self.assertEqual(self.tstspc.steps[6].step_number, '7.0')
        self.assertEqual(self.tstspc.steps[7].step_number, '8.0')
        self.assertEqual(self.tstspc.steps[8].step_number, '9.0')


def create_start_sequence_step(seq_to_start: int) -> Step:
    """
    Creates a Step instance which has the purpose to be the starting point of a sequence.

    :param: int seq_to_start: the number of the sequence which should be started.
    """
    assert isinstance(seq_to_start, int)
    s = Step()
    s.start_sequence = seq_to_start
    s.description = _('Start sequence {}'.format(seq_to_start))
    s.command_code = '#ToDo'
    return s


def create_stop_sequence_step(seq_to_stop: int) -> Step:
    """
    Creates a Step instance which has the purpose to trigger the stop of a sequence.

    :param: int seq_to_stop: the number of the sequence which should be stopped.
    """
    assert isinstance(seq_to_stop, int)
    s = Step()
    s.start_sequence = seq_to_stop
    s.description = _('Stop sequence {}'.format(seq_to_stop))
    s.command_code = '#ToDo'
    return s


if __name__ == '__main__':
    # create a log file for the self testing
    log_path = confignator.get_option('logging', 'log-dir')
    log_file = os.path.join(log_path, 'tst/data_model/data_model.log')
    logger.addHandler(hdlr=console_hdlr)
    file_hdlr = toolbox.create_file_handler(file=log_file)
    logger.addHandler(hdlr=file_hdlr)

    test_program = unittest.main(verbosity=2, exit=False)
    logger.info('Ran {} tests'.format(test_program.result.testsRun))
    if test_program.result.wasSuccessful():
        logger.info('Self-test program was successful.')
    else:
        logger.critical('Self-test program failed.')
    if len(test_program.result.failures) > 0:
        logger.error('{} tests failed'.format(len(test_program.result.failures)))
    for fail in test_program.result.failures:
        logger.error(fail[0])
        logger.debug(fail[1])