Skip to content

Commit 6a87097

Browse files
authored
Improve python tracing process (#2662)
1 parent 7d81366 commit 6a87097

File tree

32 files changed

+714
-326
lines changed

32 files changed

+714
-326
lines changed

utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import org.parsers.python.PythonParser
1010
import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour
1111
import org.utbot.framework.codegen.domain.TestFramework
1212
import org.utbot.framework.plugin.api.UtExecutionSuccess
13+
import org.utbot.python.coverage.CoverageOutputFormat
1314
import org.utbot.python.PythonMethodHeader
1415
import org.utbot.python.PythonTestGenerationConfig
1516
import org.utbot.python.PythonTestSet
16-
import org.utbot.python.utils.RequirementsInstaller
1717
import org.utbot.python.TestFileInformation
18+
import org.utbot.python.utils.RequirementsInstaller
1819
import org.utbot.python.code.PythonCode
20+
import org.utbot.python.coverage.PythonCoverageMode
1921
import org.utbot.python.framework.api.python.PythonClassId
2022
import org.utbot.python.framework.codegen.model.Pytest
2123
import org.utbot.python.framework.codegen.model.Unittest
@@ -114,9 +116,20 @@ class PythonGenerateTestsCommand : CliktCommand(
114116
.choice("PASS", "FAIL")
115117
.default("FAIL")
116118

117-
private val doNotGenerateRegressionSuite by option("--do-not-generate-regression-suite", help = "Do not generate regression test suite")
119+
private val doNotGenerateRegressionSuite by option("--do-not-generate-regression-suite", help = "Do not generate regression test suite.")
120+
.flag(default = false)
121+
122+
private val coverageMeasureMode by option("--coverage-measure-mode", help = "Use LINES or INSTRUCTIONS for coverage measurement.")
123+
.choice("INSTRUCTIONS", "LINES")
124+
.default("INSTRUCTIONS")
125+
126+
private val doNotSendCoverageContinuously by option("--do-not-send-coverage-continuously", help = "Do not send coverage during execution.")
118127
.flag(default = false)
119128

129+
private val coverageOutputFormat by option("--coverage-output-format", help = "Use LINES, INSTRUCTIONS (only from function frame).")
130+
.choice("INSTRUCTIONS", "LINES")
131+
.default("LINES")
132+
120133
private val testFramework: TestFramework
121134
get() =
122135
when (testFrameworkAsString) {
@@ -252,7 +265,10 @@ class PythonGenerateTestsCommand : CliktCommand(
252265
testSourceRootPath = Paths.get(output.toAbsolutePath()).parent.toAbsolutePath(),
253266
withMinimization = !doNotMinimize,
254267
isCanceled = { false },
255-
runtimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.valueOf(runtimeExceptionTestsBehaviour)
268+
runtimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.valueOf(runtimeExceptionTestsBehaviour),
269+
coverageMeasureMode = PythonCoverageMode.parse(coverageMeasureMode),
270+
sendCoverageContinuously = !doNotSendCoverageContinuously,
271+
coverageOutputFormat = CoverageOutputFormat.parse(coverageOutputFormat),
256272
)
257273

258274
val processor = PythonCliProcessor(

utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ package org.utbot.framework.plugin.api
1010
*
1111
* @see <a href="CONFLUENCE:Test+Minimization">Test minimization</a>
1212
*/
13-
data class Instruction(
13+
open class Instruction(
1414
val internalName: String,
1515
val methodSignature: String,
1616
val lineNumber: Int,

utbot-python-executor/src/main/python/utbot_executor/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "utbot-executor"
3-
version = "1.7.0"
3+
version = "1.8.0"
44
description = ""
55
authors = ["Vyacheslav Tamarin <vyacheslav.tamarin@yandex.ru>"]
66
readme = "README.md"

utbot-python-executor/src/main/python/utbot_executor/utbot_executor/__main__.py

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,49 @@
22
import logging
33

44
from utbot_executor.listener import PythonExecuteServer
5+
from utbot_executor.utils import TraceMode
56

67

7-
def main(hostname: str, port: int, coverage_hostname: str, coverage_port: str):
8-
server = PythonExecuteServer(hostname, port, coverage_hostname, coverage_port)
8+
def main(hostname: str, port: int, coverage_hostname: str, coverage_port: int, trace_mode: TraceMode, send_coverage: bool):
9+
server = PythonExecuteServer(hostname, port, coverage_hostname, coverage_port, trace_mode, send_coverage)
910
server.run()
1011

1112

12-
if __name__ == '__main__':
13+
if __name__ == "__main__":
1314
parser = argparse.ArgumentParser(
14-
prog='UtBot Python Executor',
15-
description='Listen socket stream and execute function value',
16-
)
17-
parser.add_argument('hostname')
18-
parser.add_argument('port', type=int)
19-
parser.add_argument('--logfile', default=None)
15+
prog="UtBot Python Executor",
16+
description="Listen socket stream and execute function value",
17+
)
18+
parser.add_argument("hostname")
19+
parser.add_argument("port", type=int)
20+
parser.add_argument("--logfile", default=None)
2021
parser.add_argument(
21-
'--loglevel',
22-
choices=["DEBUG", "INFO", "ERROR"],
23-
default="ERROR",
24-
)
25-
parser.add_argument('coverage_hostname')
26-
parser.add_argument('coverage_port', type=int)
22+
"--loglevel",
23+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
24+
default="ERROR",
25+
)
26+
parser.add_argument("coverage_hostname")
27+
parser.add_argument("coverage_port", type=int)
28+
parser.add_argument(
29+
"--coverage_type", choices=["lines", "instructions"], default="instructions"
30+
)
31+
parser.add_argument(
32+
"--send_coverage", action=argparse.BooleanOptionalAction
33+
)
2734
args = parser.parse_args()
2835

29-
loglevel = {"DEBUG": logging.DEBUG, "INFO": logging.INFO, "ERROR": logging.ERROR}[args.loglevel]
36+
loglevel = {
37+
"DEBUG": logging.DEBUG,
38+
"INFO": logging.INFO,
39+
"WARNING": logging.WARNING,
40+
"ERROR": logging.ERROR,
41+
}[args.loglevel]
3042
logging.basicConfig(
31-
filename=args.logfile,
32-
format='%(asctime)s | %(levelname)s | %(funcName)s - %(message)s',
33-
datefmt='%m/%d/%Y %H:%M:%S',
34-
level=loglevel,
35-
)
36-
main(args.hostname, args.port, args.coverage_hostname, args.coverage_port)
43+
filename=args.logfile,
44+
format="%(asctime)s | %(levelname)s | %(funcName)s - %(message)s",
45+
datefmt="%m/%d/%Y %H:%M:%S",
46+
level=loglevel,
47+
)
48+
trace_mode = TraceMode.Lines if args.coverage_type == "lines" else TraceMode.Instructions
49+
send_coverage = args.send_coverage
50+
main(args.hostname, args.port, args.coverage_hostname, args.coverage_port, trace_mode, send_coverage)

utbot-python-executor/src/main/python/utbot_executor/utbot_executor/executor.py

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
import inspect
55
import logging
66
import pathlib
7-
import socket
87
import sys
98
import traceback
10-
import typing
9+
import types
1110
from typing import Any, Callable, Dict, Iterable, List, Tuple
1211

1312
from utbot_executor.deep_serialization.deep_serialization import serialize_memory_dump, \
@@ -17,8 +16,13 @@
1716
from utbot_executor.deep_serialization.utils import PythonId, getattr_by_path
1817
from utbot_executor.memory_compressor import compress_memory
1918
from utbot_executor.parser import ExecutionRequest, ExecutionResponse, ExecutionFailResponse, ExecutionSuccessResponse
20-
from utbot_executor.ut_tracer import UtTracer
21-
from utbot_executor.utils import suppress_stdout as __suppress_stdout
19+
from utbot_executor.ut_tracer import UtTracer, UtCoverageSender
20+
from utbot_executor.utils import (
21+
suppress_stdout as __suppress_stdout,
22+
get_instructions,
23+
filter_instructions,
24+
TraceMode, UtInstruction,
25+
)
2226

2327
__all__ = ['PythonExecutor']
2428

@@ -41,9 +45,11 @@ def _load_objects(objs: List[Any]) -> MemoryDump:
4145

4246

4347
class PythonExecutor:
44-
def __init__(self, coverage_hostname: str, coverage_port: int):
48+
def __init__(self, coverage_hostname: str, coverage_port: int, trace_mode: TraceMode, send_coverage: bool):
4549
self.coverage_hostname = coverage_hostname
4650
self.coverage_port = coverage_port
51+
self.trace_mode = trace_mode
52+
self.send_coverage = send_coverage
4753

4854
@staticmethod
4955
def add_syspaths(syspaths: Iterable[str]):
@@ -91,7 +97,7 @@ def run_function(self, request: ExecutionRequest) -> ExecutionResponse:
9197
importlib.import_module(request.function_module),
9298
request.function_name
9399
)
94-
if not callable(function):
100+
if not isinstance(function, types.FunctionType):
95101
return ExecutionFailResponse(
96102
"fail",
97103
f"Invalid function path {request.function_module}.{request.function_name}"
@@ -111,23 +117,26 @@ def run_function(self, request: ExecutionRequest) -> ExecutionResponse:
111117
state_init = _update_states(loader.reload_id(), state_init_memory)
112118
serialized_state_init = serialize_memory_dump(state_init)
113119

114-
def _coverage_sender(info: typing.Tuple[str, int]):
115-
if pathlib.Path(info[0]) == pathlib.Path(request.filepath):
116-
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
117-
logging.debug("Coverage message: %s:%d", request.coverage_id, info[1])
118-
logging.debug("Port: %d", self.coverage_port)
119-
message = bytes(f'{request.coverage_id}:{info[1]}', encoding='utf-8')
120-
sock.sendto(message, (self.coverage_hostname, self.coverage_port))
121-
logging.debug("ID: %s, Coverage: %s", request.coverage_id, info)
120+
_coverage_sender = UtCoverageSender(
121+
request.coverage_id,
122+
self.coverage_hostname,
123+
self.coverage_port,
124+
send_coverage=self.send_coverage,
125+
)
122126

123127
value = _run_calculate_function_value(
124-
function,
125-
args,
126-
kwargs,
127-
request.filepath,
128-
serialized_state_init,
129-
tracer=UtTracer(_coverage_sender)
130-
)
128+
function,
129+
args,
130+
kwargs,
131+
request.filepath,
132+
serialized_state_init,
133+
tracer=UtTracer(
134+
pathlib.Path(request.filepath),
135+
[sys.prefix, sys.exec_prefix],
136+
_coverage_sender,
137+
self.trace_mode,
138+
),
139+
)
131140
except Exception as _:
132141
logging.debug("Error \n%s", traceback.format_exc())
133142
return ExecutionFailResponse("fail", traceback.format_exc())
@@ -157,7 +166,7 @@ def _serialize_state(
157166

158167

159168
def _run_calculate_function_value(
160-
function: Callable,
169+
function: types.FunctionType,
161170
args: List[Any],
162171
kwargs: Dict[str, Any],
163172
fullpath: str,
@@ -172,10 +181,8 @@ def _run_calculate_function_value(
172181

173182
__is_exception = False
174183

175-
(__sources, __start, ) = inspect.getsourcelines(function)
176-
__not_empty_lines = [i for i, line in enumerate(__sources, __start) if len(line.strip()) != 0]
177-
logging.debug("Not empty lines %s", __not_empty_lines)
178-
__end = __start + len(__sources)
184+
_, __start = inspect.getsourcelines(function)
185+
__all_code_stmts = filter_instructions(get_instructions(function.__code__), tracer.mode)
179186

180187
__tracer = tracer
181188

@@ -189,24 +196,23 @@ def _run_calculate_function_value(
189196

190197
logging.debug("Coverage: %s", __tracer.counts)
191198
logging.debug("Fullpath: %s", fullpath)
192-
module_path = pathlib.Path(fullpath)
193-
__stmts = [x[1] for x in __tracer.counts if pathlib.Path(x[0]) == module_path]
194-
__stmts_filtered = [x for x in __not_empty_lines if x in __stmts]
195-
__stmts_filtered_with_def = [__start] + __stmts_filtered
196-
__missed_filtered = [x for x in __not_empty_lines if x not in __stmts_filtered_with_def]
197-
logging.debug("Covered lines: %s", __stmts_filtered_with_def)
199+
__stmts_with_def = [UtInstruction(__start, 0, True)] + list(__tracer.counts.keys())
200+
__missed_filtered = [x for x in __all_code_stmts if x not in __stmts_with_def]
201+
logging.debug("Covered lines: %s", __stmts_with_def)
198202
logging.debug("Missed lines: %s", __missed_filtered)
199203

204+
__str_statements = [x.serialize() for x in __stmts_with_def]
205+
__str_missed_statements = [x.serialize() for x in __missed_filtered]
206+
200207
args_ids, kwargs_ids, result_id, state_after, serialized_state_after = _serialize_state(args, kwargs, __result)
201208
ids = args_ids + list(kwargs_ids.values())
202-
# state_before, state_after = compress_memory(ids, state_before, state_after)
203209
diff_ids = compress_memory(ids, state_before, state_after)
204210

205211
return ExecutionSuccessResponse(
206212
status="success",
207213
is_exception=__is_exception,
208-
statements=__stmts_filtered_with_def,
209-
missed_statements=__missed_filtered,
214+
statements=__str_statements,
215+
missed_statements=__str_missed_statements,
210216
state_init=state_init,
211217
state_before=serialized_state_before,
212218
state_after=serialized_state_after,

utbot-python-executor/src/main/python/utbot_executor/utbot_executor/listener.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from utbot_executor.deep_serialization.memory_objects import PythonSerializer
77
from utbot_executor.parser import parse_request, serialize_response, ExecutionFailResponse
88
from utbot_executor.executor import PythonExecutor
9-
9+
from utbot_executor.utils import TraceMode
1010

1111
RECV_SIZE = 2**15
1212

@@ -17,12 +17,14 @@ def __init__(
1717
hostname: str,
1818
port: int,
1919
coverage_hostname: str,
20-
coverage_port: str,
20+
coverage_port: int,
21+
trace_mode: TraceMode,
22+
send_coverage: bool
2123
):
2224
logging.info('PythonExecutor is creating...')
2325
self.clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2426
self.clientsocket.connect((hostname, port))
25-
self.executor = PythonExecutor(coverage_hostname, coverage_port)
27+
self.executor = PythonExecutor(coverage_hostname, coverage_port, trace_mode, send_coverage)
2628

2729
def run(self) -> None:
2830
logging.info('PythonExecutor is ready...')
@@ -80,9 +82,9 @@ def handler(self) -> None:
8082
response_size = str(len(bytes_data))
8183
self.clientsocket.send((response_size + os.linesep).encode())
8284

83-
sended_size = 0
84-
while len(bytes_data) > sended_size:
85-
sended_size += self.clientsocket.send(bytes_data[sended_size:])
85+
sent_size = 0
86+
while len(bytes_data) > sent_size:
87+
sent_size += self.clientsocket.send(bytes_data[sent_size:])
8688

8789
logging.debug('Sent all data')
8890
logging.info('All done...')

utbot-python-executor/src/main/python/utbot_executor/utbot_executor/parser.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dataclasses
22
import json
3-
from typing import Dict, List, Union
3+
from typing import Dict, List, Union, Tuple
44

55

66
@dataclasses.dataclass
@@ -24,8 +24,8 @@ class ExecutionResponse:
2424
class ExecutionSuccessResponse(ExecutionResponse):
2525
status: str
2626
is_exception: bool
27-
statements: List[int]
28-
missed_statements: List[int]
27+
statements: List[str]
28+
missed_statements: List[str]
2929
state_init: str
3030
state_before: str
3131
state_after: str

0 commit comments

Comments
 (0)