From 6e560f93ce27bdd27aa509061bde00c7da4c4db0 Mon Sep 17 00:00:00 2001
From: Lingfeng Fu <2871618714@qq.com>
Date: Wed, 4 Oct 2023 14:26:14 +0800
Subject: [PATCH 1/5] lock required obsolete version
---
requirements.txt | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index 2b09083..dadd9b5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,7 @@ appdirs>=1.4.4
Brotli>=1.0.9
click>=8.0.3
decorator>=5.1.1
-Flask>=2.0.2
+Flask==2.0.2
Flask-Compress>=1.10.1
Flask-Humanize>=0.3.0
Flask-SQLAlchemy>=2.5.1
@@ -17,9 +17,9 @@ psutil>=5.9.0
pyparsing>=3.0.7
python-utils>=3.1.0
six>=1.16.0
-SQLAlchemy>=1.4.31
+SQLAlchemy==1.4.31
sqlalchemy-migrate>=0.13.0
sqlparse>=0.4.2
Tempita>=0.5.2
-Werkzeug>=2.0.3
+Werkzeug==2.0.3
WTForms>=3.0.1
From b3bf15b1815fdb026eae211b6ef270465bd3a3d5 Mon Sep 17 00:00:00 2001
From: Lingfeng Fu <2871618714@qq.com>
Date: Wed, 4 Oct 2023 15:00:03 +0800
Subject: [PATCH 2/5] allow selecting mutators
---
app/templates/v2_generate_patches.html | 2 +-
app/utils/Mutation.py | 38 ++++++++++++++++----------
app/utils/SourceFile.py | 8 ++++--
app/views.py | 7 ++++-
4 files changed, 35 insertions(+), 20 deletions(-)
diff --git a/app/templates/v2_generate_patches.html b/app/templates/v2_generate_patches.html
index 237e15f..a9e1859 100644
--- a/app/templates/v2_generate_patches.html
+++ b/app/templates/v2_generate_patches.html
@@ -31,7 +31,7 @@
Generate patches for file {{ file.filename|basename }}
{% for mutator_name, mutator in mutators.items() %}
diff --git a/app/utils/Mutation.py b/app/utils/Mutation.py
index 621e97e..44fd648 100644
--- a/app/utils/Mutation.py
+++ b/app/utils/Mutation.py
@@ -51,7 +51,15 @@ def __repr__(self):
##############################################################################
-class LineDeletionMutator:
+class Mutator:
+ mutator_id: str
+ description: str
+ tags: list[str]
+
+ def find_mutations(self, line: str) -> list[Replacement]: ...
+
+
+class LineDeletionMutator(Mutator):
mutator_id = 'lineDeletion'
description = 'Deletes a whole line.'
tags = ['naive']
@@ -67,7 +75,7 @@ def find_mutations(self, line):
new_val=None)]
-class LogicalOperatorMutator:
+class LogicalOperatorMutator(Mutator):
mutator_id = 'logicalOperator'
description = 'Replaces logical operators.'
tags = ['logical', 'operator']
@@ -86,7 +94,7 @@ def find_mutations(self, line):
return self.pattern.mutate(line)
-class ComparisonOperatorMutator:
+class ComparisonOperatorMutator(Mutator):
mutator_id = 'comparisonOperator'
description = 'Replaces comparison operators.'
tags = ['operator', 'comparison']
@@ -105,7 +113,7 @@ def find_mutations(self, line):
return self.pattern.mutate(line)
-class IncDecOperatorMutator:
+class IncDecOperatorMutator(Mutator):
mutator_id = 'incDecOperator'
description = 'Swaps increment and decrement operators.'
tags = ['operator', 'artithmetic']
@@ -120,7 +128,7 @@ def find_mutations(self, line):
return self.pattern.mutate(line)
-class AssignmentOperatorMutator:
+class AssignmentOperatorMutator(Mutator):
mutator_id = 'assignmentOperator'
description = 'Replaces assignment operators.'
tags = ['operator']
@@ -139,7 +147,7 @@ def find_mutations(self, line):
return self.pattern.mutate(line)
-class BooleanAssignmentOperatorMutator:
+class BooleanAssignmentOperatorMutator(Mutator):
mutator_id = 'booleanAssignmentOperator'
description = 'Replaces Boolean assignment operators.'
tags = ['operator', 'logical']
@@ -158,7 +166,7 @@ def find_mutations(self, line):
return self.pattern.mutate(line)
-class ArithmeticOperatorMutator:
+class ArithmeticOperatorMutator(Mutator):
mutator_id = 'arithmeticOperator'
description = 'Replaces arithmetic operators.'
tags = ['operator', 'artithmetic']
@@ -176,7 +184,7 @@ def find_mutations(self, line):
return self.pattern.mutate(line)
-class BooleanArithmeticOperatorMutator:
+class BooleanArithmeticOperatorMutator(Mutator):
mutator_id = 'booleanArithmeticOperator'
description = 'Replaces Boolean arithmetic operators.'
tags = ['operator', 'logical']
@@ -194,7 +202,7 @@ def find_mutations(self, line):
return self.pattern.mutate(line)
-class BooleanLiteralMutator:
+class BooleanLiteralMutator(Mutator):
mutator_id = 'booleanLiteral'
description = 'Swaps the Boolean literals true and false.'
tags = ['logical', 'literal']
@@ -209,7 +217,7 @@ def find_mutations(self, line):
return self.pattern.mutate(line)
-class StdInserterMutator:
+class StdInserterMutator(Mutator):
mutator_id = 'stdInserter'
description = 'Changes the position where elements are inserted.'
tags = ['stl']
@@ -224,7 +232,7 @@ def find_mutations(self, line):
return self.pattern.mutate(line)
-class StdRangePredicateMutator:
+class StdRangePredicateMutator(Mutator):
mutator_id = 'stdRangePredicate'
description = 'Changes the semantics of an STL range predicate.'
tags = ['stl']
@@ -240,7 +248,7 @@ def find_mutations(self, line):
return self.pattern.mutate(line)
-class StdMinMaxMutator:
+class StdMinMaxMutator(Mutator):
mutator_id = 'stdMinMax'
description = 'Swaps STL minimum by maximum calls.'
tags = ['stl', 'artithmetic']
@@ -255,7 +263,7 @@ def find_mutations(self, line):
return self.pattern.mutate(line)
-class DecimalNumberLiteralMutator:
+class DecimalNumberLiteralMutator(Mutator):
mutator_id = 'decimalNumberLiteral'
description = 'Replaces decimal number literals with different values.'
tags = ['numerical', 'literal']
@@ -290,7 +298,7 @@ def find_mutations(self, line):
return result
-class HexNumberLiteralMutator:
+class HexNumberLiteralMutator(Mutator):
mutator_id = 'hexNumberLiteral'
description = 'Replaces hex number literals with different values.'
tags = ['numerical', 'literal']
@@ -322,7 +330,7 @@ def find_mutations(self, line):
return result
-class IteratorRangeMutator:
+class IteratorRangeMutator(Mutator):
mutator_id = 'iteratorRange'
description = 'Changes an iterator range.'
tags = ['iterators']
diff --git a/app/utils/SourceFile.py b/app/utils/SourceFile.py
index 917ee4e..e7c5a9b 100644
--- a/app/utils/SourceFile.py
+++ b/app/utils/SourceFile.py
@@ -3,7 +3,8 @@
from app import db
import os
from datetime import datetime
-from app.utils.Mutation import get_mutators
+from typing import Optional
+from app.utils.Mutation import get_mutators, Mutator
from app.utils.Replacement import Replacement
from app.models import File, Patch
@@ -25,8 +26,9 @@ def __init__(self, file: File, first_line, last_line):
# read the relevant content
self.content = '\n'.join(self.full_content[self.first_line - 1:self.last_line]) # type: str
- def generate_patches(self):
- mutators = get_mutators()
+ def generate_patches(self, mutators: Optional[dict[str, Mutator]] = None):
+ if mutators is None:
+ mutators = get_mutators()
for line_number, line_raw in self.__get_lines():
for mutator_name, mutator in mutators.items():
diff --git a/app/views.py b/app/views.py
index be1135f..fc91e40 100644
--- a/app/views.py
+++ b/app/views.py
@@ -311,7 +311,12 @@ def route_v2_project_project_id_files_file_id_generate(project_id, file_id):
last_line = -1
s = SourceFile(file, first_line, last_line)
- s.generate_patches()
+ selected_mutators = {}
+ all_mutators = get_mutators()
+ for mutator_id in all_mutators:
+ if mutator_id in request.form:
+ selected_mutators[mutator_id] = all_mutators[mutator_id]
+ s.generate_patches(selected_mutators)
flash('Successfully created patches.', category='message')
return redirect(url_for('route_v2_project_project_id', project_id=project.id))
From 2e972781082e735819154f2d8ee5b53fb0fd0990 Mon Sep 17 00:00:00 2001
From: Lingfeng Fu <2871618714@qq.com>
Date: Wed, 4 Oct 2023 14:26:14 +0800
Subject: [PATCH 3/5] ignore whitespace during patching
---
app/utils/Executor.py | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/app/utils/Executor.py b/app/utils/Executor.py
index 508ad55..425052b 100644
--- a/app/utils/Executor.py
+++ b/app/utils/Executor.py
@@ -86,7 +86,11 @@ def workflow(self, patch: Patch):
patchfile.close()
# step 2: apply patch
- self.__execute_command_timeout('patch -p1 --input={patchfile} {inputfile}'.format(patchfile=patchfile.name, inputfile=file.filename), cwd='/')
+ self.__execute_command_timeout(
+ 'patch --ignore-whitespace -p1 --input={patchfile} {inputfile}'.format(
+ patchfile=patchfile.name, inputfile=file.filename),
+ cwd='/'
+ )
# step 3: command pipeline
success = self.__apply_command(patch, 'build_command') and \
@@ -100,8 +104,11 @@ def workflow(self, patch: Patch):
db.session.commit()
# step 4: revert patch
- self.__execute_command_timeout('patch -p1 --reverse --input={patchfile} {inputfile}'.format(patchfile=patchfile.name, inputfile=file.filename),
- cwd='/')
+ self.__execute_command_timeout(
+ 'patch --ignore-whitespace -p1 --reverse --input={patchfile} {inputfile}'.format(
+ patchfile=patchfile.name, inputfile=file.filename),
+ cwd='/'
+ )
# step 6: delete patch file
os.remove(patchfile.name)
From 4c62c1337ea678f06230b481922712108da084e4 Mon Sep 17 00:00:00 2001
From: Lingfeng Fu <2871618714@qq.com>
Date: Wed, 4 Oct 2023 20:13:12 +0800
Subject: [PATCH 4/5] multi-threading workflow
---
app/utils/Executor.py | 232 +++++++++++++++++++++++++++++-------------
1 file changed, 160 insertions(+), 72 deletions(-)
diff --git a/app/utils/Executor.py b/app/utils/Executor.py
index 425052b..85843f4 100644
--- a/app/utils/Executor.py
+++ b/app/utils/Executor.py
@@ -1,25 +1,104 @@
# coding=utf-8
import shlex
+import shutil
import subprocess
+import threading
from threading import Timer, Thread
+from concurrent.futures import ThreadPoolExecutor
+from typing import Optional, Union
import psutil
-from app.models import Patch, Run, File
+from app.models import Patch, Run, Project
import tempfile
import os
import datetime
from app import db
+from pathlib import Path
+
+
+class _RaiiTempDir:
+ def __init__(self, prefix="mutate_cpp"):
+ self.path = Path(tempfile.mkdtemp(prefix=prefix))
+
+ def __del__(self):
+ shutil.rmtree(self.path)
+
+
+class _ThreadLocal:
+ workspace: _RaiiTempDir
+ last_project_id: Optional[int]
+
+
+_thread_local: Union[_ThreadLocal, threading.local] = threading.local()
+
+
+def _thread_initializer():
+ _thread_local.workspace = _RaiiTempDir()
+ _thread_local.last_project_id = None
+
+
+class _ProjectRecord:
+ def __init__(self, project: Project):
+ self.workdir = project.workdir
+ self.quickcheck_timeout = project.quickcheck_timeout
+ self.quickcheck_command = project.quickcheck_command
+ self.test_timeout = project.test_timeout
+ self.test_command = project.test_command
+ self.build_command = project.build_command
+ self.clean_command = project.clean_command
+
+
+class _PatchRecord:
+ def __init__(self, patch: Patch):
+ self.id = patch.id
+ self.state = patch.state
+ self.file_id = patch.file_id
+ self.file_filename = patch.file.filename
+ self.project_id = patch.project_id
+ self.project = _ProjectRecord(patch.project)
+ self.patch = patch.patch
+
+
+class _RunRecord:
+ def __init__(self):
+ self.command = None
+ self.patch_id = None
+ self.project_id = None
+ self.timestamp_start = None
+ self.output = None
+ self.timestamp_end = None
+ self.duration = None
+ self.log = None
+ self.success = None
+
+ def model(self):
+ m = Run()
+ m.command = self.command
+ m.patch_id = self.patch_id
+ m.project_id = self.project_id
+ m.timestamp_start = self.timestamp_start
+ m.output = self.output
+ m.timestamp_end = self.timestamp_end
+ m.duration = self.duration
+ m.log = self.log
+ m.success = self.success
+ return m
+
+
+class _ExecutionResult:
+ def __init__(self, patch_id: int):
+ self.patch_id: int = patch_id
+ self.run_records: list[_RunRecord] = []
+ self.state: str = "incomplete"
class Executor:
def __init__(self, app):
- self.__processes = []
self.running = False
self.app = app
- self.__current_patch = None
def start(self):
- if self.__current_patch is None:
+ if self.running is False:
self.running = True
Thread(target=self.main).start()
@@ -30,22 +109,76 @@ def stop(self):
def count(self):
return Patch.query.filter(Patch.state == 'incomplete').count()
- @property
- def current_patch(self):
- return self.__current_patch
-
def main(self):
with self.app.app_context():
while self.running:
- for patch in Patch.query.filter(Patch.state == 'incomplete').all():
- if self.running:
- self.workflow(patch)
+ with ThreadPoolExecutor(initializer=_thread_initializer) as executor:
+ patches = Patch.query.filter(Patch.state == 'incomplete').all()
+ patches = map(_PatchRecord, patches)
+ for result in executor.map(Executor.workflow, patches):
+ for run_record in result.run_records:
+ db.session.add(run_record.model())
+ Patch.query.get(result.patch_id).state = result.state
+ db.session.commit()
+ if not self.running:
+ break
+ executor.shutdown(cancel_futures=True)
self.stop()
- def __execute_command_timeout(self, command, timeout=None, cwd=None, stdin=None):
+ @staticmethod
+ def workflow(patch: _PatchRecord) -> _ExecutionResult:
+ result = _ExecutionResult(patch.id)
+ if patch.file_filename is None:
+ return result
+
+ # step 0: prepare workspace and file path
+ if patch.project_id != _thread_local.last_project_id:
+ shutil.rmtree(_thread_local.workspace.path)
+ shutil.copytree(patch.project.workdir, _thread_local.workspace.path)
+ _thread_local.last_project_id = patch.project_id
+ relative_path = Path(patch.file_filename).relative_to(Path(patch.project.workdir))
+ file_path = _thread_local.workspace.path / relative_path
+
+ # step 1: write patch to temp file
+ patchfile = tempfile.NamedTemporaryFile(delete=False, mode='w')
+ patchfile.write(patch.patch)
+ patchfile.close()
+
+ # step 2: apply patch
+ Executor.__execute_command_timeout(
+ 'patch --ignore-whitespace -p1 --input={patchfile} {inputfile}'.format(
+ patchfile=patchfile.name, inputfile=file_path),
+ cwd='/'
+ )
+
+ # step 3: command pipeline
+ success = (Executor.__apply_command(result, patch, 'build_command') and
+ Executor.__apply_command(result, patch, 'quickcheck_command') and
+ Executor.__apply_command(result, patch, 'test_command'))
+
+ Executor.__apply_command(result, patch, 'clean_command')
+
+ if success:
+ result.state = 'survived'
+ else:
+ result.state = 'killed'
+
+ # step 4: revert patch
+ Executor.__execute_command_timeout(
+ 'patch --ignore-whitespace -p1 --reverse --input={patchfile} {inputfile}'.format(
+ patchfile=patchfile.name, inputfile=file_path),
+ cwd='/'
+ )
+
+ # step 6: delete patch file
+ os.remove(patchfile.name)
+
+ return result
+
+ @staticmethod
+ def __execute_command_timeout(command, timeout=None, cwd=None, stdin=None):
proc = subprocess.Popen(shlex.split(command), stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=cwd)
- self.__processes.append(proc.pid)
def killer(process):
parent = psutil.Process(process.pid)
@@ -62,8 +195,6 @@ def killer(process):
finally:
timer.cancel()
- self.__processes.remove(proc.pid)
-
if cancelled:
raise subprocess.TimeoutExpired(command, timeout, stdout)
@@ -72,64 +203,23 @@ def killer(process):
return stdout
- def workflow(self, patch: Patch):
- assert self.__current_patch is None, 'no auto-concurrency!'
-
- file = File.query.get(patch.file_id)
-
- if file is not None:
- self.__current_patch = patch
-
- # step 1: write patch to temp file
- patchfile = tempfile.NamedTemporaryFile(delete=False, mode='w')
- patchfile.write(patch.patch)
- patchfile.close()
-
- # step 2: apply patch
- self.__execute_command_timeout(
- 'patch --ignore-whitespace -p1 --input={patchfile} {inputfile}'.format(
- patchfile=patchfile.name, inputfile=file.filename),
- cwd='/'
- )
-
- # step 3: command pipeline
- success = self.__apply_command(patch, 'build_command') and \
- self.__apply_command(patch, 'quickcheck_command') and \
- self.__apply_command(patch, 'test_command')
-
- self.__apply_command(patch, 'clean_command')
-
- if success:
- patch.state = 'survived'
- db.session.commit()
-
- # step 4: revert patch
- self.__execute_command_timeout(
- 'patch --ignore-whitespace -p1 --reverse --input={patchfile} {inputfile}'.format(
- patchfile=patchfile.name, inputfile=file.filename),
- cwd='/'
- )
-
- # step 6: delete patch file
- os.remove(patchfile.name)
-
- self.__current_patch = None
-
- def __apply_command(self, patch: Patch, step: str):
- print(patch, step)
+ @staticmethod
+ def __apply_command(result: _ExecutionResult, patch: _PatchRecord, step: str) -> bool:
+ print(patch.id, step)
+ project = patch.project
if step == 'quickcheck_command':
- timeout = patch.project.quickcheck_timeout
- command = patch.project.quickcheck_command
+ timeout = project.quickcheck_timeout
+ command = project.quickcheck_command
elif step == 'test_command':
- timeout = patch.project.test_timeout
- command = patch.project.test_command
+ timeout = project.test_timeout
+ command = project.test_command
elif step == 'build_command':
timeout = None
- command = patch.project.build_command
+ command = project.build_command
elif step == 'clean_command':
timeout = None
- command = patch.project.clean_command
+ command = project.clean_command
else:
raise NotImplementedError
@@ -137,7 +227,7 @@ def __apply_command(self, patch: Patch, step: str):
if not command:
return True
- run = Run()
+ run = _RunRecord()
run.command = step
run.patch_id = patch.id
run.project_id = patch.project_id
@@ -145,7 +235,7 @@ def __apply_command(self, patch: Patch, step: str):
# execute command
try:
- output = self.__execute_command_timeout(command, cwd=patch.project.workdir, timeout=timeout)
+ output = Executor.__execute_command_timeout(command, cwd=_thread_local.workspace.path, timeout=timeout)
timeout = False
success = True
nochange = False
@@ -178,11 +268,9 @@ def __apply_command(self, patch: Patch, step: str):
run.log = log
run.success = success
- db.session.add(run)
+ result.run_records.append(run)
if not success:
- patch.state = 'killed'
-
- db.session.commit()
+ result.state = 'killed'
return success
From fa5ba7a2799926626fcfbb00a4caaba0c17fdc8f Mon Sep 17 00:00:00 2001
From: Lingfeng Fu <2871618714@qq.com>
Date: Fri, 13 Oct 2023 13:58:32 +0800
Subject: [PATCH 5/5] optional multi-threading
---
app/config.py | 2 +
app/templates/v2_queue.html | 6 +-
app/utils/Executor.py | 225 ++++++++++--------------------------
app/utils/ParExecutor.py | 149 ++++++++++++++++++++++++
app/utils/SeqExecutor.py | 92 +++++++++++++++
app/views.py | 11 +-
6 files changed, 314 insertions(+), 171 deletions(-)
create mode 100644 app/utils/ParExecutor.py
create mode 100644 app/utils/SeqExecutor.py
diff --git a/app/config.py b/app/config.py
index 659351e..5510266 100644
--- a/app/config.py
+++ b/app/config.py
@@ -13,3 +13,5 @@
ITEMS_PER_PAGE = 20
TEMPLATES_AUTO_RELOAD = True
+
+PARALLEL_WORKFLOW = True
diff --git a/app/templates/v2_queue.html b/app/templates/v2_queue.html
index 3c697a5..40bf635 100644
--- a/app/templates/v2_queue.html
+++ b/app/templates/v2_queue.html
@@ -9,7 +9,11 @@
{% endblock %}
{% block content %}
- Queue
+ {% if executor.is_parallel() %}
+ Queue (Parallel)
+ {% else %}
+ Queue (Sequential)
+ {% endif %}
{% if executor.running %}
diff --git a/app/utils/Executor.py b/app/utils/Executor.py
index 85843f4..ad88add 100644
--- a/app/utils/Executor.py
+++ b/app/utils/Executor.py
@@ -1,98 +1,15 @@
# coding=utf-8
import shlex
-import shutil
import subprocess
-import threading
from threading import Timer, Thread
-from concurrent.futures import ThreadPoolExecutor
-from typing import Optional, Union
import psutil
-from app.models import Patch, Run, Project
-import tempfile
-import os
+from app.models import Patch, Run
import datetime
-from app import db
-from pathlib import Path
+from abc import ABC, abstractmethod
-class _RaiiTempDir:
- def __init__(self, prefix="mutate_cpp"):
- self.path = Path(tempfile.mkdtemp(prefix=prefix))
-
- def __del__(self):
- shutil.rmtree(self.path)
-
-
-class _ThreadLocal:
- workspace: _RaiiTempDir
- last_project_id: Optional[int]
-
-
-_thread_local: Union[_ThreadLocal, threading.local] = threading.local()
-
-
-def _thread_initializer():
- _thread_local.workspace = _RaiiTempDir()
- _thread_local.last_project_id = None
-
-
-class _ProjectRecord:
- def __init__(self, project: Project):
- self.workdir = project.workdir
- self.quickcheck_timeout = project.quickcheck_timeout
- self.quickcheck_command = project.quickcheck_command
- self.test_timeout = project.test_timeout
- self.test_command = project.test_command
- self.build_command = project.build_command
- self.clean_command = project.clean_command
-
-
-class _PatchRecord:
- def __init__(self, patch: Patch):
- self.id = patch.id
- self.state = patch.state
- self.file_id = patch.file_id
- self.file_filename = patch.file.filename
- self.project_id = patch.project_id
- self.project = _ProjectRecord(patch.project)
- self.patch = patch.patch
-
-
-class _RunRecord:
- def __init__(self):
- self.command = None
- self.patch_id = None
- self.project_id = None
- self.timestamp_start = None
- self.output = None
- self.timestamp_end = None
- self.duration = None
- self.log = None
- self.success = None
-
- def model(self):
- m = Run()
- m.command = self.command
- m.patch_id = self.patch_id
- m.project_id = self.project_id
- m.timestamp_start = self.timestamp_start
- m.output = self.output
- m.timestamp_end = self.timestamp_end
- m.duration = self.duration
- m.log = self.log
- m.success = self.success
- return m
-
-
-class _ExecutionResult:
- def __init__(self, patch_id: int):
- self.patch_id: int = patch_id
- self.run_records: list[_RunRecord] = []
- self.state: str = "incomplete"
-
-
-class Executor:
+class Executor(ABC):
def __init__(self, app):
self.running = False
self.app = app
@@ -109,74 +26,16 @@ def stop(self):
def count(self):
return Patch.query.filter(Patch.state == 'incomplete').count()
+ @abstractmethod
def main(self):
- with self.app.app_context():
- while self.running:
- with ThreadPoolExecutor(initializer=_thread_initializer) as executor:
- patches = Patch.query.filter(Patch.state == 'incomplete').all()
- patches = map(_PatchRecord, patches)
- for result in executor.map(Executor.workflow, patches):
- for run_record in result.run_records:
- db.session.add(run_record.model())
- Patch.query.get(result.patch_id).state = result.state
- db.session.commit()
- if not self.running:
- break
- executor.shutdown(cancel_futures=True)
- self.stop()
-
- @staticmethod
- def workflow(patch: _PatchRecord) -> _ExecutionResult:
- result = _ExecutionResult(patch.id)
- if patch.file_filename is None:
- return result
-
- # step 0: prepare workspace and file path
- if patch.project_id != _thread_local.last_project_id:
- shutil.rmtree(_thread_local.workspace.path)
- shutil.copytree(patch.project.workdir, _thread_local.workspace.path)
- _thread_local.last_project_id = patch.project_id
- relative_path = Path(patch.file_filename).relative_to(Path(patch.project.workdir))
- file_path = _thread_local.workspace.path / relative_path
-
- # step 1: write patch to temp file
- patchfile = tempfile.NamedTemporaryFile(delete=False, mode='w')
- patchfile.write(patch.patch)
- patchfile.close()
-
- # step 2: apply patch
- Executor.__execute_command_timeout(
- 'patch --ignore-whitespace -p1 --input={patchfile} {inputfile}'.format(
- patchfile=patchfile.name, inputfile=file_path),
- cwd='/'
- )
-
- # step 3: command pipeline
- success = (Executor.__apply_command(result, patch, 'build_command') and
- Executor.__apply_command(result, patch, 'quickcheck_command') and
- Executor.__apply_command(result, patch, 'test_command'))
+ ...
- Executor.__apply_command(result, patch, 'clean_command')
-
- if success:
- result.state = 'survived'
- else:
- result.state = 'killed'
-
- # step 4: revert patch
- Executor.__execute_command_timeout(
- 'patch --ignore-whitespace -p1 --reverse --input={patchfile} {inputfile}'.format(
- patchfile=patchfile.name, inputfile=file_path),
- cwd='/'
- )
-
- # step 6: delete patch file
- os.remove(patchfile.name)
-
- return result
+ @abstractmethod
+ def is_parallel(self):
+ ...
@staticmethod
- def __execute_command_timeout(command, timeout=None, cwd=None, stdin=None):
+ def _execute_command_timeout(command, timeout=None, cwd=None, stdin=None):
proc = subprocess.Popen(shlex.split(command), stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=cwd)
@@ -203,11 +62,49 @@ def killer(process):
return stdout
+ class _RunRecord:
+ def __init__(self):
+ self.command = None
+ self.patch_id = None
+ self.project_id = None
+ self.timestamp_start = None
+ self.output = None
+ self.timestamp_end = None
+ self.duration = None
+ self.log = None
+ self.success = None
+
+ def model(self):
+ m = Run()
+ m.command = self.command
+ m.patch_id = self.patch_id
+ m.project_id = self.project_id
+ m.timestamp_start = self.timestamp_start
+ m.output = self.output
+ m.timestamp_end = self.timestamp_end
+ m.duration = self.duration
+ m.log = self.log
+ m.success = self.success
+ return m
+
@staticmethod
- def __apply_command(result: _ExecutionResult, patch: _PatchRecord, step: str) -> bool:
- print(patch.id, step)
- project = patch.project
+ def _apply_patch(patch_file_path, input_file_path):
+ Executor._execute_command_timeout(
+ 'patch --ignore-whitespace -p1 --input={patchfile} {inputfile}'.format(
+ patchfile=patch_file_path, inputfile=input_file_path),
+ cwd='/'
+ )
+
+ @staticmethod
+ def _revert_patch(patch_file_path, input_file_path):
+ Executor._execute_command_timeout(
+ 'patch --ignore-whitespace -p1 --reverse --input={patchfile} {inputfile}'.format(
+ patchfile=patch_file_path, inputfile=input_file_path),
+ cwd='/'
+ )
+ @staticmethod
+ def _get_command_and_timeout(project, step):
if step == 'quickcheck_command':
timeout = project.quickcheck_timeout
command = project.quickcheck_command
@@ -222,20 +119,19 @@ def __apply_command(result: _ExecutionResult, patch: _PatchRecord, step: str) ->
command = project.clean_command
else:
raise NotImplementedError
+ return command, timeout
- # if no command is provided, return without creating a run; True means: next command must be executed
- if not command:
- return True
-
- run = _RunRecord()
+ @staticmethod
+ def _run_command(patch_id, project_id, step, command, cwd, timeout) -> _RunRecord:
+ run = Executor._RunRecord()
run.command = step
- run.patch_id = patch.id
- run.project_id = patch.project_id
+ run.patch_id = patch_id
+ run.project_id = project_id
run.timestamp_start = datetime.datetime.now()
# execute command
try:
- output = Executor.__execute_command_timeout(command, cwd=_thread_local.workspace.path, timeout=timeout)
+ output = Executor._execute_command_timeout(command, cwd=cwd, timeout=timeout)
timeout = False
success = True
nochange = False
@@ -268,9 +164,4 @@ def __apply_command(result: _ExecutionResult, patch: _PatchRecord, step: str) ->
run.log = log
run.success = success
- result.run_records.append(run)
-
- if not success:
- result.state = 'killed'
-
- return success
+ return run
diff --git a/app/utils/ParExecutor.py b/app/utils/ParExecutor.py
new file mode 100644
index 0000000..f771f81
--- /dev/null
+++ b/app/utils/ParExecutor.py
@@ -0,0 +1,149 @@
+# coding=utf-8
+
+import shutil
+import threading
+from concurrent.futures import ThreadPoolExecutor
+from typing import Optional, Union
+from app.models import Patch, Project
+import tempfile
+import os
+from app import db
+from pathlib import Path
+from .Executor import Executor
+
+
+class _RaiiTempDir:
+ def __init__(self, prefix="mutate_cpp"):
+ self.path = Path(tempfile.mkdtemp(prefix=prefix))
+
+ def __del__(self):
+ shutil.rmtree(self.path)
+
+
+class _ThreadLocal:
+ workspace: _RaiiTempDir
+ last_project_id: Optional[int]
+
+
+_thread_local: Union[_ThreadLocal, threading.local] = threading.local()
+
+
+def _thread_initializer():
+ _thread_local.workspace = _RaiiTempDir()
+ _thread_local.last_project_id = None
+
+
+class _ProjectRecord:
+ def __init__(self, project: Project):
+ self.workdir = project.workdir
+ self.quickcheck_timeout = project.quickcheck_timeout
+ self.quickcheck_command = project.quickcheck_command
+ self.test_timeout = project.test_timeout
+ self.test_command = project.test_command
+ self.build_command = project.build_command
+ self.clean_command = project.clean_command
+
+
+class _PatchRecord:
+ def __init__(self, patch: Patch):
+ self.id = patch.id
+ self.state = patch.state
+ self.file_id = patch.file_id
+ # noinspection PyUnresolvedReferences
+ self.file_filename = patch.file.filename
+ self.project_id = patch.project_id
+ # noinspection PyUnresolvedReferences
+ self.project = _ProjectRecord(patch.project)
+ self.patch = patch.patch
+
+
+class ParExecutor(Executor):
+ def __init__(self, app):
+ super().__init__(app)
+
+ def main(self):
+ with self.app.app_context():
+ while self.running:
+ with ThreadPoolExecutor(initializer=_thread_initializer) as executor:
+ patches = Patch.query.filter(Patch.state == 'incomplete').all()
+ patches = map(_PatchRecord, patches)
+ for result in executor.map(ParExecutor.workflow, patches):
+ for run_record in result.run_records:
+ db.session.add(run_record.model())
+ Patch.query.get(result.patch_id).state = result.state
+ db.session.commit()
+ if not self.running:
+ break
+ executor.shutdown(cancel_futures=True)
+ self.stop()
+
+ def is_parallel(self):
+ return True
+
+ class _ExecutionResult:
+ def __init__(self, patch_id: int):
+ self.patch_id: int = patch_id
+ self.run_records: list[Executor._RunRecord] = []
+ self.state: str = "incomplete"
+
+ @staticmethod
+ def workflow(patch: _PatchRecord) -> _ExecutionResult:
+ result = ParExecutor._ExecutionResult(patch.id)
+ if patch.file_filename is None:
+ return result
+
+ # step 0: prepare workspace and file path
+ if patch.project_id != _thread_local.last_project_id:
+ shutil.rmtree(_thread_local.workspace.path)
+ shutil.copytree(patch.project.workdir, _thread_local.workspace.path)
+ _thread_local.last_project_id = patch.project_id
+ relative_path = Path(patch.file_filename).relative_to(Path(patch.project.workdir))
+ file_path = _thread_local.workspace.path / relative_path
+
+ # step 1: write patch to temp file
+ patchfile = tempfile.NamedTemporaryFile(delete=False, mode='w')
+ patchfile.write(patch.patch)
+ patchfile.close()
+
+ # step 2: apply patch
+ Executor._apply_patch(patchfile.name, file_path)
+
+ # step 3: command pipeline
+ success = (ParExecutor.__apply_command(result, patch, 'build_command') and
+ ParExecutor.__apply_command(result, patch, 'quickcheck_command') and
+ ParExecutor.__apply_command(result, patch, 'test_command'))
+
+ ParExecutor.__apply_command(result, patch, 'clean_command')
+
+ if success:
+ result.state = 'survived'
+ else:
+ result.state = 'killed'
+
+ # step 4: revert patch
+ Executor._revert_patch(patchfile.name, file_path)
+
+ # step 6: delete patch file
+ os.remove(patchfile.name)
+
+ return result
+
+ @staticmethod
+ def __apply_command(result: _ExecutionResult, patch: _PatchRecord, step: str) -> bool:
+ print(patch.id, step)
+ project = patch.project
+
+ command, timeout = Executor._get_command_and_timeout(project, step)
+
+ # if no command is provided, return without creating a run; True means: next command must be executed
+ if not command:
+ return True
+
+ run = Executor._run_command(patch.id, patch.project_id, step, command, _thread_local.workspace.path, timeout)
+
+ result.run_records.append(run)
+
+ if not run.success:
+ result.state = 'killed'
+
+ return run.success
diff --git a/app/utils/SeqExecutor.py b/app/utils/SeqExecutor.py
new file mode 100644
index 0000000..4e02933
--- /dev/null
+++ b/app/utils/SeqExecutor.py
@@ -0,0 +1,92 @@
+# coding=utf-8
+
+from threading import Thread
+from app.models import Patch, Project, File
+import tempfile
+import os
+from app import db
+from .Executor import Executor
+
+
+class SeqExecutor(Executor):
+ def __init__(self, app):
+ super().__init__(app)
+ self.__current_patch = None
+
+ def start(self):
+ if self.__current_patch is None:
+ self.running = True
+ Thread(target=self.main).start()
+
+ @property
+ def current_patch(self):
+ return self.__current_patch
+
+ def main(self):
+ with self.app.app_context():
+ while self.running:
+ for patch in Patch.query.filter(Patch.state == 'incomplete').all():
+ if self.running:
+ self.workflow(patch)
+ self.stop()
+
+ def is_parallel(self):
+ return False
+
+ def workflow(self, patch: Patch):
+ assert self.__current_patch is None, 'no auto-concurrency!'
+
+ file: File = File.query.get(patch.file_id)
+
+ if file is not None:
+ self.__current_patch = patch
+
+ # step 1: write patch to temp file
+ patchfile = tempfile.NamedTemporaryFile(delete=False, mode='w')
+ patchfile.write(patch.patch)
+ patchfile.close()
+
+ # step 2: apply patch
+ Executor._apply_patch(patchfile.name, file.filename)
+
+ # step 3: command pipeline
+ success = (SeqExecutor.__apply_command(patch, 'build_command') and
+ SeqExecutor.__apply_command(patch, 'quickcheck_command') and
+ SeqExecutor.__apply_command(patch, 'test_command'))
+
+ SeqExecutor.__apply_command(patch, 'clean_command')
+
+ if success:
+ patch.state = 'survived'
+ db.session.commit()
+
+ # step 4: revert patch
+ Executor._revert_patch(patchfile.name, file.filename)
+
+ # step 6: delete patch file
+ os.remove(patchfile.name)
+
+ self.__current_patch = None
+
+ @staticmethod
+ def __apply_command(patch: Patch, step: str):
+ print(patch, step)
+ # noinspection PyUnresolvedReferences
+ project: Project = patch.project
+
+ command, timeout = Executor._get_command_and_timeout(project, step)
+
+ # if no command is provided, return without creating a run; True means: next command must be executed
+ if not command:
+ return True
+
+ run = Executor._run_command(patch.id, patch.project_id, step, command, project.workdir, timeout)
+
+ db.session.add(run.model())
+
+ if not run.success:
+ patch.state = 'killed'
+
+ db.session.commit()
+
+ return run.success
diff --git a/app/views.py b/app/views.py
index fc91e40..6b1dd90 100644
--- a/app/views.py
+++ b/app/views.py
@@ -1,5 +1,5 @@
# coding=utf-8
-
+from typing import Optional
from flask import render_template, abort, redirect, url_for, flash, request
from app import app, db
from app.forms import CreateProjectForm, CreateFileForm, SetConfirmationForm
@@ -9,14 +9,19 @@
from app.utils.Statistics import Statistics
import os
from app.utils.Executor import Executor
+from app.utils.ParExecutor import ParExecutor
+from app.utils.SeqExecutor import SeqExecutor
-executor = None
+executor: Optional[Executor] = None
@app.before_first_request
def init_executor():
global executor
- executor = Executor(app)
+ if app.config['PARALLEL_WORKFLOW']:
+ executor = ParExecutor(app)
+ else:
+ executor = SeqExecutor(app)
##############################################################################