"""
This module contains Patch class.
"""
import os
from copy import deepcopy
from .program import Program, GranularityLevel
from .atomic_operator import AtomicOperator
from .custom_operator import CustomOperator
from .test_result import TestResult
[docs]class Patch:
"""
Patch is a sequence of edit operators: both atomic and custom.
During search iteration, PYGGI modifies the source code of the target program
by applying a candidate patch. Subsequently, it runs the test script to collect
dynamic information, such as the execution time or any other user-provided
properties, via the predefined format that PYGGI recognises.
"""
def __init__(self, program):
self.program = program
self.test_result = None
self.edit_list = []
def __str__(self):
return ' | '.join(list(map(str, self.edit_list)))
def __len__(self):
return len(self.edit_list)
def __eq__(self, other):
return self.edit_list == other.edit_list
[docs] def clone(self):
"""
Create a new patch which has the same sequence of edits with the current one.
:return: The created Patch
:rtype: :py:class:`.Patch`
"""
clone_patch = Patch(self.program)
clone_patch.edit_list = deepcopy(self.edit_list)
clone_patch.test_result = None
return clone_patch
@property
def diff(self) -> str:
"""
Compare the source codes of original program and the patch-applied program
using *difflib* module(https://docs.python.org/3.6/library/difflib.html).
:return: The file comparison result
:rtype: str
"""
import difflib
self.apply()
diffs = ''
for i in range(len(self.program.target_files)):
original_target_file = os.path.join(self.program.path,
self.program.target_files[i])
modified_target_file = os.path.join(self.program.tmp_path,
self.program.target_files[i])
with open(original_target_file) as orig, open(
modified_target_file) as modi:
for diff in difflib.context_diff(
orig.readlines(),
modi.readlines(),
fromfile=original_target_file,
tofile=modified_target_file):
diffs += diff
return diffs
[docs] def run_test(self, timeout=15, result_parser=TestResult.pyggi_result_parser):
"""
Run the test script provided by the user
which is placed within the project directory.
:param float timeout: The time limit of test run (unit: seconds)
:param result_parser: The parser of test output
(default: :py:meth:`.TestResult.pyggi_result_parser`)
:type result_parser: None or callable((str, str), :py:class:`.TestResult`)
:return: The parsed output of test script execution
:rtype: :py:class:`.TestResult`
"""
import time
import subprocess
import shlex
self.apply()
cwd = os.getcwd()
os.chdir(self.program.tmp_path)
sprocess = subprocess.Popen(
shlex.split(self.program.test_command),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
try:
start = time.time()
stdout, stderr = sprocess.communicate(timeout=timeout)
elapsed_time = time.time() - start
self.test_result = result_parser(stdout.decode("ascii"), stderr.decode("ascii"))
self.test_result.elapsed_time = elapsed_time
except subprocess.TimeoutExpired:
elapsed_time = timeout * 1000 # seconds to milliseconds
self.test_result = TestResult(False, custom=None, elapsed_time=elapsed_time)
os.chdir(cwd)
return self.test_result
[docs] def add(self, edit):
"""
Add an edit to the edit list
:param edit: The edit to be added
:type edit: :py:class:`.atomic_operator.AtomicOperator` or :py:class:`.custom_operator.CustomOperator`
:return: None
"""
assert isinstance(edit, (AtomicOperator, CustomOperator))
assert edit.is_valid_for(self.program)
self.edit_list.append(edit)
[docs] def remove(self, index: int):
"""
Remove an edit from the edit list
:param int index: The index of edit to delete
"""
del self.edit_list[index]
[docs] def get_atomics(self, atomic_class_name=None):
"""
Combine all the atomic operators of the edits.
A custom operator is originally a sequence of atomic operators,
and a patch is a sequence of the edits.
So this is a sort of flattening process.
:return: The atomic operators, see *Hint*.
:rtype: list(:py:class:`.atomic_operator.AtomicOperator`)
"""
atomics = []
for edit in self.edit_list:
for atomic in edit.atomic_operators:
if not atomic_class_name or atomic_class_name == atomic.__class__.__name__:
atomics.append(atomic)
return atomics
[docs] def apply(self):
"""
This method applies the patch to the target program.
It does not directly modify the source code of the original program,
but modifies the copied program within the temporary directory.
:return: The contents of the patch-applied program, See *Hint*.
:rtype: dict(str, list(str))
.. hint::
- key: The target file name(path) related to the program root path
- value: The contents of the file
"""
assert isinstance(self.program.granularity_level, GranularityLevel)
target_files = self.program.contents.keys()
modification_points = deepcopy(self.program.modification_points)
new_contents = deepcopy(self.program.contents)
for target_file in target_files:
atomics = list(filter(lambda a: a.modification_point[0] == target_file, self.get_atomics()))
for atomic in atomics:
atomic.apply(self.program, new_contents, modification_points)
#self.program.reset_tmp_dir()
for target_file in new_contents:
with open(os.path.join(self.program.tmp_path, target_file), 'w') as tmp_file:
tmp_file.write(Program.to_source(self.program.granularity_level, new_contents[target_file]))
return new_contents