diff --git a/examples/varianter_cit/params.cit b/examples/varianter_cit/params.cit new file mode 100644 index 0000000000000000000000000000000000000000..a20c3d4ec6c5e40b0af3c47cbc53daf8f0ccf096 --- /dev/null +++ b/examples/varianter_cit/params.cit @@ -0,0 +1,16 @@ +PARAMETERS +color [black, gold, red, green ] +shape[square, triangle, circle] +state[liquid, solid, gas] +material[leather, plastic, aluminum] +coating[anodic, cathodic] +p7[1,3,5,6,7] +p8[1,2,5,3,4] +p9[1,2,3,4,5] +p10[1,2,3,4,5] + +CONSTRAINTS +color != black || shape != square +color != black || shape != triangle +color != gold || coating != cathodic +material != aluminum || color != gold diff --git a/examples/varianter_cit/params.ini b/examples/varianter_cit/params.ini deleted file mode 100644 index 32e9c590a0279fea3d6eccb7ebb079c0b4973c23..0000000000000000000000000000000000000000 --- a/examples/varianter_cit/params.ini +++ /dev/null @@ -1,6 +0,0 @@ -[parameters] -color: black, gold, red, green -shape: square, triangle, circle -state: liquid, solid, gas -material: leather, plastic, aluminum -coating: anodic, cathodic diff --git a/examples/varianter_cit/test_params.cit b/examples/varianter_cit/test_params.cit new file mode 100644 index 0000000000000000000000000000000000000000..0ec5230589f6445c94f1f47b4158222c90bc44e9 --- /dev/null +++ b/examples/varianter_cit/test_params.cit @@ -0,0 +1,12 @@ +PARAMETERS +color [black, gold, red, green ] +shape[square, triangle, circle] +state[liquid, solid, gas] +material[leather, plastic, aluminum] +coating[anodic, cathodic] + +CONSTRAINTS +color != black +color != gold +color != red + diff --git a/optional_plugins/varianter_cit/avocado_varianter_cit/Cit.py b/optional_plugins/varianter_cit/avocado_varianter_cit/Cit.py new file mode 100644 index 0000000000000000000000000000000000000000..5ebdd6f505240d94d123da6dd54e1ba31cccf700 --- /dev/null +++ b/optional_plugins/varianter_cit/avocado_varianter_cit/Cit.py @@ -0,0 +1,307 @@ +import random +import logging + +from avocado.core.output import LOG_UI +from avocado.core.output import Throbber + +from .Solver import Solver +from .CombinationMatrix import CombinationMatrix + +ITERATIONS_SIZE = 600 +LOG = LOG_UI.getChild("Cit") +LOG.setLevel(logging.INFO) + + +class Cit: + + def __init__(self, input_data, t_value, constraints): + """ + Creation of CombinationMatrix from user input + + :param input_data: parameters from user + :param t_value: size of one combination + :param constraints: constraints of combinations + """ + self.data = input_data + self.t_value = t_value + # CombinationMatrix creation + self.combination_matrix = CombinationMatrix(input_data, t_value) + # Creation of solver and simplification of constraints + self.solver = Solver(input_data, constraints) + # Combinations which do not match to the constraints are disabled + self.solver.clean_hash_table(self.combination_matrix, t_value) + self.final_matrix = [] + self.__throbber = Throbber() + + def final_matrix_init(self): + """ + Creation of the first solution. This solution is the start of searching + for the best solution + + :return: solution matrix (list(list)) + """ + self.final_matrix = [self.create_random_row_with_constraints()] + self.combination_matrix.cover_solution_row(self.final_matrix[0]) + while self.combination_matrix.total_uncovered != 0: + if self.combination_matrix.total_uncovered < self.combination_matrix.total_covered_more_than_ones: + new_row = self.compute_row() + else: + new_row = self.compute_row_using_hamming_distance() + self.combination_matrix.cover_solution_row(new_row) + self.final_matrix.append(new_row) + return self.final_matrix + + def compute(self): + """ + Searching for the best solution. It creates one solution and from that, + it tries to create smaller solution. This searching process is limited + by ITERATIONS_SIZE. When ITERATIONS_SIZE is 0 the last found solution is + the best solution. + + :return: The best solution + """ + self.final_matrix = self.final_matrix_init() + matrix = [x[:] for x in self.final_matrix] + iterations = ITERATIONS_SIZE + step_size = 1 + deleted_rows = [] + while step_size != 0: + for i in range(step_size): + delete_row = matrix.pop(random.randint(0, len(matrix) - 1)) + self.combination_matrix.uncover_solution_row(delete_row) + deleted_rows.append(delete_row) + LOG.debug("I'm trying solution with size " + str(len(matrix)) + " and " + str(iterations) + " iterations") + matrix, is_better_solution = self.find_better_solution(iterations, matrix) + if is_better_solution: + self.final_matrix = matrix[:] + deleted_rows = [] + step_size *= 2 + LOG.debug("-----solution with size " + str(len(matrix)) + " was found-----\n") + iterations = ITERATIONS_SIZE + else: + LOG.debug("-----solution with size " + str(len(matrix)) + " was not found-----\n") + for i in range(step_size): + self.combination_matrix.cover_solution_row(deleted_rows[i]) + matrix.append(deleted_rows[i]) + if step_size > 1: + step_size = 1 + else: + step_size = 0 + + return self.final_matrix + + def find_better_solution(self, counter, matrix): + """ + Changing the matrix to cover all combinations + + :param counter: maximum number of changes in the matrix + :param matrix: matrix to be changed + :return: new matrix and is changes have been successful? + """ + while self.combination_matrix.total_uncovered != 0: + LOG.debug(self.__throbber.render(), extra={"skip_newline": True}) + solution, row_index, _ = self.use_random_algorithm(matrix) + self.combination_matrix.uncover_solution_row(matrix[row_index]) + self.combination_matrix.cover_solution_row(solution) + matrix[row_index] = solution + if counter == 0: + return matrix, False + counter -= 1 + return matrix, True + + def use_random_algorithm(self, matrix): + """ + Applies one of these algorithms to the matrix. + It chooses algorithm by random in proportion 1:1:8 + + :param matrix: matrix to be changed + :return: new row of matrix, index of row inside matrix and parameters which has been changed + """ + switch = random.randint(0, 9) + if switch == 0: + solution, row_index, parameters = self.change_one_value(matrix) + elif switch == 1: + solution, row_index, parameters = self.change_one_column(matrix) + else: + solution, row_index, parameters = self.cover_missing_combination(matrix) + return solution, row_index, parameters + + def compute_row(self): + """ + Computation of one row which covers most of combinations + + :return: new solution row + """ + is_valid_row = False + while not is_valid_row: + possible_parameters = list(self.combination_matrix.uncovered_rows) + row = [-1] * len(self.data) + while len(possible_parameters) != 0: + # finding uncovered combination + combination_parameters_index = random.randint(0, len(possible_parameters) - 1) + combination_parameters = possible_parameters[combination_parameters_index] + del possible_parameters[combination_parameters_index] + combination_row = self.combination_matrix.get_row(combination_parameters) + possible_combinations = list(combination_row.get_all_uncovered_combinations()) + combination_index = random.randint(0, len(possible_combinations) - 1) + combination = possible_combinations[combination_index] + is_parameter_used = False + # Are parameters already used in row? + for i in combination_parameters: + if row[i] != -1: + is_parameter_used = True + break + if is_parameter_used: + continue + row_copy = row.copy() + # Is combination matches the constraints? + for index, parameter in enumerate(combination_parameters): + row_copy[parameter] = combination[index] + if self.combination_matrix.is_valid_solution(row_copy): + row = row_copy + # Filling in of free spaces inside the row + for index, r in enumerate(row): + if r == -1: + is_valid = False + while not is_valid: + row[index] = random.randint(0, self.data[index] - 1) + is_valid = self.combination_matrix.is_valid_solution(row) + is_valid_row = self.combination_matrix.is_valid_solution(row) + + return row + + def cover_missing_combination(self, matrix): + """ + Randomly finds one missing combination. This combination puts into each + row of the matrix. The row with the best coverage is the solution + + :param matrix: matrix to be changed + :return: solution, index of solution inside matrix and parameters which has been changed + """ + parameters, combination = self.get_missing_combination_random() + best_uncover = float("inf") + best_solution = [] + best_row_index = 0 + for row_index in range(len(matrix)): + solution = [x for x in matrix[row_index]] + for index, item in enumerate(parameters): + solution[item] = combination[index] + if self.combination_matrix.is_valid_combination(solution, parameters): + self.combination_matrix.uncover_combination(matrix[row_index], parameters) + self.combination_matrix.cover_combination(solution, parameters) + if self.combination_matrix.total_uncovered < best_uncover: + best_uncover = self.combination_matrix.total_uncovered + best_solution = solution + best_row_index = row_index + self.combination_matrix.uncover_combination(solution, parameters) + self.combination_matrix.cover_combination(matrix[row_index], parameters) + if best_uncover == 0: + break + if len(best_solution) == 0: + return self.change_one_column(matrix) + return best_solution, best_row_index, parameters + + def get_missing_combination_random(self): + """ + Randomly finds one missing combination. + + :return: parameter of combination and values of combination + """ + possible_parameters = list(self.combination_matrix.uncovered_rows) + combination_parameters_index = random.randint(0, len(possible_parameters) - 1) + combination_parameters = possible_parameters[combination_parameters_index] + combination_row = self.combination_matrix.get_row(combination_parameters) + possible_combinations = list(combination_row.get_all_uncovered_combinations()) + combination_index = random.randint(0, len(possible_combinations) - 1) + combination = possible_combinations[combination_index] + return combination_parameters, combination + + def change_one_column(self, matrix): + """ + Randomly choose one column of the matrix. In each cell of this column + changes value. The row with the best coverage is the solution. + + :param matrix: matrix to be changed + :return: solution, index of solution inside matrix and parameters which has been changed + """ + column_index = random.randint(0, len(self.data) - 1) + best_uncover = float("inf") + best_solution = [] + best_row_index = 0 + for row_index in range(len(matrix)): + solution, row_index, parameters = self.change_one_value(matrix, row_index, column_index) + self.combination_matrix.uncover_combination(matrix[row_index], parameters) + self.combination_matrix.cover_combination(solution, parameters) + if self.combination_matrix.total_uncovered < best_uncover: + best_uncover = self.combination_matrix.total_uncovered + best_solution = solution + best_row_index = row_index + self.combination_matrix.uncover_combination(solution, parameters) + self.combination_matrix.cover_combination(matrix[row_index], parameters) + if best_uncover == 0: + break + return best_solution, best_row_index, [column_index] + + def change_one_value(self, matrix, row_index=None, column_index=None): + """ + Change one cell inside the matrix + + :param matrix: matrix to be changed + :param row_index: row inside matrix. If it's None it is chosen randomly + :param column_index: column inside matrix. If it's None it is chosen randomly + :return: solution, index of solution inside matrix and parameters which has been changed + """ + if row_index is None: + row_index = random.randint(0, len(matrix) - 1) + row = [x for x in matrix[row_index]] + if column_index is None: + column_index = random.randint(0, len(row) - 1) + possible_numbers = list(range(0, row[column_index])) + list( + range(row[column_index] + 1, self.data[column_index])) + row[column_index] = random.choice(possible_numbers) + while not self.combination_matrix.is_valid_combination(row, [column_index]): + possible_numbers.remove(row[column_index]) + if len(possible_numbers) == 0: + column_index = random.randint(0, len(row) - 1) + row_index = random.randint(0, len(matrix) - 1) + row = [x for x in matrix[row_index]] + possible_numbers = list(range(0, row[column_index])) + list( + range(row[column_index] + 1, self.data[column_index])) + row[column_index] = random.choice(possible_numbers) + return row, row_index, [column_index] + + def compute_row_using_hamming_distance(self): + """ + :return: row with the biggest hamming distance from final matrix + """ + row_1 = self.create_random_row_with_constraints() + row_2 = self.create_random_row_with_constraints() + if self.compute_hamming_distance(row_1) >= self.compute_hamming_distance(row_2): + return row_1 + else: + return row_2 + + def compute_hamming_distance(self, row): + """ + :return: hamming distance of row from final matrix + """ + distance = 0 + for final_row in self.final_matrix: + for index, cell in enumerate(final_row): + if row[index] != cell: + distance += 1 + return distance + + def create_random_row_with_constraints(self): + row = [] + data_matrix = [] + for parameter in self.data: + data_matrix.append(list(range(0, parameter))) + + # delete the forbidden values ​by constraints + self.solver.clean_data_matrix(data_matrix) + for parameter, possible_values in enumerate(data_matrix): + value_choice = random.choice(possible_values) + self.solver.clean_data_matrix(data_matrix, {"name": parameter, "value": value_choice}) + row.append(value_choice) + return row diff --git a/optional_plugins/varianter_cit/avocado_varianter_cit/CombinationMatrix.py b/optional_plugins/varianter_cit/avocado_varianter_cit/CombinationMatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..b8eae4b79074d4e75c810718c9e0ec5101a18103 --- /dev/null +++ b/optional_plugins/varianter_cit/avocado_varianter_cit/CombinationMatrix.py @@ -0,0 +1,194 @@ +import itertools + +from .CombinationRow import CombinationRow as Row + + +class CombinationMatrix: + """ + CombinationMatrix object stores Rows of combinations into dictionary. + And also stores which rows are not covered. Keys in dictionary are parameters + of combinations and values are CombinationRow objects. CombinationMatrix object + has information about how many combinations are uncovered and how many of them + are covered more than ones. + """ + + def __init__(self, input_data, t_value): + """ + :param input_data: list of data from user + :param t_value: t number from user + """ + self.hash_table = {} + self.uncovered_rows = {} + self.total_uncovered = 0 + self.total_covered_more_than_ones = 0 + # Creation of rows + for c in itertools.combinations(range(len(input_data)), t_value): + row = Row(input_data, t_value, c) + self.total_uncovered += row.uncovered + self.hash_table[c] = row + self.uncovered_rows[c] = c + + def cover_solution_row(self, row): + """ + Cover all combination by one row from possible solution + + :param row: one row from solution + :return: number of still uncovered combinations + """ + for key, value in self.hash_table.items(): + val = [] + # Getting combination from solution + for i in key: + val.append(row[i]) + + uncovered_difference, covered_more_than_ones_difference = value.cover_cell(val) + # Deleting covered row from uncovered rows + if value.uncovered == 0: + self.uncovered_rows.pop(key, None) + self.total_uncovered += uncovered_difference + self.total_covered_more_than_ones += covered_more_than_ones_difference + + return self.total_uncovered + + def cover_combination(self, row, parameters): + """ + Cover combination of specific parameters by one row from possible solution + + :param row: one row from solution + :param parameters: parameters which has to be covered + :return: number of still uncovered combinations + """ + for key, value in self.hash_table.items(): + for parameter in parameters: + if parameter in key: + val = [] + # Getting combination from solution + for i in key: + val.append(row[i]) + + uncovered_difference, covered_more_than_ones_difference = value.cover_cell(val) + # Deleting covered row from uncovered rows + if value.uncovered == 0: + self.uncovered_rows.pop(key, None) + self.total_uncovered += uncovered_difference + self.total_covered_more_than_ones += covered_more_than_ones_difference + break + return self.total_uncovered + + def uncover_solution_row(self, row): + """ + Uncover all combination by one row from possible solution + + :param row: one row from solution + :return: number of uncovered combinations + """ + for key, value in self.hash_table.items(): + val = [] + # Getting combination from solution + for i in key: + val.append(row[i]) + + uncovered_difference, covered_more_than_ones_difference = value.uncover_cell(val) + # Adding uncovered row to uncovered rows + if value.uncovered != 0: + self.uncovered_rows[key] = key + self.total_uncovered += uncovered_difference + self.total_covered_more_than_ones += covered_more_than_ones_difference + + return self.total_uncovered + + def uncover_combination(self, row, parameters): + """ + Uncover combination of specific parameters by one row from possible solution + + :param row: one row from solution + :param parameters: parameters which has to be covered + :return: number of uncovered combinations + """ + for key, value in self.hash_table.items(): + for parameter in parameters: + if parameter in key: + val = [] + # Getting combination from solution + for i in key: + val.append(row[i]) + + uncovered_difference, covered_more_than_ones_difference = value.uncover_cell(val) + # Adding uncovered row to uncovered rows + if value.uncovered != 0: + self.uncovered_rows[key] = key + self.total_uncovered += uncovered_difference + self.total_covered_more_than_ones += covered_more_than_ones_difference + break + return self.total_uncovered + + def uncover(self): + """ + Uncover all combinations + """ + self.total_covered_more_than_ones = 0 + self.total_uncovered = 0 + for _, value in self.hash_table.items(): + value.completely_uncover() + self.total_uncovered += value.uncovered + + def is_valid_solution(self, row): + """ + Is the solution row match the constraints. + + :param row: one row from solution + """ + for key, value in self.hash_table.items(): + val = [] + for i in key: + val.append(row[i]) + + if not value.is_valid(val): + return False + + return True + + def is_valid_combination(self, row, parameters): + """ + Is the specific parameters from solution row match the constraints. + + :param row: one row from solution + :param parameters: parameters from row + """ + for key, value in self.hash_table.items(): + for parameter in parameters: + if parameter in key: + val = [] + for i in key: + val.append(row[i]) + + if not value.is_valid(val): + return False + break + return True + + def del_cell(self, parameters, combination): + """ + Disable one combination. If combination is disabled it means that + the combination does not match the constraints + + :param parameters: parameters whose combination is disabled + :param combination: combination to be disabled + """ + row = self.hash_table[tuple(parameters)] + uncovered_difference = row.del_cell(combination) + if row.uncovered == 0: + self.uncovered_rows.pop(tuple(parameters), None) + self.total_uncovered += uncovered_difference + + def get_row(self, key): + """ + :param key: identifier of row + :return: CombinationRow + """ + return self.hash_table[tuple(key)] + + def __eq__(self, other): + return (self.total_uncovered == other.total_uncovered and + self.total_covered_more_than_ones == other.total_covered_more_than_ones and + self.hash_table == other.hash_table) diff --git a/optional_plugins/varianter_cit/avocado_varianter_cit/CombinationRow.py b/optional_plugins/varianter_cit/avocado_varianter_cit/CombinationRow.py new file mode 100644 index 0000000000000000000000000000000000000000..392bd6fe53e14331950600f1f0b0c7b7ec1afb82 --- /dev/null +++ b/optional_plugins/varianter_cit/avocado_varianter_cit/CombinationRow.py @@ -0,0 +1,133 @@ +import itertools + + +class CombinationRow: + + """ + Row object store all combinations between two parameters into dictionary. + Keys in dictionary are values of combinations and values in dictionary are + information about coverage. Row object has information how many combinations + are ucovered and how many of them are covered more than ones. + """ + + def __init__(self, input_data, t_value, parameters): + + """ + :param input_data: list of data from user + :param t_value: t number from user + :param parameters: the tuple of parameters whose combinations Row object represents + """ + + self.hash_table = {} + self.covered_more_than_ones = 0 + self.uncovered = 0 + array = [] + "Creation of combinations" + for i in range(t_value): + array.append(range(input_data[parameters[i]])) + for i in itertools.product(*array): + self.uncovered += 1 + self.hash_table[i] = 0 + + def cover_cell(self, key): + + """ + Cover one combination inside Row + + :param key: combination to be covered + :return: number of new covered combinations and number of new covered combinations more than ones + """ + + old_uncovered = self.uncovered + old_covered_more_than_ones = self.covered_more_than_ones + value = self.hash_table[tuple(key)] + if value is not None: + if value == 0: + self.uncovered -= 1 + elif value == 1: + self.covered_more_than_ones += 1 + self.hash_table[tuple(key)] += 1 + + return self.uncovered - old_uncovered, self.covered_more_than_ones - old_covered_more_than_ones + + def uncover_cell(self, key): + + """ + Uncover one combination inside Row + + :param key: combination to be uncovered + :return: number of new covered combinations and number of new covered combinations more than ones + """ + + old_uncovered = self.uncovered + old_covered_more_than_ones = self.covered_more_than_ones + value = self.hash_table[tuple(key)] + if value is not None and value > 0: + if value == 1: + self.uncovered += 1 + elif value == 2: + self.covered_more_than_ones -= 1 + self.hash_table[tuple(key)] -= 1 + + return self.uncovered - old_uncovered, self.covered_more_than_ones - old_covered_more_than_ones + + def completely_uncover(self): + + """ + Uncover all combinations inside Row + """ + + self.uncovered = 0 + self.covered_more_than_ones = 0 + for key in self.hash_table: + if self.hash_table[key] is not None: + self.hash_table[key] = 0 + self.uncovered += 1 + + def del_cell(self, key): + + """ + Disable one combination. If combination is disabled it means that + the combination does not match the constraints + + :param key: combination to be disabled + :return: number of new covered combinations + """ + + key = tuple(key) + if self.hash_table[key] is not None: + self.hash_table[key] = None + self.uncovered -= 1 + return -1 + else: + return 0 + + def is_valid(self, key): + + """ + Is the combination match the constraints. + + :param key: combination to valid + """ + + key = tuple(key) + if self.hash_table.get(key, 0) is None: + return False + else: + return True + + def get_all_uncovered_combinations(self): + + """ + :return: list of all uncovered combination + """ + + combinations = [] + for key, value in self.hash_table.items(): + if value == 0: + combinations.append(key) + return combinations + + def __eq__(self, other): + return (self.covered_more_than_ones == other.covered_more_than_ones and self.uncovered == other.uncovered and + self.hash_table == other.hash_table) diff --git a/optional_plugins/varianter_cit/avocado_varianter_cit/Parameter.py b/optional_plugins/varianter_cit/avocado_varianter_cit/Parameter.py new file mode 100644 index 0000000000000000000000000000000000000000..9b16f80a443eb06f97c0d8f58178235e04f5a7f6 --- /dev/null +++ b/optional_plugins/varianter_cit/avocado_varianter_cit/Parameter.py @@ -0,0 +1,56 @@ +class Pair: + + def __init__(self, name, value): + self.name = name + self.value = value + + def __str__(self): + return str(self.name) + " != " + str(self.value) + + def __eq__(self, other): + return self.name == other.name and self.value == other.value + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.__str__()) + + +class Parameter: + + def __init__(self, name, parameter_id, values): + self.is_full = False + self.name = name + self.id = parameter_id + self.values = values + self.constrained_values = set() + self.constraints = {} + for i in range(len(values)): + self.constraints[i] = [] + + def add_constraint(self, constraint): + array = [] + value = None + for pair in constraint: + if pair.name == self.name: + self.constrained_values.add(pair.value) + value = pair.value + else: + array.append(pair) + if len(self.constrained_values) == len(self.values): + self.is_full = True + if len(array) != 0: + self.constraints[value].append(array) + + def get_constraints(self): + array = [] + for key in self.constraints: + array.append(self.constraints[key]) + return array + + def get_value_index(self, value): + return self.values.index(value) + + def get_size(self): + return len(self.values) diff --git a/optional_plugins/varianter_cit/avocado_varianter_cit/Parser.py b/optional_plugins/varianter_cit/avocado_varianter_cit/Parser.py new file mode 100644 index 0000000000000000000000000000000000000000..7bac503ebf4d9539f6c95c2cc59d924a670636c7 --- /dev/null +++ b/optional_plugins/varianter_cit/avocado_varianter_cit/Parser.py @@ -0,0 +1,78 @@ +from .Parameter import Parameter +from .Parameter import Pair + +import re + + +class Parser: + + @staticmethod + def parse(file_object): + """ + Parsing of input file with parameters and constraints + + :param file_object: input file for parsing + :return: array of parameters and set of constraints + """ + + rx_parameter_name = re.compile(r"(.+?)\[") + rx_parameter_value = re.compile(r"\[(.+?)\]") + rx_constraint = re.compile(r".+?!=.+?") + parameters = [] + constraints = set() + is_parameters = None + + for line in file_object: + line = line.strip() + if not line: + continue + if line == "PARAMETERS": + is_parameters = True + continue + if line == "CONSTRAINTS": + is_parameters = False + continue + if is_parameters is None: + raise ValueError("Invalid file") + if is_parameters: + # Searching for parameter name + match = rx_parameter_name.search(line) + if match is None: + raise ValueError('Parameter name is missing') + parameter_name = match.group(1).strip() + + # Searching for parameter_values + match = rx_parameter_value.search(line) + if match is None: + raise ValueError('Parameter values are missing') + parameter_values = [x.strip() for x in match.group(1).split(',')] + if len(parameter_values) != len(set(parameter_values)): + raise ValueError('Parameter values has duplicities') + + parameters.append(Parameter(parameter_name, len(parameters), parameter_values)) + else: + constraints_data = line.split("||") + array = [] + for constraint in constraints_data: + value_index = 1 + if not rx_constraint.match(constraint): + raise ValueError("Invalid format of constraint") + constraint_data = [x.strip() for x in constraint.split("!=")] + # Searching for parameter name in constraint + constraint_name = next((p.id for p in parameters if p.name == constraint_data[0]), None) + if constraint_name is None: + # If first value inside constraint wasn't parameter name try second value + try: + constraint_name = next((p.id for p in parameters if p.name == constraint_data[1])) + value_index = 0 + except StopIteration: + raise ValueError('Name in constraint not match with names in parameters') + try: + # Checking if value in constraint is matching with parameter values + constraint_value = parameters[constraint_name].get_value_index(constraint_data[value_index]) + except ValueError: + raise ValueError('Value in constraint not match with values in parameters') + array.append(Pair(constraint_name, constraint_value)) + array.sort(key=lambda x: int(x.name)) + constraints.add(tuple(array)) + return parameters, constraints diff --git a/optional_plugins/varianter_cit/avocado_varianter_cit/Solver.py b/optional_plugins/varianter_cit/avocado_varianter_cit/Solver.py new file mode 100644 index 0000000000000000000000000000000000000000..6895ee2fa9cb980afe8721a1d609205d25e68c7b --- /dev/null +++ b/optional_plugins/varianter_cit/avocado_varianter_cit/Solver.py @@ -0,0 +1,124 @@ +import itertools + +from .Parameter import Parameter + + +class Solver: + OR = "||" + EQUALS = "=" + PARAMETER = 0 + VALUE = 2 + + def __init__(self, data, constraints): + self.data = data + self.constraints = constraints + self.parameters = [] + + self.simplify_constraints() + constraint_size = len(self.constraints) + self.read_constraints() + self.compute_constraints() + self.simplify_constraints() + while constraint_size != len(self.constraints): + constraint_size = len(self.constraints) + self.parameters = [] + self.read_constraints() + self.compute_constraints() + self.simplify_constraints() + + def read_constraints(self): + # creates new parameters with their names + for i, values_size in enumerate(self.data): + self.parameters.append(Parameter(i, i, range(values_size))) + for constraint in self.constraints: + for pair in constraint: + self.parameters[pair.name].add_constraint(constraint) + + def compute_constraints(self): + for p in self.parameters: + if p.is_full: + array = p.get_constraints() + con = list(itertools.product(*array)) + if len(con) == 0: + raise ValueError("Constraints are not satisfiable") + for constraint in con: + constraint_array = set() + for c in range(len(constraint)): + for pair in range(len(constraint[c])): + constraint_array.add(constraint[c][pair]) + constraint_array = sorted(constraint_array, key=lambda x: int(x.name)) + + has_subset = False + remove = set() + for c in self.constraints: + if len(c) < len(constraint_array): + if set(c) < set(constraint_array): + has_subset = True + break + if len(c) > len(constraint_array): + if set(c) > set(constraint_array): + remove.add(c) + if not has_subset: + self.constraints.add(tuple(constraint_array)) + for r in remove: + self.constraints.remove(r) + + def simplify_constraints(self): + items_to_remove = set() + copy = list(self.constraints.copy()) + for i in range(len(copy)): + is_brake = False + for j in range(len(copy[i])): + for k in range(j + 1, len(copy[i])): + if copy[i][j].name == copy[i][k].name: + items_to_remove.add(copy[i]) + is_brake = True + break + if is_brake: + break + if is_brake: + continue + for j in range(len(copy)): + if j != i: + if len(copy[i]) < len(copy[j]): + if set(copy[i]).issubset(set(copy[j])): + items_to_remove.add(copy[j]) + for item in items_to_remove: + self.constraints.remove(item) + + def clean_hash_table(self, combination_matrix, t_value): + for constraint in self.constraints: + if len(constraint) > t_value: + continue + parameters_in_constraint = [] + for pair in constraint: + parameters_in_constraint.append(pair.name) + for c in itertools.combinations(range(len(self.data)), t_value): + if set(parameters_in_constraint).issubset(c): + value_array = [] + counter = 0 + for value in c: + if value == constraint[counter].name: + value_array.append([constraint[counter].value]) + if (counter + 1) != len(constraint): + counter += 1 + else: + value_array.append(list(range(0, self.data[value]))) + for key in itertools.product(*value_array): + combination_matrix.del_cell(c, key) + + def clean_data_matrix(self, data_matrix, parameter=None): + if parameter is None: + for constraint in self.constraints: + if len(constraint) == 1: + constraint = constraint[0] + data_matrix[constraint.name].remove(constraint.value) + else: + parameter_constraint = self.parameters[parameter["name"]].constraints[parameter["value"]] + for constraint in parameter_constraint: + constraint = constraint[0] + try: + data_matrix[constraint.name].remove(constraint.value) + except ValueError: + # this value was already deleted + pass diff --git a/optional_plugins/varianter_cit/avocado_varianter_cit/__init__.py b/optional_plugins/varianter_cit/avocado_varianter_cit/__init__.py index 018f00a11d170b16d311c24894eae67a4cfb2cb9..5a6df9efeb8b98b8442a94acd555e6e315b609ad 100644 --- a/optional_plugins/varianter_cit/avocado_varianter_cit/__init__.py +++ b/optional_plugins/varianter_cit/avocado_varianter_cit/__init__.py @@ -12,12 +12,9 @@ # Bestoun S. Ahmed # Cleber Rosa -import configparser -import copy -import itertools import os -import random import sys +import logging from avocado.core import exit_codes from avocado.core import varianter @@ -25,6 +22,8 @@ from avocado.core.output import LOG_UI from avocado.core.plugin_interfaces import CLI from avocado.core.plugin_interfaces import Varianter from avocado.core.tree import TreeNode +from avocado_varianter_cit.Cit import Cit, LOG +from avocado_varianter_cit.Parser import Parser class VarianterCitCLI(CLI): @@ -48,11 +47,11 @@ class VarianterCitCLI(CLI): cit.add_argument('--cit-order-of-combinations', metavar='ORDER', type=int, default=2, help=("Order of combinations. Defaults to " - "%(default)s, maximum number is specific " - "to parameter file content")) + "%(default)s, maximum number is 6")) def run(self, args): - pass + if getattr(args, "varianter_debug", False): + LOG.setLevel(logging.DEBUG) class VarianterCit(Varianter): @@ -66,6 +65,10 @@ class VarianterCit(Varianter): def initialize(self, args): self.variants = None + order = args.cit_order_of_combinations + if order > 6: + LOG_UI.error("The order of combinations is bigger then 6") + self.error_exit(args) cit_parameter_file = getattr(args, "cit_parameter_file", None) if cit_parameter_file is None: @@ -77,18 +80,22 @@ class VarianterCit(Varianter): "is not readable", cit_parameter_file) self.error_exit(args) - config = configparser.ConfigParser() try: - config.read(cit_parameter_file) + parameters, constraints = Parser.parse(open(cit_parameter_file)) except Exception as details: LOG_UI.error("Cannot parse parameter file: %s", details) self.error_exit(args) - parameters = [(key, value.split(', ')) - for key, value in config.items('parameters')] - order = args.cit_order_of_combinations - cit = Cit(parameters, order) - self.headers, self.variants = cit.combine() + input_data = [parameter.get_size() for parameter in parameters] + + cit = Cit(input_data, order, constraints) + final_list = cit.compute() + self.headers = [parameter.name for parameter in parameters] + results = [[parameters[j].values[final_list[i][j]] for j in range(len(final_list[i]))] + for i in range(len(final_list))] + self.variants = [] + for combination in results: + self.variants.append(dict(zip(self.headers, combination))) @staticmethod def error_exit(args): @@ -140,163 +147,3 @@ class VarianterCit(Varianter): out.extend(varianter.variant_to_str(variant, variants - 1, kwargs, False)) return "\n".join(out) - - -class Cit: - - MATRIX_ROW_SIZE = 20 - MAX_ITERATIONS = 15 - - def __init__(self, parameters, order): - # Parameters come as ('key', ['value1', 'value2', 'value3']) - self.parameters = parameters - # Length (number of values) for each parameter - self.parameters_length = [len(param[1]) for param in self.parameters] - # Order of combinations - self.order = min(order, len(parameters)) - self.hash_table = {} - - def combine(self): - """ - Computes the combination of parameters - - :returns: headers (list of parameters keys) and combinations (list of - dictionaries. Each dictionary represents a combination of - parameters. - :rtype: tuple - """ - self.create_interaction_hash_table() - final_list = self.create_final_list() - - headers = [item[0] for item in self.parameters] - result = [[self.parameters[i][1][combination[i]] - for i in range(len(combination))] - for combination in final_list] - combinations = [] - for combination in result: - combinations.append(dict(zip(headers, combination))) - - return headers, combinations - - def create_interaction_hash_table(self): - for c in itertools.combinations(range(len(self.parameters_length)), self.order): - self.hash_table[c] = self.get_iteration(c) - - def create_final_list(self): - final_list = [] - while len(self.hash_table) != 0: - iterations = 0 - previous_test_case = [] - previous_remove_list = {} - previous_weight = 0 - - while iterations < self.MAX_ITERATIONS: - max_width = len(self.hash_table) - matrix = self.create_random_matrix() - remove_list = {} - for i in matrix: - width = self.get_weight(i, remove_list) - if width == 0 or width <= previous_weight: - remove_list.clear() - continue - elif width == max_width: - final_list.append(i) - self.remove_from_hash_table(remove_list) - previous_test_case = [] - previous_remove_list = {} - continue - elif width > previous_weight: - previous_weight = width - previous_test_case = i - previous_remove_list = dict(remove_list) - remove_list.clear() - iterations += 1 - if len(previous_test_case) != 0: - previous_remove_list = self.neighborhood_search( - previous_test_case, previous_weight, - max_width, previous_remove_list) - final_list.append(previous_test_case) - self.remove_from_hash_table(previous_remove_list) - return final_list - - def get_iteration(self, parameter_combination): - parameters_array = [] - for c in parameter_combination: - array = range(self.parameters_length[c]) - parameters_array.append(array) - iterations = {} - for i in itertools.product(*parameters_array): - iterations[i] = tuple(i) - return iterations - - def get_weight(self, test_case, remove_list): - weight = 0 - for i in self.hash_table: - iteration = tuple(test_case[j] for j in i) - try: - value = self.hash_table[i][iteration] - weight += 1 - remove_list[i] = value - except KeyError: - continue - - return weight - - def remove_from_hash_table(self, remove_list): - for i in remove_list: - del self.hash_table[i][remove_list[i]] - if len(self.hash_table[i]) == 0: - self.hash_table.pop(i) - remove_list.clear() - - def create_random_matrix(self): - matrix = [] - for _ in range(self.MATRIX_ROW_SIZE): - row = [] - for j in self.parameters_length: - row.append(random.randint(0, j-1)) - matrix.append(row) - return matrix - - def neighborhood_search(self, test_case, width, max_width, remove_list): - neighborhood = list(test_case) - neighborhood_remove_list = {} - for i in range(len(test_case)): - # neighborhood +1 - if (neighborhood[i] + 1) == self.parameters_length[i]: - neighborhood[i] = 0 - else: - neighborhood[i] += 1 - neighborhood_width = self.get_weight(neighborhood, neighborhood_remove_list) - if neighborhood_width > width: - width = neighborhood_width - remove_list = copy.deepcopy(neighborhood_remove_list) - del test_case[:] - for j in neighborhood: - test_case.append(j) - if neighborhood_width == max_width: - return remove_list - if neighborhood[i] == 0: - neighborhood[i] = self.parameters_length[i] - 1 - else: - neighborhood[i] -= 1 - - # neighborhood -1 - if neighborhood[i] == 0: - neighborhood[i] = self.parameters_length[i] - 1 - else: - neighborhood[i] -= 1 - neighborhood_width = self.get_weight(neighborhood, neighborhood_remove_list) - if neighborhood_width > width: - width = neighborhood_width - remove_list = copy.deepcopy(neighborhood_remove_list) - del test_case[:] - for j in neighborhood: - test_case.append(j) - if neighborhood_width == max_width: - return remove_list - if (neighborhood[i] + 1) == self.parameters_length[i]: - neighborhood[i] = 0 - else: - neighborhood[i] += 1 - return remove_list diff --git a/optional_plugins/varianter_cit/tests/test_functional.py b/optional_plugins/varianter_cit/tests/test_basic.py similarity index 78% rename from optional_plugins/varianter_cit/tests/test_functional.py rename to optional_plugins/varianter_cit/tests/test_basic.py index cb2fa3e1504bad0af3df13eeba81c3ca4fc6a940..59bcb4584bab09a89fd5e6879d6f1f18a6d8fb04 100644 --- a/optional_plugins/varianter_cit/tests/test_functional.py +++ b/optional_plugins/varianter_cit/tests/test_basic.py @@ -8,18 +8,23 @@ from avocado.utils import process from selftests import AVOCADO, BASEDIR, temp_dir_prefix -class Variants(unittest.TestCase): +class Basic(unittest.TestCase): def test_max_variants(self): os.chdir(BASEDIR) + params_path = os.path.join(BASEDIR, 'examples', + 'varianter_cit', 'test_params.cit') cmd_line = ( - '{0} variants --cit-order-of-combinations=5 ' - '--cit-parameter-file examples/varianter_cit/params.ini' - ).format(AVOCADO) + '{0} variants --cit-order-of-combinations=2 ' + '--cit-parameter-file {1}' + ).format(AVOCADO, params_path) result = process.run(cmd_line) lines = result.stdout.splitlines() - self.assertEqual(b'CIT Variants (216):', lines[0]) - self.assertEqual(217, len(lines)) + self.assertEqual(b'CIT Variants (9):', lines[0]) + self.assertEqual(10, len(lines)) + for i in range(1, len(lines)): + with self.subTest(combination=lines[i]): + self.assertIn(b"green", lines[i]) class Run(unittest.TestCase): @@ -31,7 +36,7 @@ class Run(unittest.TestCase): def test(self): os.chdir(BASEDIR) params_path = os.path.join(BASEDIR, 'examples', - 'varianter_cit', 'params.ini') + 'varianter_cit', 'test_params.cit') test_path = os.path.join(BASEDIR, 'examples', 'tests', 'cit_parameters.py') cmd_line = ( @@ -41,13 +46,7 @@ class Run(unittest.TestCase): '-- {3}' ).format(AVOCADO, self.tmpdir, params_path, test_path) result = process.run(cmd_line) - # all values of colors should be looked for at least once - self.assertIn(b"PARAMS (key=color, path=*, default=None) => 'black'", - result.stdout) - self.assertIn(b"PARAMS (key=color, path=*, default=None) => 'gold'", - result.stdout) - self.assertIn(b"PARAMS (key=color, path=*, default=None) => 'red'", - result.stdout) + # all values should be looked for at least once self.assertIn(b"PARAMS (key=color, path=*, default=None) => 'green'", result.stdout) # all values of shape should be looked for at least once diff --git a/optional_plugins/varianter_cit/tests/test_cit.py b/optional_plugins/varianter_cit/tests/test_cit.py new file mode 100644 index 0000000000000000000000000000000000000000..8989e4a85dc912afaf184b296ad496c88f1f0079 --- /dev/null +++ b/optional_plugins/varianter_cit/tests/test_cit.py @@ -0,0 +1,127 @@ +import random +import unittest +from copy import copy + +from avocado_varianter_cit.Cit import Cit +from avocado_varianter_cit.CombinationMatrix import CombinationMatrix +from avocado_varianter_cit.Parameter import Pair +from avocado_varianter_cit.Solver import Solver + + +class CitInitialization(unittest.TestCase): + + def test_initialization(self): + parameters = [3, 3, 3, 3] + constraints = {(Pair(0, 0), Pair(2, 0)), (Pair(0, 1), Pair(1, 1), Pair(2, 0)), (Pair(0, 2), Pair(3, 2))} + t_value = 2 + solver = Solver(parameters, constraints) + combination_matrix = CombinationMatrix(parameters, t_value) + solver.clean_hash_table(combination_matrix, t_value) + cit = Cit(parameters, t_value, constraints) + self.assertEqual(combination_matrix, cit.combination_matrix, "The initialization of cit algorithm is wrong") + + +class CitTests(unittest.TestCase): + + def setUp(self): + parameters = [3, 3, 3, 3] + constraints = {(Pair(0, 0), Pair(2, 0)), (Pair(0, 1), Pair(1, 1), Pair(2, 0)), (Pair(0, 2), Pair(3, 2))} + t_value = 2 + self.cit = Cit(parameters, t_value, constraints) + + def test_create_random_row_with_constraints(self): + for _ in range(0, 10): + row = self.cit.create_random_row_with_constraints() + with self.subTest(random_row=row): + self.assertTrue(self.cit.combination_matrix.is_valid_solution(row), "New random row is not valid") + + def test_compute_hamming_distance(self): + self.cit.final_matrix.append([1, 0, 1, 2]) + self.cit.final_matrix.append([2, 1, 1, 0]) + row = [2, 0, 3, 2] + expected_distance = 5 + self.assertEqual(expected_distance, self.cit.compute_hamming_distance(row), "Wrong hamming distance") + + def test_final_matrix_init(self): + combination_matrix = copy(self.cit.combination_matrix) + final_matrix = self.cit.final_matrix_init() + + expected_total_uncovered = 0 + expected_uncovered_rows = {} + self.assertEqual(expected_total_uncovered, self.cit.combination_matrix.total_uncovered, + "Final matrix don't cover all combinations") + self.assertEqual(expected_uncovered_rows, self.cit.combination_matrix.uncovered_rows, + "Final matrix don't cover all combination rows") + + for row in final_matrix: + combination_matrix.cover_solution_row(row) + self.assertEqual(expected_total_uncovered, self.cit.combination_matrix.total_uncovered, + "Final matrix don't cover all combinations but CIT thinks it does") + self.assertEqual(expected_uncovered_rows, self.cit.combination_matrix.uncovered_rows, + "Final matrix don't cover all combination rows but CIT thinks it does") + + def test_change_one_value_random(self): + final_matrix = self.cit.final_matrix_init() + row, row_index, column_index = self.cit.change_one_value(final_matrix) + self.assertNotEqual(final_matrix[row_index][column_index[0]], row[column_index[0]], "Value did not change") + row[column_index[0]] = final_matrix[row_index][column_index[0]] + self.assertEqual(final_matrix[row_index], row, "Different value was changed") + + def test_change_one_value_with_index(self): + final_matrix = self.cit.final_matrix_init() + expected_row_index = 2 + expected_column_index = 0 + row, row_index, column_index = self.cit.change_one_value(final_matrix, row_index=expected_row_index, + column_index=expected_column_index) + self.assertEqual(expected_column_index, column_index[0], "Column index is wrong") + self.assertEqual(expected_row_index, row_index, "Row index is wrong") + self.assertNotEqual(final_matrix[row_index][column_index[0]], row[column_index[0]], "Value did not change") + row[column_index[0]] = final_matrix[row_index][column_index[0]] + self.assertEqual(final_matrix[row_index], row, "Different value was changed") + + def test_change_one_column(self): + final_matrix = self.cit.final_matrix_init() + while self.cit.combination_matrix.total_uncovered == 0: + delete_row = final_matrix.pop(random.randint(0, len(final_matrix) - 1)) + self.cit.combination_matrix.uncover_solution_row(delete_row) + expected_total_covered_more_than_ones = self.cit.combination_matrix.total_covered_more_than_ones + expected_total_uncovered = self.cit.combination_matrix.total_uncovered + expected_uncovered_rows = copy(self.cit.combination_matrix.uncovered_rows) + row, row_index, column_index = self.cit.change_one_column(final_matrix) + self.assertEqual(expected_total_uncovered, self.cit.combination_matrix.total_uncovered, "Coverage was change") + self.assertEqual(expected_total_covered_more_than_ones, + self.cit.combination_matrix.total_covered_more_than_ones, + "Coverage was change") + self.assertEqual(expected_uncovered_rows, self.cit.combination_matrix.uncovered_rows, "Coverage was change") + self.assertNotEqual(final_matrix[row_index][column_index[0]], row[column_index[0]], "Value did not change") + row[column_index[0]] = final_matrix[row_index][column_index[0]] + self.assertEqual(final_matrix[row_index], row, "Different value was changed") + + def test_get_missing_combination_random(self): + final_matrix = self.cit.final_matrix_init() + while self.cit.combination_matrix.total_uncovered == 0: + delete_row = final_matrix.pop(random.randint(0, len(final_matrix) - 1)) + self.cit.combination_matrix.uncover_solution_row(delete_row) + combination_parameters, combination = self.cit.get_missing_combination_random() + self.assertEqual(0, self.cit.combination_matrix.hash_table[combination_parameters].hash_table[combination], + "Combination is already covered") + + def test_cover_missing_combination(self): + final_matrix = self.cit.final_matrix_init() + while self.cit.combination_matrix.total_uncovered == 0: + delete_row = final_matrix.pop(random.randint(0, len(final_matrix) - 1)) + self.cit.combination_matrix.uncover_solution_row(delete_row) + expected_total_covered_more_than_ones = self.cit.combination_matrix.total_covered_more_than_ones + expected_total_uncovered = self.cit.combination_matrix.total_uncovered + expected_uncovered_rows = copy(self.cit.combination_matrix.uncovered_rows) + row, row_index, parameters = self.cit.cover_missing_combination(final_matrix) + self.assertEqual(expected_total_uncovered, self.cit.combination_matrix.total_uncovered, "Coverage was change") + self.assertEqual(expected_total_covered_more_than_ones, + self.cit.combination_matrix.total_covered_more_than_ones, + "Coverage was change") + self.assertEqual(expected_uncovered_rows, self.cit.combination_matrix.uncovered_rows, "Coverage was change") + self.assertTrue(final_matrix[row_index][parameters[0]] != row[parameters[0]] or + final_matrix[row_index][parameters[1]] != row[parameters[1]], "Value did not change") + row[parameters[0]] = final_matrix[row_index][parameters[0]] + row[parameters[1]] = final_matrix[row_index][parameters[1]] + self.assertEqual(final_matrix[row_index], row, "Different value was changed") diff --git a/optional_plugins/varianter_cit/tests/test_combinationRow.py b/optional_plugins/varianter_cit/tests/test_combinationRow.py new file mode 100644 index 0000000000000000000000000000000000000000..923745bd40dc343b096d0176b2c4dbacdf7f0c44 --- /dev/null +++ b/optional_plugins/varianter_cit/tests/test_combinationRow.py @@ -0,0 +1,141 @@ +import unittest +from avocado_varianter_cit.CombinationRow import CombinationRow + + +class RowInitialization(unittest.TestCase): + + def test_combination_row_initialization(self): + """ + Test of proper initialization + """ + data = [3, 3, 3, 4] + parameters = (1, 3) + t_value = 2 + row = CombinationRow(data, t_value, parameters) + excepted_uncovered = 12 + excepted_covered_more_than_ones = 0 + excepted_hash_table = {(0, 0): 0, (0, 1): 0, (0, 2): 0, (0, 3): 0, + (1, 0): 0, (1, 1): 0, (1, 2): 0, (1, 3): 0, + (2, 0): 0, (2, 1): 0, (2, 2): 0, (2, 3): 0} + self.assertEqual(row.uncovered, excepted_uncovered, "Uncovered number is wrong.") + self.assertEqual(row.covered_more_than_ones, excepted_covered_more_than_ones, + "Covered_more_than_ones number is wrong.") + self.assertEqual(row.hash_table, excepted_hash_table, "Hash table is wrong.") + + +class CombinationRowTest(unittest.TestCase): + + def setUp(self): + self.data = [3, 3, 3, 4] + self.parameters = (1, 3) + self.t_value = 2 + self.row = CombinationRow(self.data, self.t_value, self.parameters) + + # Tests of cover_cell function + + def test_cover_cell_uncovered_value(self): + self.assertEqual(self.row.cover_cell((0, 0)), (-1, 0), "cover_cell return wrong values") + self.assertEqual(self.row.uncovered, 11, "cover_cell create wrong uncovered value") + self.assertEqual(self.row.covered_more_than_ones, 0, "cover_cell create wrong covered_more_than_ones value") + self.assertEqual(self.row.hash_table[(0, 0)], 1, "cover_cell don't cover values") + + def test_cover_cell_covered_value(self): + self.row.hash_table[(0, 0)] = 1 + self.row.uncovered = 11 + self.assertEqual(self.row.cover_cell((0, 0)), (0, 1), "cover_cell return wrong values") + self.assertEqual(self.row.uncovered, 11, "cover_cell create wrong uncovered value") + self.assertEqual(self.row.covered_more_than_ones, 1, "cover_cell create wrong covered_more_than_ones value") + self.assertEqual(self.row.hash_table[(0, 0)], 2, "cover_cell don't cover values") + + def test_cover_cell_cover_disabled_value(self): + self.row.hash_table[(0, 0)] = None + self.assertEqual(self.row.cover_cell((0, 0)), (0, 0), "cover_cell return wrong values") + self.assertEqual(self.row.uncovered, 12, "cover_cell create wrong uncovered value") + self.assertEqual(self.row.covered_more_than_ones, 0, "cover_cell create wrong covered_more_than_ones value") + self.assertEqual(self.row.hash_table[(0, 0)], None, "cover_cell change disabled value") + + # Tests of uncover_cell function + + def test_uncover_cell_uncovered_value(self): + self.assertEqual(self.row.uncover_cell((0, 0)), (0, 0), "uncover_cell return wrong values") + self.assertEqual(self.row.uncovered, 12, "uncover_cell create wrong uncovered value") + self.assertEqual(self.row.covered_more_than_ones, 0, "uncover_cell create wrong covered_more_than_ones value") + self.assertEqual(self.row.hash_table[(0, 0)], 0, "uncover_cell change uncovered value") + + def test_uncover_cell_covered_value(self): + self.row.hash_table[(0, 0)] = 1 + self.row.uncovered = 11 + self.assertEqual(self.row.uncover_cell((0, 0)), (1, 0), "uncover_cell return wrong values") + self.assertEqual(self.row.uncovered, 12, "uncover_cell create wrong uncovered value") + self.assertEqual(self.row.covered_more_than_ones, 0, "uncover_cell create wrong covered_more_than_ones value") + self.assertEqual(self.row.hash_table[(0, 0)], 0, "uncover_cell change uncovered value") + + def test_uncover_cell_covered_more_than_one_value(self): + self.row.hash_table[(0, 0)] = 2 + self.row.uncovered = 11 + self.row.covered_more_than_ones = 1 + self.assertEqual(self.row.uncover_cell((0, 0)), (0, -1), "uncover_cell return wrong values") + self.assertEqual(self.row.uncovered, 11, "uncover_cell create wrong uncovered value") + self.assertEqual(self.row.covered_more_than_ones, 0, "uncover_cell create wrong covered_more_than_ones value") + self.assertEqual(self.row.hash_table[(0, 0)], 1, "uncover_cell change uncovered value") + + def test_uncover_cell_disabled_value(self): + self.row.hash_table[(0, 0)] = None + self.assertEqual(self.row.uncover_cell((0, 0)), (0, 0), "uncover_cell return wrong values") + self.assertEqual(self.row.uncovered, 12, "uncover_cell create wrong uncovered value") + self.assertEqual(self.row.covered_more_than_ones, 0, "uncover_cell create wrong covered_more_than_ones value") + self.assertEqual(self.row.hash_table[(0, 0)], None, "uncover_cell change disabled value") + + # Test of completely_uncover function + + def test_completely_uncover(self): + self.row.hash_table[(0, 0)] = 1 + self.row.hash_table[(0, 1)] = 2 + self.row.hash_table[(0, 2)] = None + self.row.completely_uncover() + self.assertEqual(self.row.uncovered, 11, "completely_uncover create wrong uncovered value") + self.assertEqual(self.row.covered_more_than_ones, 0, + "completely_uncover create wrong covered_more_than_ones value") + self.assertEqual(self.row.hash_table[(0, 0)], 0, "completely_uncover don't uncover value") + self.assertEqual(self.row.hash_table[(0, 1)], 0, "completely_uncover don't uncover value") + self.assertEqual(self.row.hash_table[(0, 2)], None, "completely_uncover change disabled value") + + # Tests of del_cell function + + def test_del_cell_uncovered_value(self): + self.assertEqual(self.row.del_cell((0, 0)), -1, "del_cell return wrong values") + self.assertEqual(self.row.uncovered, 11, "del_cell create wrong uncovered value") + self.assertEqual(self.row.covered_more_than_ones, 0, "del_cell create wrong covered_more_than_ones value") + self.assertEqual(self.row.hash_table[(0, 0)], None, "del_cell don't disable value") + + def test_del_cell_disabled_value(self): + self.row.hash_table[(0, 0)] = None + self.row.uncovered = 11 + self.assertEqual(self.row.del_cell((0, 0)), 0, "del_cell return wrong values") + self.assertEqual(self.row.uncovered, 11, "del_cell create wrong uncovered value") + self.assertEqual(self.row.covered_more_than_ones, 0, "del_cell create wrong covered_more_than_ones value") + self.assertEqual(self.row.hash_table[(0, 0)], None, "del_cell don't disable value") + + # Tests of is_valid function + + def test_is_valid_valid(self): + self.assertEqual(self.row.is_valid((0, 0)), True, "is_valid return wrong values") + + def test_is_valid_invalid(self): + self.row.hash_table[(0, 0)] = None + self.assertEqual(self.row.is_valid((0, 0)), False, "is_valid return wrong values") + + # Test of get_all_uncovered_combinations function + + def test_get_all_uncovered_combinations(self): + self.row.hash_table[(0, 0)] = None + self.row.hash_table[(0, 1)] = 1 + self.row.hash_table[(0, 2)] = 2 + self.row.hash_table[(0, 3)] = 3 + ex = [(1, 0), (1, 1), (1, 2), (1, 3), + (2, 0), (2, 1), (2, 2), (2, 3)] + self.assertEqual(len(set(self.row.get_all_uncovered_combinations()).intersection(ex)), len(ex)) + + +if __name__ == '__main__': + unittest.main() diff --git a/optional_plugins/varianter_cit/tests/test_comninationMatrix.py b/optional_plugins/varianter_cit/tests/test_comninationMatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..4a5ddef890e90082e2c7f81d7237758a2c455500 --- /dev/null +++ b/optional_plugins/varianter_cit/tests/test_comninationMatrix.py @@ -0,0 +1,195 @@ +import unittest + +from avocado_varianter_cit.CombinationMatrix import CombinationMatrix +from avocado_varianter_cit.CombinationRow import CombinationRow + + +def combination_row_equals(row_1, row_2): + return (row_1.covered_more_than_ones == row_2.covered_more_than_ones and row_1.uncovered == row_2.uncovered + and row_1.hash_table == row_2.hash_table) + + +class MatrixInitialization(unittest.TestCase): + + def test_combination_matrix_initialization(self): + """ + Test of proper initialization + """ + data = [3, 3, 3, 4] + t_value = 2 + matrix = CombinationMatrix(data, t_value) + excepted_uncovered = 63 + excepted_covered_more_than_ones = 0 + excepted_row_size = 6 + excepted_hash_table = {(0, 1): CombinationRow(data, t_value, (0, 1)), + (0, 2): CombinationRow(data, t_value, (0, 2)), + (0, 3): CombinationRow(data, t_value, (0, 3)), + (1, 2): CombinationRow(data, t_value, (1, 2)), + (1, 3): CombinationRow(data, t_value, (1, 3)), + (2, 3): CombinationRow(data, t_value, (2, 3))} + self.assertEqual(matrix.total_uncovered, excepted_uncovered, "Total uncovered number is wrong.") + self.assertEqual(matrix.total_covered_more_than_ones, excepted_covered_more_than_ones, + "Total uovered_more_than_ones number is wrong.") + self.assertEqual(len(matrix.hash_table), excepted_row_size, "Matrix has wrong row size") + self.assertEqual(len(matrix.uncovered_rows), excepted_row_size, "Matrix has wrong uncovered row size") + for key in matrix.hash_table: + with self.subTest(combination=key): + self.assertTrue(combination_row_equals(matrix.hash_table[key], excepted_hash_table[key])) + + +class CombinationMatrixTest(unittest.TestCase): + + def setUp(self): + self.data = [3, 3, 3, 4] + self.t_value = 2 + self.matrix = CombinationMatrix(self.data, self.t_value) + self.excepted_hash_table = {(0, 1): CombinationRow(self.data, self.t_value, (0, 1)), + (0, 2): CombinationRow(self.data, self.t_value, (0, 2)), + (0, 3): CombinationRow(self.data, self.t_value, (0, 3)), + (1, 2): CombinationRow(self.data, self.t_value, (1, 2)), + (1, 3): CombinationRow(self.data, self.t_value, (1, 3)), + (2, 3): CombinationRow(self.data, self.t_value, (2, 3))} + + def test_cover_solution_row(self): + solution_row = [1, 0, 2, 3] + excepted_uncovered = 57 + excepted_covered_more_than_ones = 0 + excepted_uncovered_row_size = 6 + self.excepted_hash_table[0, 1].cover_cell((1, 0)) + self.excepted_hash_table[0, 2].cover_cell((1, 2)) + self.excepted_hash_table[0, 3].cover_cell((1, 3)) + self.excepted_hash_table[1, 2].cover_cell((0, 2)) + self.excepted_hash_table[1, 3].cover_cell((0, 3)) + self.excepted_hash_table[2, 3].cover_cell((2, 3)) + self.matrix.cover_solution_row(solution_row) + self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") + self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, + "Total uovered_more_than_ones number is wrong.") + self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), + "Matrix has wrong uncovered row size") + for key in self.matrix.hash_table: + with self.subTest(combination=key): + self.assertTrue(combination_row_equals(self.matrix.hash_table[key], self.excepted_hash_table[key])) + solution_row = [0, 0, 2, 3] + self.matrix.cover_solution_row(solution_row) + solution_row = [0, 1, 2, 3] + self.matrix.cover_solution_row(solution_row) + solution_row = [0, 2, 2, 3] + self.matrix.cover_solution_row(solution_row) + solution_row = [1, 1, 2, 3] + self.matrix.cover_solution_row(solution_row) + solution_row = [1, 2, 2, 3] + self.matrix.cover_solution_row(solution_row) + solution_row = [2, 0, 2, 3] + self.matrix.cover_solution_row(solution_row) + solution_row = [2, 1, 2, 3] + self.matrix.cover_solution_row(solution_row) + solution_row = [2, 2, 2, 3] + self.matrix.cover_solution_row(solution_row) + excepted_uncovered_row_size = 5 + excepted_uncovered = 41 + excepted_covered_more_than_ones = 13 + self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") + self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, + "Total uovered_more_than_ones number is wrong.") + self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), + "Matrix has wrong uncovered row size") + + def test_cover_combination(self): + solution_row = [1, 0, 2, 3] + self.matrix.cover_combination(solution_row, (1, 0)) + excepted_uncovered_row_size = 6 + excepted_uncovered = 58 + excepted_covered_more_than_ones = 0 + self.excepted_hash_table[0, 1].cover_cell((1, 0)) + self.excepted_hash_table[0, 2].cover_cell((1, 2)) + self.excepted_hash_table[0, 3].cover_cell((1, 3)) + self.excepted_hash_table[1, 2].cover_cell((0, 2)) + self.excepted_hash_table[1, 3].cover_cell((0, 3)) + self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") + self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, + "Total uovered_more_than_ones number is wrong.") + self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), + "Matrix has wrong uncovered row size") + for key in self.matrix.hash_table: + with self.subTest(combination=key): + self.assertTrue(combination_row_equals(self.matrix.hash_table[key], self.excepted_hash_table[key])) + + self.matrix.cover_combination(solution_row, (1, 0)) + excepted_covered_more_than_ones = 5 + self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") + self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, + "Total uovered_more_than_ones number is wrong.") + self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), + "Matrix has wrong uncovered row size") + + def test_uncover_solution_row(self): + solution_row = [1, 0, 2, 3] + self.matrix.cover_solution_row(solution_row) + self.matrix.uncover_solution_row(solution_row) + excepted_uncovered_row_size = 6 + excepted_uncovered = 63 + excepted_covered_more_than_ones = 0 + self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") + self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, + "Total uovered_more_than_ones number is wrong.") + self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), + "Matrix has wrong uncovered row size") + for key in self.matrix.hash_table: + with self.subTest(combination=key): + self.assertTrue(combination_row_equals(self.matrix.hash_table[key], self.excepted_hash_table[key])) + + self.matrix.cover_solution_row(solution_row) + self.matrix.cover_solution_row(solution_row) + self.excepted_hash_table[0, 1].cover_cell((1, 0)) + self.excepted_hash_table[0, 2].cover_cell((1, 2)) + self.excepted_hash_table[0, 3].cover_cell((1, 3)) + self.excepted_hash_table[1, 2].cover_cell((0, 2)) + self.excepted_hash_table[1, 3].cover_cell((0, 3)) + self.excepted_hash_table[2, 3].cover_cell((2, 3)) + self.matrix.uncover_solution_row(solution_row) + excepted_uncovered_row_size = 6 + excepted_uncovered = 57 + excepted_covered_more_than_ones = 0 + self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") + self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, + "Total uovered_more_than_ones number is wrong.") + self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), + "Matrix has wrong uncovered row size") + for key in self.matrix.hash_table: + with self.subTest(combination=key): + self.assertTrue(combination_row_equals(self.matrix.hash_table[key], self.excepted_hash_table[key])) + + def test_uncover_combination(self): + solution_row = [1, 0, 2, 3] + self.matrix.cover_solution_row(solution_row) + excepted_uncovered_row_size = 6 + excepted_uncovered = 62 + excepted_covered_more_than_ones = 0 + self.excepted_hash_table[2, 3].cover_cell((2, 3)) + self.matrix.uncover_combination(solution_row, (0, 1)) + self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") + self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, + "Total uovered_more_than_ones number is wrong.") + self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), + "Matrix has wrong uncovered row size") + for key in self.matrix.hash_table: + with self.subTest(combination=key): + self.assertTrue(combination_row_equals(self.matrix.hash_table[key], self.excepted_hash_table[key])) + + def test_uncover(self): + solution_row = [1, 0, 2, 3] + self.matrix.cover_solution_row(solution_row) + self.matrix.cover_solution_row(solution_row) + excepted_uncovered_row_size = 6 + excepted_uncovered = 63 + excepted_covered_more_than_ones = 0 + self.matrix.uncover() + self.assertEqual(excepted_uncovered, self.matrix.total_uncovered, "Total uncovered number is wrong.") + self.assertEqual(excepted_covered_more_than_ones, self.matrix.total_covered_more_than_ones, + "Total uovered_more_than_ones number is wrong.") + self.assertEqual(excepted_uncovered_row_size, len(self.matrix.uncovered_rows), + "Matrix has wrong uncovered row size") + for key in self.matrix.hash_table: + with self.subTest(combination=key): + self.assertTrue(combination_row_equals(self.matrix.hash_table[key], self.excepted_hash_table[key])) diff --git a/optional_plugins/varianter_cit/tests/test_solver.py b/optional_plugins/varianter_cit/tests/test_solver.py new file mode 100644 index 0000000000000000000000000000000000000000..b9de8b499e28a56cee548af8caf5792a0ea3f404 --- /dev/null +++ b/optional_plugins/varianter_cit/tests/test_solver.py @@ -0,0 +1,134 @@ +import unittest + +from avocado_varianter_cit.Solver import Solver +from avocado_varianter_cit.Parameter import Pair + + +class SolverTest(unittest.TestCase): + + """ + Test for compute_constraints function + """ + + def test_compute_constraints_without_secret_constraint(self): + """ + Test that, function shouldn't change constraints if there isn't any secret constraint + + """ + parameters = [3, 3, 3] + constraints = {(Pair(0, 0), Pair(2, 0)), (Pair(0, 1), Pair(1, 1)), (Pair(1, 0), Pair(2, 0))} + solver = Solver([], []) + solver.data = parameters + solver.constraints = constraints + solver.read_constraints() + solver.compute_constraints() + self.assertEqual(solver.constraints, constraints, "compute_constraints change constraints without secret " + "constraint") + + def test_compute_constraints_new_constraint_with_more_than_one_value_for_one_parameter(self): + """ + Test that, function shouldn't change constraints. It founds new constraint, + but the constraint has one parameter with two values. This means that it isn't secreted constraint. + """ + parameters = [3, 3, 3] + constraints = {(Pair(0, 0), Pair(1, 0)), (Pair(0, 1), Pair(1, 1)), (Pair(1, 2), Pair(2, 0))} + solver = Solver([], []) + solver.data = parameters + solver.constraints = constraints + solver.read_constraints() + solver.compute_constraints() + self.assertEqual(solver.constraints, constraints, "compute_constraints change constraints without secret " + "constraint") + + def test_compute_constraints_detect_invalid_constraints(self): + """ + Test that, function should raise an exception, if there is invalid_constraints + """ + parameters = [3, 3, 3] + constraints = {(Pair(0, 0),), (Pair(0, 1),), (Pair(0, 2),)} + solver = Solver([], []) + solver.data = parameters + solver.constraints = constraints + solver.read_constraints() + self.assertRaises(ValueError, solver.compute_constraints) + + def test_compute_constraints_with_one_secrete_constraint(self): + """ + Test that, function should find new constraint + """ + parameters = [3, 3, 3, 3] + constraints = {(Pair(0, 0), Pair(2, 0)), (Pair(0, 1), Pair(1, 1), Pair(2, 0)), (Pair(0, 2), Pair(3, 2))} + solver = Solver([], []) + solver.data = parameters + solver.constraints = constraints + solver.read_constraints() + solver.compute_constraints() + expectation = {(Pair(0, 0), Pair(2, 0)), (Pair(0, 1), Pair(1, 1), Pair(2, 0)), (Pair(0, 2), Pair(3, 2)), + (Pair(1, 1), Pair(2, 0), Pair(3, 2))} + self.assertEqual(solver.constraints, expectation, "compute_constraints didn't find secret constraints") + + # Test for simplify_constraints function + + def test_simplify_constraints_constraints_without_simplification(self): + """ + Test that, function do not delete important constraints + """ + parameters = [3, 3, 3, 3] + constraints = {(Pair(0, 0), Pair(2, 0)), (Pair(0, 1), Pair(1, 1), Pair(2, 0)), (Pair(0, 2), Pair(3, 2))} + solver = Solver([], []) + solver.data = parameters + solver.constraints = constraints + solver.read_constraints() + solver.simplify_constraints() + self.assertEqual(solver.constraints, constraints, "simplify_constraints deleted some important constraints") + + def test_simplify_constraints_constraints_with_simplification(self): + """ + Test that, function do not delete important constraints + """ + parameters = [3, 3, 3, 3] + constraints = {(Pair(0, 0), Pair(2, 0)), (Pair(0, 1), Pair(2, 0), Pair(1, 1)), + (Pair(0, 2), Pair(2, 0)), (Pair(2, 0),)} + solver = Solver([], []) + solver.data = parameters + solver.constraints = constraints + solver.read_constraints() + solver.simplify_constraints() + expectation = {(Pair(2, 0),)} + self.assertEqual(solver.constraints, expectation, "simplify_constraints deleted some important constraints") + + # Test of Minimum forbidden tuple algorithm + + def test_solver_without_secrete_constraints(self): + """ + Test that, solver didn't change constraints if there isn't any secret constraint + """ + parameters = [3, 3, 3] + constraints = {(Pair(0, 0), Pair(2, 0)), (Pair(0, 1), Pair(1, 1)), (Pair(1, 0), Pair(2, 0))} + solver = Solver(parameters, constraints) + self.assertEqual(solver.constraints, constraints, "solver change constraints without secret " + "constraint") + + def test_solver_constraints_without_simplification(self): + """ + Test that, solver do not delete important constraints + """ + parameters = [3, 3, 3, 3] + constraints = {(Pair(0, 0), Pair(2, 0)), (Pair(0, 1), Pair(1, 1), Pair(2, 0)), (Pair(0, 2), Pair(3, 2))} + solver = Solver(parameters, constraints) + self.assertEqual(solver.constraints, constraints, "solver deleted some important constraints") + + def test_solver_constraints_with_simplification_and_secrete(self): + """ + Test that, Minimum forbidden tuple algorithm can find and simplify constraints + """ + parameters = [3, 3, 3, 3] + constraints = {(Pair(0, 0), Pair(1, 0)), (Pair(0, 0), Pair(1, 2)), (Pair(0, 1), Pair(3, 0)), + (Pair(0, 2), Pair(3, 0)), (Pair(1, 1), Pair(3, 0))} + solver = Solver(parameters, constraints) + expectation = {(Pair(0, 0), Pair(1, 0)), (Pair(0, 0), Pair(1, 2)), (Pair(3, 0),)} + self.assertEqual(solver.constraints, expectation, "solver can not compute and simplify constraints") + + +if __name__ == '__main__': + unittest.main() diff --git a/optional_plugins/varianter_cit/tests/test_unit.py b/optional_plugins/varianter_cit/tests/test_unit.py deleted file mode 100644 index 5f70e6ff9529a19382ec55108289dc9bfe8e5dc0..0000000000000000000000000000000000000000 --- a/optional_plugins/varianter_cit/tests/test_unit.py +++ /dev/null @@ -1,41 +0,0 @@ -import unittest - -from avocado_varianter_cit import Cit - - -class CitTest(unittest.TestCase): - - PARAMS = [('key1', ['x1', 'x2', 'x3']), ('key2', ['y1', 'y2', 'y3']), - ('key3', ['z1', 'z2', 'z3']), ('key4', ['w1', 'w2', 'w3'])] - - def test_orders(self): - for i in range(len(self.PARAMS)): - cit = Cit(self.PARAMS, i+1) - headers, _ = cit.combine() - # headers should always be the same, no matter the order - # of combinations - self.assertEqual(headers, ['key1', 'key2', 'key3', 'key4']) - - def test_number_of_combinations(self): - # the algorithm doesn't allow us to know beforehand, for a - # reason, the precise number of combinations that will be - # computed. still, we can check for the minimum number of - # combinations that should be produced - cit = Cit(self.PARAMS, 1) - self.assertEqual(len(cit.combine()[1]), 3) - cit = Cit(self.PARAMS, 2) - self.assertGreaterEqual(len(cit.combine()[1]), 9) - cit = Cit(self.PARAMS, 3) - self.assertGreaterEqual(len(cit.combine()[1]), 27) - - def test_max_order(self): - # test that with a order equal or larger than the number of - # keys, we have a predictable number of combinations - cit = Cit(self.PARAMS, 4) - self.assertEqual(len(cit.combine()[1]), 81) - cit = Cit(self.PARAMS, 10) - self.assertEqual(len(cit.combine()[1]), 81) - - -if __name__ == '__main__': - unittest.main()