Commit Diff


commit - c3bf1e83ca58b6e1c539af029942c9b49a3df6ef
commit + 493b34178ab94af91370be67931f4961a736f19d
blob - 71f14b87f91feebf8d675ca16719a501977b82b1
blob + d76abfb73d37abc848e9d08d869aae0da1bfd9a4
--- generate.py
+++ generate.py
@@ -14,6 +14,55 @@ STATUS_COMPILE_ERROR = "CompileError"
 STATUS_RUNTIME_ERROR = "RuntimeError"
 STATUS_TIMEOUT = "Timeout"
 STATUS_IGNORED = "Ignored"
+
+# Source: https://mull.readthedocs.io/en/latest/SupportedMutations.html
+
+MUTATION_TYPES = {"cxx_add_assign_to_sub_assign": {"old": "+=", "new": "-="},
+				"cxx_add_to_sub": {"old": "+", "new": "-"},
+				"cxx_and_assign_to_or_assign": {"old": "&=", "new": "|="},
+				"cxx_and_to_or": {"old": "&", "new": "|"},
+				"cxx_assign_const": "Replaces ‘a = b’ with ‘a = 42’",
+				"cxx_bitwise_not_to_noop": "Replaces ~x with x",
+				"cxx_div_assign_to_mul_assign": {"old": "/=", "new": "*="},
+				"cxx_div_to_mul": {"old": "/", "new": "*"},
+				"cxx_eq_to_ne": {"old": "==", "new": "!="},
+				"cxx_ge_to_gt": {"old": ">=", "new": ">"},
+				"cxx_ge_to_lt": {"old": ">=", "new": "<"},
+				"cxx_gt_to_ge": {"old": ">", "new": ">="},
+				"cxx_gt_to_le": {"old": ">", "new": "<="},
+				"cxx_init_const": "Replaces ‘T a = b’ with ‘T a = 42’",
+				"cxx_le_to_gt": {"old": "<=", "new": ">"},
+				"cxx_le_to_lt": {"old": "<=", "new": "<"},
+				"cxx_logical_and_to_or": {"old": "&&", "new": "||"},
+				"cxx_logical_or_to_and": {"old": "||", "new": "&&"},
+				"cxx_lshift_assign_to_rshift_assign": {"old": "<<=", "new": ">>="},
+				"cxx_lshift_to_rshift": {"old": "<<", "new": ">>"},
+				"cxx_lt_to_ge": {"old": "<", "new": ">="},
+				"cxx_lt_to_le": {"old": "<", "new": "<="},
+				"cxx_minus_to_noop": "Replaces -x with x",
+				"cxx_mul_assign_to_div_assign": {"old": "*=", "new": "/="},
+				"cxx_mul_to_div": {"old": "*", "new": "/"},
+				"cxx_ne_to_eq": {"old": "!=", "new": "=="},
+				"cxx_or_assign_to_and_assign": {"old": "|=", "new": "&="},
+				"cxx_or_to_and": {"old": "|", "new": "&"},
+				"cxx_post_dec_to_post_inc": "Replaces x– with x++",
+				"cxx_post_inc_to_post_dec": "Replaces x++ with x–",
+				"cxx_pre_dec_to_pre_inc": "Replaces –x with ++x",
+				"cxx_pre_inc_to_pre_dec": "Replaces ++x with –x",
+				"cxx_rem_assign_to_div_assign": {"old": "%=", "new": "/="},
+				"cxx_rem_to_div": {"old": "%", "new": "/"},
+				"cxx_remove_negation": "Replaces !a with a",
+				"cxx_rshift_assign_to_lshift_assign": {"old": ">>=", "new": "<<="},
+				"cxx_rshift_to_lshift": {"old": "<<", "new": ">>"},
+				"cxx_sub_assign_to_add_assign": {"old": "-=", "new": "+="},
+				"cxx_sub_to_add": {"old": "-", "new": "+"},
+				"cxx_xor_assign_to_or_assign": {"old": "^=", "new": "|="},
+				"cxx_xor_to_or": {"old": "^", "new": "|"},
+				"negate_mutator": "Negates conditionals !x to x and x to !x",
+				"remove_void_function_mutator": "Removes calls to a function returning void",
+				"replace_call_mutator": "Replaces call to a function with 42",
+				"scalar_value_mutator": "Replaces zeros with 42, and non-zeros with 0",
+				}
 
 LIST_STATUSES = [STATUS_KILLED,
                 STATUS_SURVIVED,
@@ -25,6 +74,8 @@ LIST_STATUSES = [STATUS_KILLED,
 
 SUPPORTED_SCHEMA_VERSIONS = ["1.0"]
 
+INVALID_SYMBOLS = "<>:\"/\|?*. "
+
 DEFAULT_TEXT_TEMPLATE = """
 {% set files = json_data['files'] %}
 
@@ -263,7 +314,14 @@ Generated by <a href="https://github.com/ligurio/py-mu
 </body>
 </html>
 """
+
+def escape_invalid_symbols(string, invalid_symbols):
+	escaped_string = string
+	for char in invalid_symbols:
+		escaped_string = escaped_string.replace(char, '-')
 
+	return escaped_string
+
 def print_stdout(json_data):
 	schema_version = json_data.get("schemaVersion", None)
 	print("Version:", schema_version)
@@ -280,12 +338,7 @@ def print_stdout(json_data):
 			print("\t{}: {}".format(mutatorName, status))
 
 
-def render_template(json_data, template):
-	files_mutant_statuses = []
-	for file_name, properties in json_data.get("files", None).items():
-		files_mutant_statuses.append({file_name: file_mutant_statuses(properties)})
-	report_mutant_statuses = sum_statuses(files_mutant_statuses)
-
+def render_template(json_data, report_mutant_statuses, template):
 	t = Template(template)
 	time = datetime.datetime.now()
 
@@ -312,7 +365,7 @@ def dict_statuses():
 	return statuses
 
 
-def file_mutant_statuses(json_data):
+def file_mutant_statuses(file_json_data):
 	"""
 	json_data: a dict that includes dicts "file" described in
 	'mutation-elements' schema.
@@ -321,7 +374,7 @@ def file_mutant_statuses(json_data):
 	a number mutants with that status.
 	"""
 
-	mutants = json_data.get("mutants", None)
+	mutants = file_json_data.get("mutants", None)
 	statuses = dict_statuses()
 	for mutant in mutants:
 		status = mutant.get("status", None)
@@ -359,7 +412,118 @@ def sum_statuses(list_of_statuses_per_file):
 	
 	return total_num_statuses
 
+def file_dict_generator(json_report):
+	files_dict = json_report.get("files", None)
+	for file_path, properties_dict in files_dict.items():
+		yield (file_path, properties_dict)
 
+def mutant_dict_generator(file_dict):
+    """
+	Generate dicts with structure like below.
+        {
+          "id": "cxx_post_inc_to_post_dec",
+          "location": {
+            "end": {
+              "column": 14,
+              "line": 97
+            },
+            "start": {
+              "column": 2,
+              "line": 97
+            }
+          },
+          "mutatorName": "Replaced x++ with x--",
+          "replacement": "--",
+          "status": "Killed"
+        },
+	"""
+    mutants = file_dict.get("mutants", None)
+    for mutant in mutants:
+        yield mutant
+
+def generate_patch_with_mutant(source_file_path, mutant_dict):
+	"""
+	Process a dict with sctructure like below and generate a patch with mutant.
+
+        {
+          "id": "cxx_post_inc_to_post_dec",
+          "location": {
+            "end": {
+              "column": 14,
+              "line": 97
+            },
+            "start": {
+              "column": 2,
+              "line": 97
+            }
+          },
+          "mutatorName": "Replaced x++ with x--",
+          "replacement": "--",
+          "status": "Killed"
+        },
+	"""
+
+	replacement = mutant_dict.get("replacement", None)
+	if replacement == "":
+		print("replacement is not found")
+		return ""
+
+	location = mutant_dict.get("location", None)
+
+	loc_start = location.get("start", None)
+	loc_end = location.get("end", None)
+
+	start_column = loc_start.get("column", None)
+	start_line = loc_start.get("line", None)
+	end_column = loc_end.get("column", None)
+	end_line = loc_end.get("line", None)
+
+	if end_line != start_line:
+		print("Start line and end line is not equal, it is unsupported")
+		raise(Exception)
+
+	print(source_file_path)
+	source_code_lines = []
+	with open(source_file_path, "r") as source_file:
+		source_code_lines = [line.rstrip() for line in source_file]
+
+	line_no = start_line - 1
+	orig_line = source_code_lines[line_no]
+	changed_line = mutate_string(orig_line, replacement, start_column, end_column)
+	print("'{}' --> '{}'".format(orig_line, changed_line))
+	patch_lines = []
+
+	patch_lines.append("--- {}".format(source_file_path))
+	patch_line_start = 0
+	patch_line_end = len(source_code_lines)
+	if start_line > 3:
+		patch_line_start = start_line - 3
+	if end_line < len(source_code_lines) - 3:
+		patch_line_end = end_line + 3
+
+	patch_lines.append("+++ {}".format(source_file_path))
+	patch_lines.append("@@ -{},{} +{},{} @@".format(patch_line_start, start_column, patch_line_end, end_column))
+	for l in range(patch_line_start, start_line - 1):
+		patch_lines.append(source_code_lines[l])
+	patch_lines.append("- {}".format(orig_line))
+	patch_lines.append("+ {}".format(changed_line))
+	for l in range(end_line, patch_line_end):
+		patch_lines.append(source_code_lines[l])
+
+	return "\n".join(patch_lines)
+
+
+def mutate_string(string, replacement, start_column, end_column):
+	print("{} {}, {}".format(replacement, start_column, end_column))
+	print(string)
+	length = end_column - start_column
+	carets = "^" * length
+	placeholder = " " * start_column
+	print(placeholder, carets)
+
+	return string
+	
+
 if __name__ == "__main__":
 	parser = argparse.ArgumentParser()
 	parser.add_argument("--data", dest="data_path", default="",
@@ -368,6 +532,8 @@ if __name__ == "__main__":
 			help="Path to a generated report")
 	parser.add_argument("--html", dest="html", action="store_true",
 			help="Use HTML in a generated report")
+	parser.add_argument("--with-patches", dest="with_patches", action="store_true",
+			help="Generate files with patches for each mutant")
 	args = parser.parse_args()
 
 	if not os.path.exists(args.data_path):
@@ -388,7 +554,32 @@ if __name__ == "__main__":
 	if args.html:
 		template = DEFAULT_HTML_TEMPLATE
 
-	report_data = render_template(json_data, template)
+	files_mutant_statuses = []
+	mutant_idx = 0
+	for file_path, properties_dict in file_dict_generator(json_data):
+		files_mutant_statuses.append({file_path: file_mutant_statuses(properties_dict)})
+		if not os.path.exists(file_path):
+			print("{} is not found".format(file_path))
+			continue
+		escaped_source_file_basename = ""
+		if args.with_patches:
+			source_file_basename = os.path.basename(file_path)
+			escaped_source_file_basename = escape_invalid_symbols(source_file_basename, INVALID_SYMBOLS)
+		for mutant_dict in mutant_dict_generator(properties_dict):
+			mutant_idx += 1
+			mutant_status = mutant_dict.get("status", "unknown").lower()
+			patch_buf = generate_patch_with_mutant(file_path, mutant_dict)
+			if args.with_patches and patch_buf != "":
+				patch_file_name = "{:05d}-{}-{}.patch".format(mutant_idx, mutant_status, escaped_source_file_basename)
+				with open(patch_file_name, "w") as patch_file:
+					patch_file.write(patch_buf)
+
+	report_mutant_statuses = sum_statuses(files_mutant_statuses)
+
+	t = Template(template)
+	time = datetime.datetime.now()
+
+	report_data = render_template(json_data, report_mutant_statuses, template)
 	if args.report_path:
 		with open(args.report_path, "w") as report:
 			report.write(report_data)