aboutsummaryrefslogtreecommitdiffstats
path: root/library/python/testing
diff options
context:
space:
mode:
authorDevtools Arcadia <arcadia-devtools@yandex-team.ru>2022-02-07 18:08:42 +0300
committerDevtools Arcadia <arcadia-devtools@mous.vla.yp-c.yandex.net>2022-02-07 18:08:42 +0300
commit1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch)
treee26c9fed0de5d9873cce7e00bc214573dc2195b7 /library/python/testing
downloadydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'library/python/testing')
-rw-r--r--library/python/testing/__init__.py0
-rw-r--r--library/python/testing/filter/filter.py57
-rw-r--r--library/python/testing/filter/ya.make5
-rw-r--r--library/python/testing/import_test/import_test.py124
-rw-r--r--library/python/testing/import_test/ya.make10
-rw-r--r--library/python/testing/recipe/__init__.py102
-rw-r--r--library/python/testing/recipe/ports.py33
-rw-r--r--library/python/testing/recipe/ya.make19
-rw-r--r--library/python/testing/ya.make22
-rw-r--r--library/python/testing/yatest_common/ya.make40
-rw-r--r--library/python/testing/yatest_common/yatest/__init__.py3
-rw-r--r--library/python/testing/yatest_common/yatest/common/__init__.py8
-rw-r--r--library/python/testing/yatest_common/yatest/common/benchmark.py22
-rw-r--r--library/python/testing/yatest_common/yatest/common/canonical.py176
-rw-r--r--library/python/testing/yatest_common/yatest/common/environment.py5
-rw-r--r--library/python/testing/yatest_common/yatest/common/errors.py20
-rw-r--r--library/python/testing/yatest_common/yatest/common/legacy.py12
-rw-r--r--library/python/testing/yatest_common/yatest/common/misc.py19
-rw-r--r--library/python/testing/yatest_common/yatest/common/network.py271
-rw-r--r--library/python/testing/yatest_common/yatest/common/path.py90
-rw-r--r--library/python/testing/yatest_common/yatest/common/process.py733
-rw-r--r--library/python/testing/yatest_common/yatest/common/runtime.py343
-rw-r--r--library/python/testing/yatest_common/yatest/common/runtime_java.py46
-rw-r--r--library/python/testing/yatest_common/yatest/common/tags.py5
-rw-r--r--library/python/testing/yatest_common/yatest/common/ya.make1
-rw-r--r--library/python/testing/yatest_lib/__init__.py0
-rw-r--r--library/python/testing/yatest_lib/external.py192
-rw-r--r--library/python/testing/yatest_lib/test_splitter.py102
-rw-r--r--library/python/testing/yatest_lib/tests/test_external.py20
-rw-r--r--library/python/testing/yatest_lib/tests/test_testsplitter.py103
-rw-r--r--library/python/testing/yatest_lib/tests/ya.make14
-rw-r--r--library/python/testing/yatest_lib/tools.py64
-rw-r--r--library/python/testing/yatest_lib/ya.make26
-rw-r--r--library/python/testing/yatest_lib/ya.py239
34 files changed, 2926 insertions, 0 deletions
diff --git a/library/python/testing/__init__.py b/library/python/testing/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/library/python/testing/__init__.py
diff --git a/library/python/testing/filter/filter.py b/library/python/testing/filter/filter.py
new file mode 100644
index 0000000000..a1642bd052
--- /dev/null
+++ b/library/python/testing/filter/filter.py
@@ -0,0 +1,57 @@
+# coding: utf-8
+# TODO move devtools/ya/test/filter.py to library/python/testing/filter/filter.py
+import re
+import fnmatch
+import logging
+
+logger = logging.getLogger(__name__)
+TEST_SUBTEST_SEPARATOR = '::'
+
+PARSE_TAG_RE = re.compile("([+-]?[\w:]*)")
+
+
+class FilterException(Exception):
+ mute = True
+
+
+def fix_filter(flt):
+ if TEST_SUBTEST_SEPARATOR not in flt and "*" not in flt:
+ # user wants to filter by test module name
+ flt = flt + TEST_SUBTEST_SEPARATOR + "*"
+ return flt
+
+
+def escape_for_fnmatch(s):
+ return s.replace("[", "&#91;").replace("]", "&#93;")
+
+
+def make_py_file_filter(filter_names):
+ if filter_names is not None:
+ with_star = []
+ wo_star = set()
+ for flt in filter_names:
+ flt = flt.split(':')[0]
+ if '*' in flt:
+ with_star.append(flt.split('*')[0] + '*')
+ else:
+ wo_star.add(flt)
+
+ def predicate(filename):
+ if filter_names is None:
+ return True
+ return filename in wo_star or any([fnmatch.fnmatch(escape_for_fnmatch(filename), escape_for_fnmatch(filter_name)) for filter_name in with_star])
+
+ return predicate
+
+
+def make_name_filter(filter_names):
+ filter_names = map(fix_filter, filter_names)
+ filter_full_names = set()
+ for name in filter_names:
+ if '*' not in name:
+ filter_full_names.add(name)
+
+ def predicate(testname):
+ return testname in filter_full_names or any([fnmatch.fnmatch(escape_for_fnmatch(testname), escape_for_fnmatch(filter_name)) for filter_name in filter_names])
+
+ return predicate
diff --git a/library/python/testing/filter/ya.make b/library/python/testing/filter/ya.make
new file mode 100644
index 0000000000..22c485d258
--- /dev/null
+++ b/library/python/testing/filter/ya.make
@@ -0,0 +1,5 @@
+PY23_LIBRARY()
+OWNER(g:yatest)
+PY_SRCS(filter.py)
+
+END()
diff --git a/library/python/testing/import_test/import_test.py b/library/python/testing/import_test/import_test.py
new file mode 100644
index 0000000000..3e3b7234ef
--- /dev/null
+++ b/library/python/testing/import_test/import_test.py
@@ -0,0 +1,124 @@
+from __future__ import print_function
+
+import os
+import re
+import sys
+import time
+import traceback
+
+import __res
+from __res import importer
+
+
+def check_imports(no_check=(), extra=(), skip_func=None, py_main=None):
+ """
+ tests all bundled modules are importable
+ just add
+ "PEERDIR(library/python/import_test)" to your CMakeLists.txt and
+ "from import_test import test_imports" to your python test source file.
+ """
+ str_ = lambda s: s
+ if not isinstance(b'', str):
+ str_ = lambda s: s.decode('UTF-8')
+
+ exceptions = list(no_check)
+ for key, _ in __res.iter_keys(b'py/no_check_imports/'):
+ exceptions += str_(__res.find(key)).split()
+ if exceptions:
+ exceptions.sort()
+ print('NO_CHECK_IMPORTS', ' '.join(exceptions))
+
+ patterns = [re.escape(s).replace(r'\*', r'.*') for s in exceptions]
+ rx = re.compile('^({})$'.format('|'.join(patterns)))
+
+ failed = []
+ import_times = {}
+
+ norm = lambda s: s[:-9] if s.endswith('.__init__') else s
+
+ modules = sys.extra_modules | set(extra)
+ modules = sorted(modules, key=norm)
+ if py_main:
+ modules = [py_main] + modules
+
+ for module in modules:
+ if module not in extra and (rx.search(module) or skip_func and skip_func(module)):
+ print('SKIP', module)
+ continue
+
+ name = module.rsplit('.', 1)[-1]
+ if name == '__main__' and 'if __name__ ==' not in importer.get_source(module):
+ print('SKIP', module, '''without "if __name__ == '__main__'" check''')
+ continue
+
+ def print_backtrace_marked(e):
+ tb_exc = traceback.format_exception(*e)
+ for item in tb_exc:
+ for l in item.splitlines():
+ print('FAIL:', l, file=sys.stderr)
+
+ try:
+ print('TRY', module)
+ # XXX waiting for py3 to use print(..., flush=True)
+ sys.stdout.flush()
+
+ s = time.time()
+ if module == '__main__':
+ importer.load_module('__main__', '__main__py')
+ elif module.endswith('.__init__'):
+ __import__(module[:-len('.__init__')])
+ else:
+ __import__(module)
+
+ delay = time.time() - s
+ import_times[str(module)] = delay
+ print('OK ', module, '{:.3f}s'.format(delay))
+
+ except Exception as e:
+ print('FAIL:', module, e, file=sys.stderr)
+ print_backtrace_marked(sys.exc_info())
+ failed.append('{}: {}'.format(module, e))
+
+ except:
+ e = sys.exc_info()
+ print('FAIL:', module, e, file=sys.stderr)
+ print_backtrace_marked(e)
+ failed.append('{}: {}'.format(module, e))
+ raise
+
+ print("Slowest imports:")
+ for m, t in sorted(import_times.items(), key=lambda x: x[1], reverse=True)[:30]:
+ print(' ', '{:.3f}s'.format(t), m)
+
+ if failed:
+ raise ImportError('modules not imported:\n' + '\n'.join(failed))
+
+
+test_imports = check_imports
+
+
+def main():
+ skip_names = sys.argv[1:]
+
+ os.environ['Y_PYTHON_IMPORT_TEST'] = ''
+
+ # We should initialize Django before importing any applications
+ if os.getenv('DJANGO_SETTINGS_MODULE'):
+ try:
+ import django
+ except ImportError:
+ pass
+ else:
+ django.setup()
+
+ py_main = __res.find('PY_MAIN')
+
+ if py_main:
+ py_main_module = py_main.split(b':', 1)[0].decode('UTF-8')
+ else:
+ py_main_module = None
+
+ try:
+ check_imports(no_check=skip_names, py_main=py_main_module)
+ except:
+ sys.exit(1)
diff --git a/library/python/testing/import_test/ya.make b/library/python/testing/import_test/ya.make
new file mode 100644
index 0000000000..fae36ffe8f
--- /dev/null
+++ b/library/python/testing/import_test/ya.make
@@ -0,0 +1,10 @@
+OWNER(
+ g:yatest
+ exprmntr
+)
+
+PY23_LIBRARY()
+
+PY_SRCS(import_test.py)
+
+END()
diff --git a/library/python/testing/recipe/__init__.py b/library/python/testing/recipe/__init__.py
new file mode 100644
index 0000000000..5ef9c5c189
--- /dev/null
+++ b/library/python/testing/recipe/__init__.py
@@ -0,0 +1,102 @@
+from __future__ import print_function
+
+import os
+import sys
+import json
+import logging
+import argparse
+
+from yatest_lib.ya import Ya
+
+RECIPE_START_OPTION = "start"
+RECIPE_STOP_OPTION = "stop"
+
+ya = None
+collect_cores = None
+sanitizer_extra_checks = None
+
+
+def _setup_logging(level=logging.DEBUG):
+ root_logger = logging.getLogger()
+ root_logger.setLevel(level)
+
+ log_format = '%(asctime)s - %(levelname)s - %(name)s - %(funcName)s: %(message)s'
+
+ stdout_handler = logging.StreamHandler(sys.stdout)
+ stdout_handler.setLevel(logging.DEBUG)
+ formatter = logging.Formatter(log_format)
+ stdout_handler.setFormatter(formatter)
+ root_logger.addHandler(stdout_handler)
+
+
+def get_options():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--show-cwd", action="store_true", dest="show_cwd", default=False, help="show recipe cwd")
+ parser.add_argument("--test-debug", action="store_true", dest="test_debug", default=False, help="test debug mode")
+ parser.add_argument("--test-stderr", action="store_true", dest="test_stderr", default=False, help="test stderr")
+ parser.add_argument("--pdb", action="store_true", dest="pdb", default=False, help="run pdb on error")
+ parser.add_argument("--sanitizer-extra-checks", dest="sanitizer_extra_checks", action="store_true", default=False, help="enables extra checks for tests built with sanitizers")
+ parser.add_argument("--collect-cores", dest="collect_cores", action="store_true", default=False, help="allows core dump file recovering during test")
+
+ args, opts = parser.parse_known_args()
+
+ global ya, sanitizer_extra_checks, collect_cores
+ _setup_logging()
+
+ context = {
+ "test_stderr": args.test_stderr,
+ }
+
+ ya = Ya(context=context)
+
+ ya._data_root = "" # XXX remove
+
+ sanitizer_extra_checks = args.sanitizer_extra_checks
+ if sanitizer_extra_checks:
+ for envvar in ['LSAN_OPTIONS', 'ASAN_OPTIONS']:
+ if envvar in os.environ:
+ os.environ.pop(envvar)
+ if envvar + '_ORIGINAL' in os.environ:
+ os.environ[envvar] = os.environ[envvar + '_ORIGINAL']
+ collect_cores = args.collect_cores
+
+ for recipe_option in RECIPE_START_OPTION, RECIPE_STOP_OPTION:
+ if recipe_option in opts:
+ return args, opts[opts.index(recipe_option):]
+
+
+def set_env(key, value):
+ with open(ya.env_file, "a") as f:
+ json.dump({key: value}, f)
+ f.write("\n")
+
+
+def tty():
+ if os.isatty(1):
+ return
+
+ f = open('/dev/tty', 'w+')
+ fd = f.fileno()
+ os.dup2(fd, 0)
+ os.dup2(fd, 1)
+ os.dup2(fd, 2)
+
+
+def declare_recipe(start, stop):
+ parsed_args, argv = get_options()
+
+ if parsed_args.show_cwd:
+ print("Recipe \"{} {}\" working dir is {}".format(sys.argv[0], " ".join(argv), os.getcwd()))
+
+ try:
+ if argv[0] == RECIPE_START_OPTION:
+ start(argv[1:])
+ elif argv[0] == RECIPE_STOP_OPTION:
+ stop(argv[1:])
+ except Exception:
+ if parsed_args.pdb:
+ tty()
+ import ipdb
+ ipdb.post_mortem()
+ else:
+ raise
diff --git a/library/python/testing/recipe/ports.py b/library/python/testing/recipe/ports.py
new file mode 100644
index 0000000000..9f7de1e767
--- /dev/null
+++ b/library/python/testing/recipe/ports.py
@@ -0,0 +1,33 @@
+import os
+import sys
+import subprocess
+import time
+
+from yatest.common.network import PortManager
+
+
+def __get_port_range():
+ port_count = int(sys.argv[1])
+ pid_filename = sys.argv[2]
+ port_manager = PortManager()
+ start_port = port_manager.get_port_range(None, port_count)
+ sys.stderr.write(str(start_port) + "\n")
+ with open(pid_filename, 'w') as afile:
+ afile.write(str(os.getpid()))
+ while 1:
+ time.sleep(1)
+
+
+def get_port_range(port_count=1, pid_filename="recipe_port.pid"):
+ env = os.environ.copy()
+ env["Y_PYTHON_ENTRY_POINT"] = "library.python.testing.recipe.ports:__get_port_range"
+ res = subprocess.Popen([sys.argv[0], str(port_count), pid_filename], env=env, cwd=os.getcwd(), stderr=subprocess.PIPE)
+ while not os.path.exists(pid_filename):
+ time.sleep(0.01)
+ port_start = int(res.stderr.readline())
+ return port_start
+
+
+def release_port_range(pid_filename="recipe_port.pid"):
+ with open(pid_filename, 'r') as afile:
+ os.kill(int(afile.read()), 9)
diff --git a/library/python/testing/recipe/ya.make b/library/python/testing/recipe/ya.make
new file mode 100644
index 0000000000..dd323aa245
--- /dev/null
+++ b/library/python/testing/recipe/ya.make
@@ -0,0 +1,19 @@
+OWNER(
+ exprmntr
+ g:yatest
+)
+
+PY23_LIBRARY()
+
+PY_SRCS(
+ __init__.py
+ ports.py
+)
+
+PEERDIR(
+ contrib/python/ipdb
+ library/python/testing/yatest_common
+ library/python/testing/yatest_lib
+)
+
+END()
diff --git a/library/python/testing/ya.make b/library/python/testing/ya.make
new file mode 100644
index 0000000000..883bc8d7ab
--- /dev/null
+++ b/library/python/testing/ya.make
@@ -0,0 +1,22 @@
+OWNER(g:yatest)
+
+RECURSE(
+ behave
+ deprecated
+ fake_ya_package
+ filter
+ gtest
+ gtest/test
+ gtest/test/gtest
+ pyremock
+ pytest_runner
+ pytest_runner/example
+ pytest_runner/test
+ recipe
+ system_info
+ types_test
+ yapackage
+ yapackage/test
+ yatest_common
+ yatest_lib
+)
diff --git a/library/python/testing/yatest_common/ya.make b/library/python/testing/yatest_common/ya.make
new file mode 100644
index 0000000000..5662db4c5d
--- /dev/null
+++ b/library/python/testing/yatest_common/ya.make
@@ -0,0 +1,40 @@
+OWNER(g:yatest)
+
+PY23_LIBRARY()
+
+OWNER(g:yatest)
+
+NO_EXTENDED_SOURCE_SEARCH()
+
+PY_SRCS(
+ TOP_LEVEL
+ yatest/__init__.py
+ yatest/common/__init__.py
+ yatest/common/benchmark.py
+ yatest/common/canonical.py
+ yatest/common/environment.py
+ yatest/common/errors.py
+ yatest/common/legacy.py
+ yatest/common/misc.py
+ yatest/common/network.py
+ yatest/common/path.py
+ yatest/common/process.py
+ yatest/common/runtime.py
+ yatest/common/runtime_java.py
+ yatest/common/tags.py
+)
+
+PEERDIR(
+ contrib/python/six
+ library/python/cores
+ library/python/filelock
+ library/python/fs
+)
+
+IF (NOT CATBOOST_OPENSOURCE)
+ PEERDIR(
+ library/python/coredump_filter
+ )
+ENDIF()
+
+END()
diff --git a/library/python/testing/yatest_common/yatest/__init__.py b/library/python/testing/yatest_common/yatest/__init__.py
new file mode 100644
index 0000000000..b846b3317a
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/__init__.py
@@ -0,0 +1,3 @@
+__all__ = ["common"]
+
+from . import common
diff --git a/library/python/testing/yatest_common/yatest/common/__init__.py b/library/python/testing/yatest_common/yatest/common/__init__.py
new file mode 100644
index 0000000000..cf57779e27
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/__init__.py
@@ -0,0 +1,8 @@
+from .benchmark import * # noqa
+from .canonical import * # noqa
+from .errors import * # noqa
+from .misc import * # noqa
+from .path import * # noqa
+from .process import * # noqa
+from .runtime import * # noqa
+from .tags import * # noqa
diff --git a/library/python/testing/yatest_common/yatest/common/benchmark.py b/library/python/testing/yatest_common/yatest/common/benchmark.py
new file mode 100644
index 0000000000..c3784cbe4c
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/benchmark.py
@@ -0,0 +1,22 @@
+import json
+
+from . import process
+from . import runtime
+
+
+def execute_benchmark(path, budget=None, threads=None):
+ """
+ Run benchmark and return values
+ :param path: path to benchmark binary
+ :param budget: time budget, sec (supported only by ybenchmark)
+ :param threads: number of threads to run benchmark (supported only by ybenchmark)
+ :return: map of benchmark values
+ """
+ benchmark_path = runtime.binary_path(path)
+ cmd = [benchmark_path, "--benchmark_format=json"]
+ if budget is not None:
+ cmd += ["-b", str(budget)]
+ if threads is not None:
+ cmd += ["-t", str(threads)]
+ res = process.execute(cmd)
+ return json.loads(res.std_out)
diff --git a/library/python/testing/yatest_common/yatest/common/canonical.py b/library/python/testing/yatest_common/yatest/common/canonical.py
new file mode 100644
index 0000000000..b6a136d3e9
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/canonical.py
@@ -0,0 +1,176 @@
+import os
+import logging
+import shutil
+import tempfile
+
+import six
+
+from . import process
+from . import runtime
+from . import path
+
+yatest_logger = logging.getLogger("ya.test")
+
+
+def _copy(src, dst, universal_lines=False):
+ if universal_lines:
+ with open(dst, "wb") as f:
+ for line in open(src, "rbU"):
+ f.write(line)
+ return
+ shutil.copy(src, dst)
+
+
+def canonical_file(path, diff_tool=None, local=False, universal_lines=False, diff_file_name=None, diff_tool_timeout=None):
+ """
+ Create canonical file that can be returned from a test
+ :param path: path to the file
+ :param diff_tool: custom diff tool to use for comparison with the canonical one, if None - default will be used
+ :param local: save file locally, otherwise move to sandbox
+ :param universal_lines: normalize EOL
+ :param diff_tool_timeout: timeout for running diff tool
+ :return: object that can be canonized
+ """
+ abs_path = os.path.abspath(path)
+ assert os.path.exists(abs_path), "Canonical path {} does not exist".format(path)
+ tempdir = tempfile.mkdtemp(prefix="canon_tmp", dir=runtime.build_path())
+ safe_path = os.path.join(tempdir, os.path.basename(abs_path))
+ # if the created file is in output_path, we copy it, so that it will be available when the tests finishes
+ _copy(path, safe_path, universal_lines=universal_lines)
+ if diff_tool:
+ if not isinstance(diff_tool, six.string_types):
+ try: # check if iterable
+ if not isinstance(diff_tool[0], six.string_types):
+ raise Exception("Invalid custom diff-tool: not cmd")
+ except:
+ raise Exception("Invalid custom diff-tool: not binary path")
+ return runtime._get_ya_plugin_instance().file(safe_path, diff_tool=diff_tool, local=local, diff_file_name=diff_file_name, diff_tool_timeout=diff_tool_timeout)
+
+
+def canonical_dir(path, diff_tool=None, diff_file_name=None, diff_tool_timeout=None):
+ abs_path = os.path.abspath(path)
+ assert os.path.exists(abs_path), "Canonical path {} does not exist".format(path)
+ assert os.path.isdir(abs_path), "Path {} is not a directory".format(path)
+ if diff_file_name and not diff_tool:
+ raise Exception("diff_file_name can be only be used with diff_tool for canonical_dir")
+ tempdir = tempfile.mkdtemp()
+ safe_path = os.path.join(tempdir, os.path.basename(abs_path))
+ shutil.copytree(abs_path, safe_path)
+ return runtime._get_ya_plugin_instance().file(safe_path, diff_tool=diff_tool, diff_file_name=diff_file_name, diff_tool_timeout=diff_tool_timeout)
+
+
+def canonical_execute(
+ binary, args=None, check_exit_code=True,
+ shell=False, timeout=None, cwd=None,
+ env=None, stdin=None, stderr=None, creationflags=0,
+ file_name=None, save_locally=False, close_fds=False,
+ diff_tool=None, diff_file_name=None, diff_tool_timeout=None,
+):
+ """
+ Shortcut to execute a binary and canonize its stdout
+ :param binary: absolute path to the binary
+ :param args: binary arguments
+ :param check_exit_code: will raise ExecutionError if the command exits with non zero code
+ :param shell: use shell to run the command
+ :param timeout: execution timeout
+ :param cwd: working directory
+ :param env: command environment
+ :param stdin: command stdin
+ :param stderr: command stderr
+ :param creationflags: command creation flags
+ :param file_name: output file name. if not specified program name will be used
+ :param diff_tool: path to custome diff tool
+ :param diff_file_name: custom diff file name to create when diff is found
+ :param diff_tool_timeout: timeout for running diff tool
+ :return: object that can be canonized
+ """
+ if type(binary) == list:
+ command = binary
+ else:
+ command = [binary]
+ command += _prepare_args(args)
+ if shell:
+ command = " ".join(command)
+ execute_args = locals()
+ del execute_args["binary"]
+ del execute_args["args"]
+ del execute_args["file_name"]
+ del execute_args["save_locally"]
+ del execute_args["diff_tool"]
+ del execute_args["diff_file_name"]
+ del execute_args["diff_tool_timeout"]
+ if not file_name and stdin:
+ file_name = os.path.basename(stdin.name)
+ return _canonical_execute(process.execute, execute_args, file_name, save_locally, diff_tool, diff_file_name, diff_tool_timeout)
+
+
+def canonical_py_execute(
+ script_path, args=None, check_exit_code=True,
+ shell=False, timeout=None, cwd=None, env=None,
+ stdin=None, stderr=None, creationflags=0,
+ file_name=None, save_locally=False, close_fds=False,
+ diff_tool=None, diff_file_name=None, diff_tool_timeout=None,
+):
+ """
+ Shortcut to execute a python script and canonize its stdout
+ :param script_path: path to the script arcadia relative
+ :param args: script arguments
+ :param check_exit_code: will raise ExecutionError if the command exits with non zero code
+ :param shell: use shell to run the command
+ :param timeout: execution timeout
+ :param cwd: working directory
+ :param env: command environment
+ :param stdin: command stdin
+ :param stderr: command stderr
+ :param creationflags: command creation flags
+ :param file_name: output file name. if not specified program name will be used
+ :param diff_tool: path to custome diff tool
+ :param diff_file_name: custom diff file name to create when diff is found
+ :param diff_tool_timeout: timeout for running diff tool
+ :return: object that can be canonized
+ """
+ command = [runtime.source_path(script_path)] + _prepare_args(args)
+ if shell:
+ command = " ".join(command)
+ execute_args = locals()
+ del execute_args["script_path"]
+ del execute_args["args"]
+ del execute_args["file_name"]
+ del execute_args["save_locally"]
+ del execute_args["diff_tool"]
+ del execute_args["diff_file_name"]
+ del execute_args["diff_tool_timeout"]
+ return _canonical_execute(process.py_execute, execute_args, file_name, save_locally, diff_tool, diff_file_name, diff_tool_timeout)
+
+
+def _prepare_args(args):
+ if args is None:
+ args = []
+ if isinstance(args, six.string_types):
+ args = map(lambda a: a.strip(), args.split())
+ return args
+
+
+def _canonical_execute(excutor, kwargs, file_name, save_locally, diff_tool, diff_file_name, diff_tool_timeout):
+ res = excutor(**kwargs)
+ command = kwargs["command"]
+ file_name = file_name or process.get_command_name(command)
+ if file_name.endswith(".exe"):
+ file_name = os.path.splitext(file_name)[0] # don't want to bring windows stuff in file names
+ out_file_path = path.get_unique_file_path(runtime.output_path(), "{}.out.txt".format(file_name))
+ err_file_path = path.get_unique_file_path(runtime.output_path(), "{}.err.txt".format(file_name))
+
+ try:
+ os.makedirs(os.path.dirname(out_file_path))
+ except OSError:
+ pass
+
+ with open(out_file_path, "wb") as out_file:
+ yatest_logger.debug("Will store file in %s", out_file_path)
+ out_file.write(res.std_out)
+
+ if res.std_err:
+ with open(err_file_path, "wb") as err_file:
+ err_file.write(res.std_err)
+
+ return canonical_file(out_file_path, local=save_locally, diff_tool=diff_tool, diff_file_name=diff_file_name, diff_tool_timeout=diff_tool_timeout)
diff --git a/library/python/testing/yatest_common/yatest/common/environment.py b/library/python/testing/yatest_common/yatest/common/environment.py
new file mode 100644
index 0000000000..43f48d0958
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/environment.py
@@ -0,0 +1,5 @@
+# coding: utf-8
+
+
+def extend_env_var(env, name, value, sep=":"):
+ return sep.join(filter(None, [env.get(name), value]))
diff --git a/library/python/testing/yatest_common/yatest/common/errors.py b/library/python/testing/yatest_common/yatest/common/errors.py
new file mode 100644
index 0000000000..8c038fc381
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/errors.py
@@ -0,0 +1,20 @@
+import os
+import sys
+
+
+class RestartTestException(Exception):
+
+ def __init__(self, *args, **kwargs):
+ super(RestartTestException, self).__init__(*args, **kwargs)
+ sys.stderr.write("##restart-test##\n")
+ sys.stderr.flush()
+ os.environ["FORCE_EXIT_TESTSFAILED"] = "1"
+
+
+class InfrastructureException(Exception):
+
+ def __init__(self, *args, **kwargs):
+ super(InfrastructureException, self).__init__(*args, **kwargs)
+ sys.stderr.write("##infrastructure-error##\n")
+ sys.stderr.flush()
+ os.environ["FORCE_EXIT_TESTSFAILED"] = "1"
diff --git a/library/python/testing/yatest_common/yatest/common/legacy.py b/library/python/testing/yatest_common/yatest/common/legacy.py
new file mode 100644
index 0000000000..459972d253
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/legacy.py
@@ -0,0 +1,12 @@
+from . import canonical
+
+
+def old_canonical_file(output_file_name, storage_md5):
+ import yalibrary.svn
+ yalibrary.svn.run_svn([
+ 'export',
+ 'svn+ssh://arcadia.yandex.ru/arc/trunk/arcadia_tests_data/tests_canonical_output/' + storage_md5,
+ output_file_name,
+ "--force"
+ ])
+ return canonical.canonical_file(output_file_name)
diff --git a/library/python/testing/yatest_common/yatest/common/misc.py b/library/python/testing/yatest_common/yatest/common/misc.py
new file mode 100644
index 0000000000..20d3725ac9
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/misc.py
@@ -0,0 +1,19 @@
+import functools
+
+
+def first(it):
+ for d in it:
+ if d:
+ return d
+
+
+def lazy(func):
+ res = []
+
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ if not res:
+ res.append(func(*args, **kwargs))
+ return res[0]
+
+ return wrapper
diff --git a/library/python/testing/yatest_common/yatest/common/network.py b/library/python/testing/yatest_common/yatest/common/network.py
new file mode 100644
index 0000000000..37bcb1b8e0
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/network.py
@@ -0,0 +1,271 @@
+# coding=utf-8
+
+import os
+import errno
+import socket
+import random
+import logging
+import platform
+import threading
+
+import six
+
+UI16MAXVAL = (1 << 16) - 1
+logger = logging.getLogger(__name__)
+
+
+class PortManagerException(Exception):
+ pass
+
+
+class PortManager(object):
+ """
+ See documentation here
+
+ https://wiki.yandex-team.ru/yatool/test/#python-acquire-ports
+ """
+
+ def __init__(self, sync_dir=None):
+ self._sync_dir = sync_dir or os.environ.get('PORT_SYNC_PATH')
+ if self._sync_dir:
+ _makedirs(self._sync_dir)
+
+ self._valid_range = get_valid_port_range()
+ self._valid_port_count = self._count_valid_ports()
+ self._filelocks = {}
+ self._lock = threading.Lock()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.release()
+
+ def get_port(self, port=0):
+ '''
+ Gets free TCP port
+ '''
+ return self.get_tcp_port(port)
+
+ def get_tcp_port(self, port=0):
+ '''
+ Gets free TCP port
+ '''
+ return self._get_port(port, socket.SOCK_STREAM)
+
+ def get_udp_port(self, port=0):
+ '''
+ Gets free UDP port
+ '''
+ return self._get_port(port, socket.SOCK_DGRAM)
+
+ def get_tcp_and_udp_port(self, port=0):
+ '''
+ Gets one free port for use in both TCP and UDP protocols
+ '''
+ if port and self._no_random_ports():
+ return port
+
+ retries = 20
+ while retries > 0:
+ retries -= 1
+
+ result_port = self.get_tcp_port()
+ if not self.is_port_free(result_port, socket.SOCK_DGRAM):
+ self.release_port(result_port)
+ # Don't try to _capture_port(), it's already captured in the get_tcp_port()
+ return result_port
+ raise Exception('Failed to find port')
+
+ def release_port(self, port):
+ with self._lock:
+ self._release_port_no_lock(port)
+
+ def _release_port_no_lock(self, port):
+ filelock = self._filelocks.pop(port, None)
+ if filelock:
+ filelock.release()
+
+ def release(self):
+ with self._lock:
+ while self._filelocks:
+ _, filelock = self._filelocks.popitem()
+ if filelock:
+ filelock.release()
+
+ def get_port_range(self, start_port, count, random_start=True):
+ assert count > 0
+ if start_port and self._no_random_ports():
+ return start_port
+
+ candidates = []
+
+ def drop_candidates():
+ for port in candidates:
+ self._release_port_no_lock(port)
+ candidates[:] = []
+
+ with self._lock:
+ for attempts in six.moves.range(128):
+ for left, right in self._valid_range:
+ if right - left < count:
+ continue
+
+ if random_start:
+ start = random.randint(left, right - ((right - left) // 2))
+ else:
+ start = left
+ for probe_port in six.moves.range(start, right):
+ if self._capture_port_no_lock(probe_port, socket.SOCK_STREAM):
+ candidates.append(probe_port)
+ else:
+ drop_candidates()
+
+ if len(candidates) == count:
+ return candidates[0]
+ # Can't find required number of ports without gap in the current range
+ drop_candidates()
+
+ raise PortManagerException("Failed to find valid port range (start_port: {} count: {}) (range: {} used: {})".format(
+ start_port, count, self._valid_range, self._filelocks))
+
+ def _count_valid_ports(self):
+ res = 0
+ for left, right in self._valid_range:
+ res += right - left
+ assert res, ('There are no available valid ports', self._valid_range)
+ return res
+
+ def _get_port(self, port, sock_type):
+ if port and self._no_random_ports():
+ return port
+
+ if len(self._filelocks) >= self._valid_port_count:
+ raise PortManagerException("All valid ports are taken ({}): {}".format(self._valid_range, self._filelocks))
+
+ salt = random.randint(0, UI16MAXVAL)
+ for attempt in six.moves.range(self._valid_port_count):
+ probe_port = (salt + attempt) % self._valid_port_count
+
+ for left, right in self._valid_range:
+ if probe_port >= (right - left):
+ probe_port -= right - left
+ else:
+ probe_port += left
+ break
+ if not self._capture_port(probe_port, sock_type):
+ continue
+ return probe_port
+
+ raise PortManagerException("Failed to find valid port (range: {} used: {})".format(self._valid_range, self._filelocks))
+
+ def _capture_port(self, port, sock_type):
+ with self._lock:
+ return self._capture_port_no_lock(port, sock_type)
+
+ def is_port_free(self, port, sock_type=socket.SOCK_STREAM):
+ sock = socket.socket(socket.AF_INET6, sock_type)
+ try:
+ sock.bind(('::', port))
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ except socket.error as e:
+ if e.errno == errno.EADDRINUSE:
+ return False
+ raise
+ finally:
+ sock.close()
+ return True
+
+ def _capture_port_no_lock(self, port, sock_type):
+ if port in self._filelocks:
+ return False
+
+ filelock = None
+ if self._sync_dir:
+ # yatest.common should try to be hermetic and don't have peerdirs
+ # otherwise, PYTEST_SCRIPT (aka USE_ARCADIA_PYTHON=no) won't work
+ import library.python.filelock
+
+ filelock = library.python.filelock.FileLock(os.path.join(self._sync_dir, str(port)))
+ if not filelock.acquire(blocking=False):
+ return False
+ if self.is_port_free(port, sock_type):
+ self._filelocks[port] = filelock
+ return True
+ else:
+ filelock.release()
+ return False
+
+ if self.is_port_free(port, sock_type):
+ self._filelocks[port] = filelock
+ return True
+ if filelock:
+ filelock.release()
+ return False
+
+ def _no_random_ports(self):
+ return os.environ.get("NO_RANDOM_PORTS")
+
+
+def get_valid_port_range():
+ first_valid = 1025
+ last_valid = UI16MAXVAL
+
+ given_range = os.environ.get('VALID_PORT_RANGE')
+ if given_range and ':' in given_range:
+ return [list(int(x) for x in given_range.split(':', 2))]
+
+ first_eph, last_eph = get_ephemeral_range()
+ first_invalid = max(first_eph, first_valid)
+ last_invalid = min(last_eph, last_valid)
+
+ ranges = []
+ if first_invalid > first_valid:
+ ranges.append((first_valid, first_invalid - 1))
+ if last_invalid < last_valid:
+ ranges.append((last_invalid + 1, last_valid))
+ return ranges
+
+
+def get_ephemeral_range():
+ if platform.system() == 'Linux':
+ filename = "/proc/sys/net/ipv4/ip_local_port_range"
+ if os.path.exists(filename):
+ with open(filename) as afile:
+ data = afile.read(1024) # fix for musl
+ port_range = tuple(map(int, data.strip().split()))
+ if len(port_range) == 2:
+ return port_range
+ else:
+ logger.warning("Bad ip_local_port_range format: '%s'. Going to use IANA suggestion", data)
+ elif platform.system() == 'Darwin':
+ first = _sysctlbyname_uint("net.inet.ip.portrange.first")
+ last = _sysctlbyname_uint("net.inet.ip.portrange.last")
+ if first and last:
+ return first, last
+ # IANA suggestion
+ return (1 << 15) + (1 << 14), UI16MAXVAL
+
+
+def _sysctlbyname_uint(name):
+ try:
+ from ctypes import CDLL, c_uint, byref
+ from ctypes.util import find_library
+ except ImportError:
+ return
+
+ libc = CDLL(find_library("c"))
+ size = c_uint(0)
+ res = c_uint(0)
+ libc.sysctlbyname(name, None, byref(size), None, 0)
+ libc.sysctlbyname(name, byref(res), byref(size), None, 0)
+ return res.value
+
+
+def _makedirs(path):
+ try:
+ os.makedirs(path)
+ except OSError as e:
+ if e.errno == errno.EEXIST:
+ return
+ raise
diff --git a/library/python/testing/yatest_common/yatest/common/path.py b/library/python/testing/yatest_common/yatest/common/path.py
new file mode 100644
index 0000000000..6fed7dda8a
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/path.py
@@ -0,0 +1,90 @@
+# coding=utf-8
+
+import errno
+import os
+import shutil
+import contextlib
+
+import library.python.fs as lpf
+
+
+def replace_in_file(path, old, new):
+ """
+ Replace text occurrences in a file
+ :param path: path to the file
+ :param old: text to replace
+ :param new: replacement
+ """
+ with open(path) as fp:
+ content = fp.read()
+
+ lpf.ensure_removed(path)
+ with open(path, 'w') as fp:
+ fp.write(content.replace(old, new))
+
+
+@contextlib.contextmanager
+def change_dir(path):
+ old = os.getcwd()
+ try:
+ os.chdir(path)
+ yield path
+ finally:
+ os.chdir(old)
+
+
+def copytree(src, dst, symlinks=False, ignore=None, postprocessing=None):
+ '''
+ Copy an entire directory of files into an existing directory
+ instead of raising Exception what shtuil.copytree does
+ '''
+ if not os.path.exists(dst) and os.path.isdir(src):
+ os.makedirs(dst)
+ for item in os.listdir(src):
+ s = os.path.join(src, item)
+ d = os.path.join(dst, item)
+ if os.path.isdir(s):
+ shutil.copytree(s, d, symlinks, ignore)
+ else:
+ shutil.copy2(s, d)
+ if postprocessing:
+ postprocessing(dst, False)
+ for root, dirs, files in os.walk(dst):
+ for path in dirs:
+ postprocessing(os.path.join(root, path), False)
+ for path in files:
+ postprocessing(os.path.join(root, path), True)
+
+
+def get_unique_file_path(dir_path, file_pattern, create_file=True, max_suffix=10000):
+ def atomic_file_create(path):
+ try:
+ fd = os.open(path, os.O_CREAT | os.O_EXCL, 0o644)
+ os.close(fd)
+ return True
+ except OSError as e:
+ if e.errno in [errno.EEXIST, errno.EISDIR, errno.ETXTBSY]:
+ return False
+ # Access issue with file itself, not parent directory.
+ if e.errno == errno.EACCES and os.path.exists(path):
+ return False
+ raise e
+
+ def atomic_dir_create(path):
+ try:
+ os.mkdir(path)
+ return True
+ except OSError as e:
+ if e.errno == errno.EEXIST:
+ return False
+ raise e
+
+ file_path = os.path.join(dir_path, file_pattern)
+ lpf.ensure_dir(os.path.dirname(file_path))
+ file_counter = 0
+ handler = atomic_file_create if create_file else atomic_dir_create
+ while os.path.exists(file_path) or not handler(file_path):
+ file_path = os.path.join(dir_path, file_pattern + ".{}".format(file_counter))
+ file_counter += 1
+ assert file_counter < max_suffix
+ return file_path
diff --git a/library/python/testing/yatest_common/yatest/common/process.py b/library/python/testing/yatest_common/yatest/common/process.py
new file mode 100644
index 0000000000..a8bcc21f51
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/process.py
@@ -0,0 +1,733 @@
+# coding: utf-8
+
+import os
+import re
+import time
+import signal
+import shutil
+import logging
+import tempfile
+import subprocess
+import errno
+import distutils.version
+
+import six
+
+try:
+ # yatest.common should try to be hermetic, otherwise, PYTEST_SCRIPT (aka USE_ARCADIA_PYTHON=no) won't work.
+ import library.python.cores as cores
+except ImportError:
+ cores = None
+
+from . import runtime
+from . import path
+from . import environment
+
+
+MAX_OUT_LEN = 1000 * 1000 # 1 mb
+MAX_MESSAGE_LEN = 1500
+SANITIZER_ERROR_PATTERN = br": ([A-Z][\w]+Sanitizer)"
+GLIBC_PATTERN = re.compile(r"\S+@GLIBC_([0-9.]+)")
+yatest_logger = logging.getLogger("ya.test")
+
+
+def truncate(s, size):
+ if s is None:
+ return None
+ elif len(s) <= size:
+ return s
+ else:
+ return (b'...' if isinstance(s, bytes) else '...') + s[-(size - 3):]
+
+
+def get_command_name(command):
+ return os.path.basename(command.split()[0] if isinstance(command, six.string_types) else command[0])
+
+
+class ExecutionError(Exception):
+
+ def __init__(self, execution_result):
+ if not isinstance(execution_result.command, six.string_types):
+ command = " ".join(str(arg) for arg in execution_result.command)
+ else:
+ command = execution_result.command
+ message = "Command '{command}' has failed with code {code}.\nErrors:\n{err}\n".format(
+ command=command,
+ code=execution_result.exit_code,
+ err=_format_error(execution_result.std_err))
+ if cores:
+ if execution_result.backtrace:
+ message += "Backtrace:\n[[rst]]{}[[bad]]\n".format(cores.colorize_backtrace(execution_result._backtrace))
+ else:
+ message += "Backtrace is not available: module cores isn't available"
+
+ super(ExecutionError, self).__init__(message)
+ self.execution_result = execution_result
+
+
+class TimeoutError(Exception):
+ pass
+
+
+class ExecutionTimeoutError(TimeoutError):
+ def __init__(self, execution_result, *args, **kwargs):
+ super(ExecutionTimeoutError, self).__init__(args, kwargs)
+ self.execution_result = execution_result
+
+
+class InvalidExecutionStateError(Exception):
+ pass
+
+
+class SignalInterruptionError(Exception):
+ def __init__(self, message=None):
+ super(SignalInterruptionError, self).__init__(message)
+ self.res = None
+
+
+class InvalidCommandError(Exception):
+ pass
+
+
+class _Execution(object):
+
+ def __init__(self, command, process, out_file, err_file, process_progress_listener=None, cwd=None, collect_cores=True, check_sanitizer=True, started=0, user_stdout=False, user_stderr=False):
+ self._command = command
+ self._process = process
+ self._out_file = out_file
+ self._err_file = err_file
+ self._std_out = None
+ self._std_err = None
+ self._elapsed = None
+ self._start = time.time()
+ self._process_progress_listener = process_progress_listener
+ self._cwd = cwd or os.getcwd()
+ self._collect_cores = collect_cores
+ self._backtrace = ''
+ self._check_sanitizer = check_sanitizer
+ self._metrics = {}
+ self._started = started
+ self._user_stdout = bool(user_stdout)
+ self._user_stderr = bool(user_stderr)
+ self._exit_code = None
+ if process_progress_listener:
+ process_progress_listener.open(command, process, out_file, err_file)
+
+ @property
+ def running(self):
+ return self._process.poll() is None
+
+ def kill(self):
+ if self.running:
+ self._save_outputs(False)
+ _kill_process_tree(self._process.pid)
+ self._clean_files()
+ # DEVTOOLS-2347
+ yatest_logger.debug("Process status before wait_for: %s", self.running)
+ try:
+ wait_for(lambda: not self.running, timeout=5, fail_message="Could not kill process {}".format(self._process.pid), sleep_time=.1)
+ except TimeoutError:
+ yatest_logger.debug("Process status after wait_for: %s", self.running)
+ yatest_logger.debug("Process %d info: %s", self._process.pid, _get_proc_tree_info([self._process.pid]))
+ raise
+ else:
+ raise InvalidExecutionStateError("Cannot kill a stopped process")
+
+ def terminate(self):
+ if self.running:
+ self._process.terminate()
+
+ @property
+ def process(self):
+ return self._process
+
+ @property
+ def command(self):
+ return self._command
+
+ @property
+ def returncode(self):
+ return self.exit_code
+
+ @property
+ def exit_code(self):
+ """
+ Deprecated, use returncode
+ """
+ if self._exit_code is None:
+ self._exit_code = self._process.returncode
+ return self._exit_code
+
+ @property
+ def stdout(self):
+ return self.std_out
+
+ @property
+ def std_out(self):
+ """
+ Deprecated, use stdout
+ """
+ if self._std_out is not None:
+ return self._std_out
+ if self._process.stdout and not self._user_stdout:
+ self._std_out = self._process.stdout.read()
+ return self._std_out
+
+ @property
+ def stderr(self):
+ return self.std_err
+
+ @property
+ def std_err(self):
+ """
+ Deprecated, use stderr
+ """
+ if self._std_err is not None:
+ return self._std_err
+ if self._process.stderr and not self._user_stderr:
+ self._std_err = self._process.stderr.read()
+ return self._std_err
+
+ @property
+ def elapsed(self):
+ return self._elapsed
+
+ @property
+ def backtrace(self):
+ return self._backtrace
+
+ @property
+ def metrics(self):
+ return self._metrics
+
+ def _save_outputs(self, clean_files=True):
+ if self._process_progress_listener:
+ self._process_progress_listener()
+ self._process_progress_listener.close()
+ if not self._user_stdout:
+ if self._out_file is None:
+ pass
+ elif self._out_file != subprocess.PIPE:
+ self._out_file.flush()
+ self._out_file.seek(0, os.SEEK_SET)
+ self._std_out = self._out_file.read()
+ else:
+ self._std_out = self._process.stdout.read()
+ if not self._user_stderr:
+ if self._err_file is None:
+ pass
+ elif self._err_file != subprocess.PIPE:
+ self._err_file.flush()
+ self._err_file.seek(0, os.SEEK_SET)
+ self._std_err = self._err_file.read()
+ else:
+ self._std_err = self._process.stderr.read()
+
+ if clean_files:
+ self._clean_files()
+ yatest_logger.debug("Command (pid %s) rc: %s", self._process.pid, self.exit_code)
+ yatest_logger.debug("Command (pid %s) elapsed time (sec): %s", self._process.pid, self.elapsed)
+ if self._metrics:
+ for key, value in six.iteritems(self._metrics):
+ yatest_logger.debug("Command (pid %s) %s: %s", self._process.pid, key, value)
+
+ # Since this code is Python2/3 compatible, we don't know is _std_out/_std_err is real bytes or bytes-str.
+ printable_std_out, err = _try_convert_bytes_to_string(self._std_out)
+ if err:
+ yatest_logger.debug("Got error during parse process stdout: %s", err)
+ yatest_logger.debug("stdout will be displayed as raw bytes.")
+ printable_std_err, err = _try_convert_bytes_to_string(self._std_err)
+ if err:
+ yatest_logger.debug("Got error during parse process stderr: %s", err)
+ yatest_logger.debug("stderr will be displayed as raw bytes.")
+
+ yatest_logger.debug("Command (pid %s) output:\n%s", self._process.pid, truncate(printable_std_out, MAX_OUT_LEN))
+ yatest_logger.debug("Command (pid %s) errors:\n%s", self._process.pid, truncate(printable_std_err, MAX_OUT_LEN))
+
+ def _clean_files(self):
+ if self._err_file and not self._user_stderr and self._err_file != subprocess.PIPE:
+ self._err_file.close()
+ self._err_file = None
+ if self._out_file and not self._user_stdout and self._out_file != subprocess.PIPE:
+ self._out_file.close()
+ self._out_file = None
+
+ def _recover_core(self):
+ core_path = cores.recover_core_dump_file(self.command[0], self._cwd, self.process.pid)
+ if core_path:
+ # Core dump file recovering may be disabled (for distbuild for example) - produce only bt
+ store_cores = runtime._get_ya_config().collect_cores
+ if store_cores:
+ new_core_path = path.get_unique_file_path(runtime.output_path(), "{}.{}.core".format(os.path.basename(self.command[0]), self._process.pid))
+ # Copy core dump file, because it may be overwritten
+ yatest_logger.debug("Coping core dump file from '%s' to the '%s'", core_path, new_core_path)
+ shutil.copyfile(core_path, new_core_path)
+ core_path = new_core_path
+
+ bt_filename = None
+ pbt_filename = None
+
+ if os.path.exists(runtime.gdb_path()):
+ self._backtrace = cores.get_gdb_full_backtrace(self.command[0], core_path, runtime.gdb_path())
+ bt_filename = path.get_unique_file_path(runtime.output_path(), "{}.{}.backtrace".format(os.path.basename(self.command[0]), self._process.pid))
+ with open(bt_filename, "wb") as afile:
+ afile.write(six.ensure_binary(self._backtrace))
+ # generate pretty html version of backtrace aka Tri Korochki
+ pbt_filename = bt_filename + ".html"
+ backtrace_to_html(bt_filename, pbt_filename)
+
+ if store_cores:
+ runtime._register_core(os.path.basename(self.command[0]), self.command[0], core_path, bt_filename, pbt_filename)
+ else:
+ runtime._register_core(os.path.basename(self.command[0]), None, None, bt_filename, pbt_filename)
+
+ def wait(self, check_exit_code=True, timeout=None, on_timeout=None):
+ def _wait():
+ finished = None
+ interrupted = False
+ try:
+ if hasattr(os, "wait4"):
+ try:
+ if hasattr(subprocess, "_eintr_retry_call"):
+ pid, sts, rusage = subprocess._eintr_retry_call(os.wait4, self._process.pid, 0)
+ else:
+ # PEP 475
+ pid, sts, rusage = os.wait4(self._process.pid, 0)
+ finished = time.time()
+ self._process._handle_exitstatus(sts)
+ for field in [
+ "ru_idrss",
+ "ru_inblock",
+ "ru_isrss",
+ "ru_ixrss",
+ "ru_majflt",
+ "ru_maxrss",
+ "ru_minflt",
+ "ru_msgrcv",
+ "ru_msgsnd",
+ "ru_nivcsw",
+ "ru_nsignals",
+ "ru_nswap",
+ "ru_nvcsw",
+ "ru_oublock",
+ "ru_stime",
+ "ru_utime",
+ ]:
+ if hasattr(rusage, field):
+ self._metrics[field.replace("ru_", "")] = getattr(rusage, field)
+ except OSError as exc:
+
+ if exc.errno == errno.ECHILD:
+ yatest_logger.debug("Process resource usage is not available as process finished before wait4 was called")
+ else:
+ raise
+ except SignalInterruptionError:
+ interrupted = True
+ raise
+ finally:
+ if not interrupted:
+ self._process.wait() # this has to be here unconditionally, so that all process properties are set
+
+ if not finished:
+ finished = time.time()
+ self._metrics["wtime"] = round(finished - self._started, 3)
+
+ try:
+ if timeout:
+ process_is_finished = lambda: not self.running
+ fail_message = "Command '%s' stopped by %d seconds timeout" % (self._command, timeout)
+ try:
+ wait_for(process_is_finished, timeout, fail_message, sleep_time=0.1, on_check_condition=self._process_progress_listener)
+ except TimeoutError as e:
+ if on_timeout:
+ yatest_logger.debug("Calling user specified on_timeout function")
+ try:
+ on_timeout(self, timeout)
+ except Exception:
+ yatest_logger.exception("Exception while calling on_timeout")
+ raise ExecutionTimeoutError(self, str(e))
+ # Wait should be always called here, it finalizes internal states of its process and sets up return code
+ _wait()
+ except BaseException as e:
+ _kill_process_tree(self._process.pid)
+ _wait()
+ yatest_logger.debug("Command exception: %s", e)
+ raise
+ finally:
+ self._elapsed = time.time() - self._start
+ self._save_outputs()
+ self.verify_no_coredumps()
+
+ self._finalise(check_exit_code)
+
+ def _finalise(self, check_exit_code):
+ # Set the signal (negative number) which caused the process to exit
+ if check_exit_code and self.exit_code != 0:
+ yatest_logger.error("Execution failed with exit code: %s\n\t,std_out:%s\n\tstd_err:%s\n",
+ self.exit_code, truncate(self.std_out, MAX_OUT_LEN), truncate(self.std_err, MAX_OUT_LEN))
+ raise ExecutionError(self)
+
+ # Don't search for sanitize errors if stderr was redirected
+ self.verify_sanitize_errors()
+
+ def verify_no_coredumps(self):
+ """
+ Verify there is no coredump from this binary. If there is then report backtrace.
+ """
+ if self.exit_code < 0 and self._collect_cores:
+ if cores:
+ try:
+ self._recover_core()
+ except Exception:
+ yatest_logger.exception("Exception while recovering core")
+ else:
+ yatest_logger.warning("Core dump file recovering is skipped: module cores isn't available")
+
+ def verify_sanitize_errors(self):
+ """
+ Verify there are no sanitizer (ASAN, MSAN, TSAN, etc) errors for this binary. If there are any report them.
+ """
+ if self._std_err and self._check_sanitizer and runtime._get_ya_config().sanitizer_extra_checks:
+ build_path = runtime.build_path()
+ if self.command[0].startswith(build_path):
+ match = re.search(SANITIZER_ERROR_PATTERN, self._std_err)
+ if match:
+ yatest_logger.error("%s sanitizer found errors:\n\tstd_err:%s\n", match.group(1), truncate(self.std_err, MAX_OUT_LEN))
+ raise ExecutionError(self)
+ else:
+ yatest_logger.debug("No sanitizer errors found")
+ else:
+ yatest_logger.debug("'%s' doesn't belong to '%s' - no check for sanitize errors", self.command[0], build_path)
+
+
+def on_timeout_gen_coredump(exec_obj, _):
+ """
+ Function can be passed to the execute(..., timeout=X, on_timeout=on_timeout_gen_coredump)
+ to generate core dump file, backtrace ahd html-version of the backtrace in case of timeout.
+ All files will be available in the testing_out_stuff and via links.
+ """
+ try:
+ os.kill(exec_obj.process.pid, signal.SIGQUIT)
+ except OSError:
+ # process might be already terminated
+ pass
+
+
+def execute(
+ command, check_exit_code=True,
+ shell=False, timeout=None,
+ cwd=None, env=None,
+ stdin=None, stdout=None, stderr=None,
+ creationflags=0, wait=True,
+ process_progress_listener=None, close_fds=False,
+ collect_cores=True, check_sanitizer=True, preexec_fn=None, on_timeout=None,
+ executor=_Execution,
+):
+ """
+ Executes a command
+ :param command: command: can be a list of arguments or a string
+ :param check_exit_code: will raise ExecutionError if the command exits with non zero code
+ :param shell: use shell to run the command
+ :param timeout: execution timeout
+ :param cwd: working directory
+ :param env: command environment
+ :param stdin: command stdin
+ :param stdout: command stdout
+ :param stderr: command stderr
+ :param creationflags: command creation flags
+ :param wait: should wait until the command finishes
+ :param process_progress_listener=object that is polled while execution is in progress
+ :param close_fds: subrpocess.Popen close_fds args
+ :param collect_cores: recover core dump files if shell == False
+ :param check_sanitizer: raise ExecutionError if stderr contains sanitize errors
+ :param preexec_fn: subrpocess.Popen preexec_fn arg
+ :param on_timeout: on_timeout(<execution object>, <timeout value>) callback
+
+ :return _Execution: Execution object
+ """
+ if env is None:
+ env = os.environ.copy()
+ else:
+ # Certain environment variables must be present for programs to work properly.
+ # For more info see DEVTOOLSSUPPORT-4907
+ mandatory_env_name = 'YA_MANDATORY_ENV_VARS'
+ mandatory_vars = env.get(mandatory_env_name, os.environ.get(mandatory_env_name)) or ''
+ if mandatory_vars:
+ env[mandatory_env_name] = mandatory_vars
+ mandatory_system_vars = filter(None, mandatory_vars.split(':'))
+ else:
+ mandatory_system_vars = ['TMPDIR']
+
+ for var in mandatory_system_vars:
+ if var not in env and var in os.environ:
+ env[var] = os.environ[var]
+
+ if not wait and timeout is not None:
+ raise ValueError("Incompatible arguments 'timeout' and wait=False")
+
+ # if subprocess.PIPE in [stdout, stderr]:
+ # raise ValueError("Don't use pipe to obtain stream data - it may leads to the deadlock")
+
+ def get_out_stream(stream, default_name):
+ if stream is None:
+ # No stream is supplied: open new temp file
+ return _get_command_output_file(command, default_name), False
+
+ if isinstance(stream, six.string_types):
+ # User filename is supplied: open file for writing
+ return open(stream, 'wb+'), stream.startswith('/dev/')
+
+ # Open file or PIPE sentinel is supplied
+ is_pipe = stream == subprocess.PIPE
+ return stream, not is_pipe
+
+ # to be able to have stdout/stderr and track the process time execution, we don't use subprocess.PIPE,
+ # as it can cause processes hangs, but use tempfiles instead
+ out_file, user_stdout = get_out_stream(stdout, 'out')
+ err_file, user_stderr = get_out_stream(stderr, 'err')
+ in_file = stdin
+
+ if shell and type(command) == list:
+ command = " ".join(command)
+
+ if shell:
+ collect_cores = False
+ check_sanitizer = False
+ else:
+ if isinstance(command, (list, tuple)):
+ executable = command[0]
+ else:
+ executable = command
+ if os.path.isabs(executable):
+ if not os.path.isfile(executable) and not os.path.isfile(executable + ".exe"):
+ exists = os.path.exists(executable)
+ if exists:
+ stat = os.stat(executable)
+ else:
+ stat = None
+ raise InvalidCommandError("Target program is not a file: {} (exists: {} stat: {})".format(executable, exists, stat))
+ if not os.access(executable, os.X_OK) and not os.access(executable + ".exe", os.X_OK):
+ raise InvalidCommandError("Target program is not executable: {}".format(executable))
+
+ if check_sanitizer:
+ env["LSAN_OPTIONS"] = environment.extend_env_var(os.environ, "LSAN_OPTIONS", "exitcode=100")
+
+ if stdin:
+ name = "PIPE" if stdin == subprocess.PIPE else stdin.name
+ yatest_logger.debug("Executing '%s' with input '%s' in '%s'", command, name, cwd)
+ else:
+ yatest_logger.debug("Executing '%s' in '%s'", command, cwd)
+ # XXX
+
+ started = time.time()
+ process = subprocess.Popen(
+ command, shell=shell, universal_newlines=True,
+ stdout=out_file, stderr=err_file, stdin=in_file,
+ cwd=cwd, env=env, creationflags=creationflags, close_fds=close_fds, preexec_fn=preexec_fn,
+ )
+ yatest_logger.debug("Command pid: %s", process.pid)
+
+ res = executor(command, process, out_file, err_file, process_progress_listener, cwd, collect_cores, check_sanitizer, started, user_stdout=user_stdout, user_stderr=user_stderr)
+ if wait:
+ res.wait(check_exit_code, timeout, on_timeout)
+ return res
+
+
+def _get_command_output_file(cmd, ext):
+ parts = [get_command_name(cmd)]
+ if 'YA_RETRY_INDEX' in os.environ:
+ parts.append('retry{}'.format(os.environ.get('YA_RETRY_INDEX')))
+ if int(os.environ.get('YA_SPLIT_COUNT', '0')) > 1:
+ parts.append('chunk{}'.format(os.environ.get('YA_SPLIT_INDEX', '0')))
+
+ filename = '.'.join(parts + [ext])
+ try:
+ # if execution is performed from test, save out / err to the test logs dir
+ import yatest.common
+ import library.python.pytest.plugins.ya
+ if getattr(library.python.pytest.plugins.ya, 'pytest_config', None) is None:
+ raise ImportError("not in test")
+ filename = path.get_unique_file_path(yatest.common.output_path(), filename)
+ yatest_logger.debug("Command %s will be placed to %s", ext, os.path.basename(filename))
+ return open(filename, "wb+")
+ except ImportError:
+ return tempfile.NamedTemporaryFile(delete=False, suffix=filename)
+
+
+def _get_proc_tree_info(pids):
+ if os.name == 'nt':
+ return 'Not supported'
+ else:
+ stdout, _ = subprocess.Popen(["/bin/ps", "-wufp"] + [str(p) for p in pids], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+ return stdout
+
+
+def py_execute(
+ command, check_exit_code=True,
+ shell=False, timeout=None,
+ cwd=None, env=None,
+ stdin=None, stdout=None, stderr=None,
+ creationflags=0, wait=True,
+ process_progress_listener=None, close_fds=False
+):
+ """
+ Executes a command with the arcadia python
+ :param command: command to pass to python
+ :param check_exit_code: will raise ExecutionError if the command exits with non zero code
+ :param shell: use shell to run the command
+ :param timeout: execution timeout
+ :param cwd: working directory
+ :param env: command environment
+ :param stdin: command stdin
+ :param stdout: command stdout
+ :param stderr: command stderr
+ :param creationflags: command creation flags
+ :param wait: should wait until the command finishes
+ :param process_progress_listener=object that is polled while execution is in progress
+ :return _Execution: Execution object
+ """
+ if isinstance(command, six.string_types):
+ command = [command]
+ command = [runtime.python_path()] + command
+ if shell:
+ command = " ".join(command)
+ return execute(**locals())
+
+
+def _format_error(error):
+ return truncate(error, MAX_MESSAGE_LEN)
+
+
+def wait_for(check_function, timeout, fail_message="", sleep_time=1.0, on_check_condition=None):
+ """
+ Tries to execute `check_function` for `timeout` seconds.
+ Continue until function returns nonfalse value.
+ If function doesn't return nonfalse value for `timeout` seconds
+ OperationTimeoutException is raised.
+ Return first nonfalse result returned by `checkFunction`.
+ """
+ if sleep_time <= 0:
+ raise ValueError("Incorrect sleep time value {}".format(sleep_time))
+ if timeout < 0:
+ raise ValueError("Incorrect timeout value {}".format(timeout))
+ start = time.time()
+ while start + timeout > time.time():
+ if on_check_condition:
+ on_check_condition()
+
+ res = check_function()
+ if res:
+ return res
+ time.sleep(sleep_time)
+
+ message = "{} second(s) wait timeout has expired".format(timeout)
+ if fail_message:
+ message += ": {}".format(fail_message)
+ raise TimeoutError(truncate(message, MAX_MESSAGE_LEN))
+
+
+def _kill_process_tree(process_pid, target_pid_signal=None):
+ """
+ Kills child processes, req. Note that psutil should be installed
+ @param process_pid: parent id to search for descendants
+ """
+ yatest_logger.debug("Killing process %s", process_pid)
+ if os.name == 'nt':
+ _win_kill_process_tree(process_pid)
+ else:
+ _nix_kill_process_tree(process_pid, target_pid_signal)
+
+
+def _nix_get_proc_children(pid):
+ try:
+ cmd = ["pgrep", "-P", str(pid)]
+ return [int(p) for p in subprocess.check_output(cmd).split()]
+ except Exception:
+ return []
+
+
+def _get_binname(pid):
+ try:
+ return os.path.basename(os.readlink('/proc/{}/exe'.format(pid)))
+ except Exception as e:
+ return "error({})".format(e)
+
+
+def _nix_kill_process_tree(pid, target_pid_signal=None):
+ """
+ Kills the process tree.
+ """
+ yatest_logger.debug("Killing process tree for pid {} (bin:'{}')".format(pid, _get_binname(pid)))
+
+ def try_to_send_signal(pid, sig):
+ try:
+ os.kill(pid, sig)
+ yatest_logger.debug("Sent signal %d to the pid %d", sig, pid)
+ except Exception as exc:
+ yatest_logger.debug("Error while sending signal {sig} to pid {pid}: {error}".format(sig=sig, pid=pid, error=str(exc)))
+
+ try_to_send_signal(pid, signal.SIGSTOP) # Stop the process to prevent it from starting any child processes.
+
+ # Get the child process PID list.
+ child_pids = _nix_get_proc_children(pid)
+ # Stop the child processes.
+ for child_pid in child_pids:
+ try:
+ # Kill the child recursively.
+ _kill_process_tree(int(child_pid))
+ except Exception as e:
+ # Skip the error and continue killing.
+ yatest_logger.debug("Killing child pid {pid} failed: {error}".format(pid=child_pid, error=e))
+ continue
+
+ try_to_send_signal(pid, target_pid_signal or signal.SIGKILL) # Kill the root process.
+
+ # sometimes on freebsd sigkill cannot kill the process and either sigkill or sigcont should be sent
+ # https://www.mail-archive.com/freebsd-hackers@freebsd.org/msg159646.html
+ try_to_send_signal(pid, signal.SIGCONT)
+
+
+def _win_kill_process_tree(pid):
+ subprocess.call(['taskkill', '/F', '/T', '/PID', str(pid)])
+
+
+def _run_readelf(binary_path):
+ return str(subprocess.check_output([runtime.binary_path('contrib/python/pyelftools/readelf/readelf'), '-s', runtime.binary_path(binary_path)]))
+
+
+def check_glibc_version(binary_path):
+ lucid_glibc_version = distutils.version.LooseVersion("2.11")
+
+ for l in _run_readelf(binary_path).split('\n'):
+ match = GLIBC_PATTERN.search(l)
+ if not match:
+ continue
+ assert distutils.version.LooseVersion(match.group(1)) <= lucid_glibc_version, match.group(0)
+
+
+def backtrace_to_html(bt_filename, output):
+ try:
+ from library.python import coredump_filter
+ with open(output, "wb") as afile:
+ coredump_filter.filter_stackdump(bt_filename, stream=afile)
+ except ImportError as e:
+ yatest_logger.debug("Failed to import coredump_filter: %s", e)
+ with open(output, "wb") as afile:
+ afile.write("<html>Failed to import coredump_filter in USE_ARCADIA_PYTHON=no mode</html>")
+
+
+def _try_convert_bytes_to_string(source):
+ """ Function is necessary while this code Python2/3 compatible, because bytes in Python3 is a real bytes and in Python2 is not """
+ # Bit ugly typecheck, because in Python2 isinstance(str(), bytes) and "type(str()) is bytes" working as True as well
+ if 'bytes' not in str(type(source)):
+ # We already got not bytes. Nothing to do here.
+ return source, False
+
+ result = source
+ error = False
+ try:
+ result = source.decode(encoding='utf-8')
+ except ValueError as e:
+ error = e
+
+ return result, error
diff --git a/library/python/testing/yatest_common/yatest/common/runtime.py b/library/python/testing/yatest_common/yatest/common/runtime.py
new file mode 100644
index 0000000000..e55e193446
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/runtime.py
@@ -0,0 +1,343 @@
+import errno
+import functools
+import json
+import os
+import threading
+
+import six
+
+
+_lock = threading.Lock()
+
+
+def _get_ya_config():
+ try:
+ import library.python.pytest.plugins.ya as ya_plugin
+ if ya_plugin.pytest_config is not None:
+ return ya_plugin.pytest_config
+ import pytest
+ return pytest.config
+ except (ImportError, AttributeError):
+ try:
+ import library.python.testing.recipe
+ if library.python.testing.recipe.ya:
+ return library.python.testing.recipe
+ except (ImportError, AttributeError):
+ pass
+ raise NotImplementedError("yatest.common.* is only available from the testing runtime")
+
+
+def _get_ya_plugin_instance():
+ return _get_ya_config().ya
+
+
+def _norm_path(path):
+ if path is None:
+ return None
+ assert isinstance(path, six.string_types)
+ if "\\" in path:
+ raise AssertionError("path {} contains Windows seprators \\ - replace them with '/'".format(path))
+ return os.path.normpath(path)
+
+
+def _join_path(main_path, path):
+ if not path:
+ return main_path
+ return os.path.join(main_path, _norm_path(path))
+
+
+def not_test(func):
+ """
+ Mark any function as not a test for py.test
+ :param func:
+ :return:
+ """
+ @functools.wraps(func)
+ def wrapper(*args, **kwds):
+ return func(*args, **kwds)
+ setattr(wrapper, '__test__', False)
+ return wrapper
+
+
+def source_path(path=None):
+ """
+ Get source path inside arcadia
+ :param path: path arcadia relative, e.g. yatest.common.source_path('devtools/ya')
+ :return: absolute path to the source folder
+ """
+ return _join_path(_get_ya_plugin_instance().source_root, path)
+
+
+def build_path(path=None):
+ """
+ Get path inside build directory
+ :param path: path relative to the build directory, e.g. yatest.common.build_path('devtools/ya/bin')
+ :return: absolute path inside build directory
+ """
+ return _join_path(_get_ya_plugin_instance().build_root, path)
+
+
+def java_path():
+ """
+ [DEPRECATED] Get path to java
+ :return: absolute path to java
+ """
+ from . import runtime_java
+ return runtime_java.get_java_path(binary_path(os.path.join('contrib', 'tools', 'jdk')))
+
+
+def java_home():
+ """
+ Get jdk directory path
+ """
+ from . import runtime_java
+ jdk_dir = runtime_java.get_build_java_dir(binary_path('jdk'))
+ if not jdk_dir:
+ raise Exception("Cannot find jdk - make sure 'jdk' is added to the DEPENDS section and exists for the current platform")
+ return jdk_dir
+
+
+def java_bin():
+ """
+ Get path to the java binary
+ Requires DEPENDS(jdk)
+ """
+ return os.path.join(java_home(), "bin", "java")
+
+
+def data_path(path=None):
+ """
+ Get path inside arcadia_tests_data directory
+ :param path: path relative to the arcadia_tests_data directory, e.g. yatest.common.data_path("pers/rerank_service")
+ :return: absolute path inside arcadia_tests_data
+ """
+ return _join_path(_get_ya_plugin_instance().data_root, path)
+
+
+def output_path(path=None):
+ """
+ Get path inside the current test suite output dir.
+ Placing files to this dir guarantees that files will be accessible after the test suite execution.
+ :param path: path relative to the test suite output dir
+ :return: absolute path inside the test suite output dir
+ """
+ return _join_path(_get_ya_plugin_instance().output_dir, path)
+
+
+def ram_drive_path(path=None):
+ """
+ :param path: path relative to the ram drive.
+ :return: absolute path inside the ram drive directory or None if no ram drive was provided by environment.
+ """
+ if 'YA_TEST_RAM_DRIVE_PATH' in os.environ:
+ return _join_path(os.environ['YA_TEST_RAM_DRIVE_PATH'], path)
+
+
+def output_ram_drive_path(path=None):
+ """
+ Returns path inside ram drive directory which will be saved in the testing_out_stuff directory after testing.
+ Returns None if no ram drive was provided by environment.
+ :param path: path relative to the output ram drive directory
+ """
+ if 'YA_TEST_OUTPUT_RAM_DRIVE_PATH' in os.environ:
+ return _join_path(os.environ['YA_TEST_OUTPUT_RAM_DRIVE_PATH'], path)
+
+
+def binary_path(path=None):
+ """
+ Get path to the built binary
+ :param path: path to the binary relative to the build directory e.g. yatest.common.binary_path('devtools/ya/bin/ya-bin')
+ :return: absolute path to the binary
+ """
+ path = _norm_path(path)
+ return _get_ya_plugin_instance().get_binary(path)
+
+
+def work_path(path=None):
+ """
+ Get path inside the current test suite working directory. Creating files in the work directory does not guarantee
+ that files will be accessible after the test suite execution
+ :param path: path relative to the test suite working dir
+ :return: absolute path inside the test suite working dir
+ """
+ return _join_path(
+ os.environ.get("TEST_WORK_PATH") or
+ _get_ya_plugin_instance().get_context("work_path") or
+ os.getcwd(),
+ path)
+
+
+def python_path():
+ """
+ Get path to the arcadia python.
+
+ Warn: if you are using build with system python (-DUSE_SYSTEM_PYTHON=X) beware that some python bundles
+ are built in a stripped-down form that is needed for building, not running tests.
+ See comments in the file below to find out which version of python is compatible with tests.
+ https://a.yandex-team.ru/arc/trunk/arcadia/build/platform/python/resources.inc
+ :return: absolute path to python
+ """
+ return _get_ya_plugin_instance().python_path
+
+
+def valgrind_path():
+ """
+ Get path to valgrind
+ :return: absolute path to valgrind
+ """
+ return _get_ya_plugin_instance().valgrind_path
+
+
+def get_param(key, default=None):
+ """
+ Get arbitrary parameter passed via command line
+ :param key: key
+ :param default: default value
+ :return: parameter value or the default
+ """
+ return _get_ya_plugin_instance().get_param(key, default)
+
+
+def get_param_dict_copy():
+ """
+ Return copy of dictionary with all parameters. Changes to this dictionary do *not* change parameters.
+
+ :return: copy of dictionary with all parameters
+ """
+ return _get_ya_plugin_instance().get_param_dict_copy()
+
+
+@not_test
+def test_output_path(path=None):
+ """
+ Get dir in the suite output_path for the current test case
+ """
+ test_out_dir = os.path.splitext(_get_ya_config().current_test_log_path)[0]
+ try:
+ os.makedirs(test_out_dir)
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ raise
+ return _join_path(test_out_dir, path)
+
+
+def project_path(path=None):
+ """
+ Get path in build root relating to build_root/project path
+ """
+ return _join_path(os.path.join(build_path(), context.project_path), path)
+
+
+def gdb_path():
+ """
+ Get path to the gdb
+ """
+ return _get_ya_plugin_instance().gdb_path
+
+
+def c_compiler_path():
+ """
+ Get path to the gdb
+ """
+ return os.environ.get("YA_CC")
+
+
+def get_yt_hdd_path(path=None):
+ if 'HDD_PATH' in os.environ:
+ return _join_path(os.environ['HDD_PATH'], path)
+
+
+def cxx_compiler_path():
+ """
+ Get path to the gdb
+ """
+ return os.environ.get("YA_CXX")
+
+
+def global_resources():
+ try:
+ return json.loads(os.environ.get("YA_GLOBAL_RESOURCES"))
+ except (TypeError, ValueError):
+ return {}
+
+
+def _register_core(name, binary_path, core_path, bt_path, pbt_path):
+ config = _get_ya_config()
+
+ with _lock:
+ if not hasattr(config, 'test_cores_count'):
+ config.test_cores_count = 0
+ config.test_cores_count += 1
+ count_str = '' if config.test_cores_count == 1 else str(config.test_cores_count)
+
+ log_entry = config.test_logs[config.current_item_nodeid]
+ if binary_path:
+ log_entry['{} binary{}'.format(name, count_str)] = binary_path
+ if core_path:
+ log_entry['{} core{}'.format(name, count_str)] = core_path
+ if bt_path:
+ log_entry['{} backtrace{}'.format(name, count_str)] = bt_path
+ if pbt_path:
+ log_entry['{} backtrace html{}'.format(name, count_str)] = pbt_path
+
+
+@not_test
+def test_source_path(path=None):
+ return _join_path(os.path.join(source_path(), context.project_path), path)
+
+
+class Context(object):
+ """
+ Runtime context
+ """
+
+ @property
+ def build_type(self):
+ return _get_ya_plugin_instance().get_context("build_type")
+
+ @property
+ def project_path(self):
+ return _get_ya_plugin_instance().get_context("project_path")
+
+ @property
+ def test_stderr(self):
+ return _get_ya_plugin_instance().get_context("test_stderr")
+
+ @property
+ def test_debug(self):
+ return _get_ya_plugin_instance().get_context("test_debug")
+
+ @property
+ def test_traceback(self):
+ return _get_ya_plugin_instance().get_context("test_traceback")
+
+ @property
+ def test_name(self):
+ return _get_ya_config().current_test_name
+
+ @property
+ def sanitize(self):
+ """
+ Detect if current test run is under sanitizer
+
+ :return: one of `None`, 'address', 'memory', 'thread', 'undefined'
+ """
+ return _get_ya_plugin_instance().get_context("sanitize")
+
+ @property
+ def flags(self):
+ _flags = _get_ya_plugin_instance().get_context("flags")
+ if _flags:
+ _flags_dict = dict()
+ for f in _flags:
+ key, value = f.split('=', 1)
+ _flags_dict[key] = value
+ return _flags_dict
+ else:
+ return dict()
+
+ def get_context_key(self, key):
+ return _get_ya_plugin_instance().get_context(key)
+
+
+context = Context()
diff --git a/library/python/testing/yatest_common/yatest/common/runtime_java.py b/library/python/testing/yatest_common/yatest/common/runtime_java.py
new file mode 100644
index 0000000000..39bbb45570
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/runtime_java.py
@@ -0,0 +1,46 @@
+import os
+import tarfile
+import contextlib
+
+from . import runtime
+
+_JAVA_DIR = []
+
+
+def get_java_path(jdk_dir):
+ # deprecated - to be deleted
+ java_paths = (os.path.join(jdk_dir, 'bin', 'java'), os.path.join(jdk_dir, 'bin', 'java.exe'))
+
+ for p in java_paths:
+ if os.path.exists(p):
+ return p
+
+ for f in os.listdir(jdk_dir):
+ if f.endswith('.tar'):
+ with contextlib.closing(tarfile.open(os.path.join(jdk_dir, f))) as tf:
+ tf.extractall(jdk_dir)
+
+ for p in java_paths:
+ if os.path.exists(p):
+ return p
+
+ return ''
+
+
+def get_build_java_dir(jdk_dir):
+ versions = [8, 10, 11, 12, 13, 14, 15]
+
+ if not _JAVA_DIR:
+ for version in versions:
+ jdk_tar_path = os.path.join(jdk_dir, "jdk{}.tar".format(version))
+ if os.path.exists(jdk_tar_path):
+ jdk_dir = runtime.build_path('jdk4test')
+ with contextlib.closing(tarfile.open(jdk_tar_path)) as tf:
+ tf.extractall(jdk_dir)
+ assert os.path.exists(os.path.join(jdk_dir, "bin", "java"))
+ _JAVA_DIR.append(jdk_dir)
+ break
+ else:
+ _JAVA_DIR.append(None)
+
+ return _JAVA_DIR[0]
diff --git a/library/python/testing/yatest_common/yatest/common/tags.py b/library/python/testing/yatest_common/yatest/common/tags.py
new file mode 100644
index 0000000000..9e7a74cdf5
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/tags.py
@@ -0,0 +1,5 @@
+try:
+ import pytest
+ ya_external = getattr(pytest.mark, "ya:external")
+except ImportError:
+ ya_external = None
diff --git a/library/python/testing/yatest_common/yatest/common/ya.make b/library/python/testing/yatest_common/yatest/common/ya.make
new file mode 100644
index 0000000000..f7c50dfe64
--- /dev/null
+++ b/library/python/testing/yatest_common/yatest/common/ya.make
@@ -0,0 +1 @@
+OWNER(g:yatest)
diff --git a/library/python/testing/yatest_lib/__init__.py b/library/python/testing/yatest_lib/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/library/python/testing/yatest_lib/__init__.py
diff --git a/library/python/testing/yatest_lib/external.py b/library/python/testing/yatest_lib/external.py
new file mode 100644
index 0000000000..39113230d9
--- /dev/null
+++ b/library/python/testing/yatest_lib/external.py
@@ -0,0 +1,192 @@
+from __future__ import absolute_import
+
+import re
+import sys
+import copy
+import logging
+
+from . import tools
+from datetime import date, datetime
+
+import enum
+import six
+
+logger = logging.getLogger(__name__)
+MDS_URI_PREFIX = 'https://storage.yandex-team.ru/get-devtools/'
+
+
+def apply(func, value, apply_to_keys=False):
+ """
+ Applies func to every possible member of value
+ :param value: could be either a primitive object or a complex one (list, dicts)
+ :param func: func to be applied
+ :return:
+ """
+ def _apply(func, value, value_path):
+ if value_path is None:
+ value_path = []
+
+ if isinstance(value, list) or isinstance(value, tuple):
+ res = []
+ for ind, item in enumerate(value):
+ path = copy.copy(value_path)
+ path.append(ind)
+ res.append(_apply(func, item, path))
+ elif isinstance(value, dict):
+ if is_external(value):
+ # this is a special serialized object pointing to some external place
+ res = func(value, value_path)
+ else:
+ res = {}
+ for key, val in sorted(value.items(), key=lambda dict_item: dict_item[0]):
+ path = copy.copy(value_path)
+ path.append(key)
+ res[_apply(func, key, path) if apply_to_keys else key] = _apply(func, val, path)
+ else:
+ res = func(value, value_path)
+ return res
+ return _apply(func, value, None)
+
+
+def is_coroutine(val):
+ if sys.version_info[0] < 3:
+ return False
+ else:
+ import asyncio
+ return asyncio.iscoroutinefunction(val) or asyncio.iscoroutine(val)
+
+
+def serialize(value):
+ """
+ Serialize value to json-convertible object
+ Ensures that all components of value can be serialized to json
+ :param value: object to be serialized
+ """
+ def _serialize(val, _):
+ if val is None:
+ return val
+ if isinstance(val, six.string_types) or isinstance(val, bytes):
+ return tools.to_utf8(val)
+ if isinstance(val, enum.Enum):
+ return str(val)
+ if isinstance(val, six.integer_types) or type(val) in [float, bool]:
+ return val
+ if is_external(val):
+ return dict(val)
+ if isinstance(val, (date, datetime)):
+ return repr(val)
+ if is_coroutine(val):
+ return None
+ raise ValueError("Cannot serialize value '{}' of type {}".format(val, type(val)))
+ return apply(_serialize, value, apply_to_keys=True)
+
+
+def is_external(value):
+ return isinstance(value, dict) and "uri" in value.keys()
+
+
+class ExternalSchema(object):
+ File = "file"
+ SandboxResource = "sbr"
+ Delayed = "delayed"
+ HTTP = "http"
+
+
+class CanonicalObject(dict):
+ def __iter__(self):
+ raise TypeError("Iterating canonical object is not implemented")
+
+
+class ExternalDataInfo(object):
+
+ def __init__(self, data):
+ assert is_external(data)
+ self._data = data
+
+ def __str__(self):
+ type_str = "File" if self.is_file else "Sandbox resource"
+ return "{}({})".format(type_str, self.path)
+
+ def __repr__(self):
+ return str(self)
+
+ @property
+ def uri(self):
+ return self._data["uri"]
+
+ @property
+ def checksum(self):
+ return self._data.get("checksum")
+
+ @property
+ def is_file(self):
+ return self.uri.startswith(ExternalSchema.File)
+
+ @property
+ def is_sandbox_resource(self):
+ return self.uri.startswith(ExternalSchema.SandboxResource)
+
+ @property
+ def is_delayed(self):
+ return self.uri.startswith(ExternalSchema.Delayed)
+
+ @property
+ def is_http(self):
+ return self.uri.startswith(ExternalSchema.HTTP)
+
+ @property
+ def path(self):
+ if self.uri.count("://") != 1:
+ logger.error("Invalid external data uri: '%s'", self.uri)
+ return self.uri
+ _, path = self.uri.split("://")
+ return path
+
+ def get_mds_key(self):
+ assert self.is_http
+ m = re.match(re.escape(MDS_URI_PREFIX) + r'(.*?)($|#)', self.uri)
+ if m:
+ return m.group(1)
+ raise AssertionError("Failed to extract mds key properly from '{}'".format(self.uri))
+
+ @property
+ def size(self):
+ return self._data.get("size")
+
+ def serialize(self):
+ return self._data
+
+ @classmethod
+ def _serialize(cls, schema, path, checksum=None, attrs=None):
+ res = CanonicalObject({"uri": "{}://{}".format(schema, path)})
+ if checksum:
+ res["checksum"] = checksum
+ if attrs:
+ res.update(attrs)
+ return res
+
+ @classmethod
+ def serialize_file(cls, path, checksum=None, diff_tool=None, local=False, diff_file_name=None, diff_tool_timeout=None, size=None):
+ attrs = {}
+ if diff_tool:
+ attrs["diff_tool"] = diff_tool
+ if local:
+ attrs["local"] = local
+ if diff_file_name:
+ attrs["diff_file_name"] = diff_file_name
+ if diff_tool_timeout:
+ attrs["diff_tool_timeout"] = diff_tool_timeout
+ if size is not None:
+ attrs["size"] = size
+ return cls._serialize(ExternalSchema.File, path, checksum, attrs=attrs)
+
+ @classmethod
+ def serialize_resource(cls, id, checksum=None):
+ return cls._serialize(ExternalSchema.SandboxResource, id, checksum)
+
+ @classmethod
+ def serialize_delayed(cls, upload_id, checksum):
+ return cls._serialize(ExternalSchema.Delayed, upload_id, checksum)
+
+ def get(self, key, default=None):
+ return self._data.get(key, default)
diff --git a/library/python/testing/yatest_lib/test_splitter.py b/library/python/testing/yatest_lib/test_splitter.py
new file mode 100644
index 0000000000..acbcd4300e
--- /dev/null
+++ b/library/python/testing/yatest_lib/test_splitter.py
@@ -0,0 +1,102 @@
+# coding: utf-8
+
+import collections
+
+
+def flatten_tests(test_classes):
+ """
+ >>> test_classes = {x: [x] for x in range(5)}
+ >>> flatten_tests(test_classes)
+ [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)]
+ >>> test_classes = {x: [x + 1, x + 2] for x in range(2)}
+ >>> flatten_tests(test_classes)
+ [(0, 1), (0, 2), (1, 2), (1, 3)]
+ """
+ tests = []
+ for class_name, test_names in test_classes.items():
+ tests += [(class_name, test_name) for test_name in test_names]
+ return tests
+
+
+def get_sequential_chunk(tests, modulo, modulo_index, is_sorted=False):
+ """
+ >>> get_sequential_chunk(range(10), 4, 0)
+ [0, 1, 2]
+ >>> get_sequential_chunk(range(10), 4, 1)
+ [3, 4, 5]
+ >>> get_sequential_chunk(range(10), 4, 2)
+ [6, 7]
+ >>> get_sequential_chunk(range(10), 4, 3)
+ [8, 9]
+ >>> get_sequential_chunk(range(10), 4, 4)
+ []
+ >>> get_sequential_chunk(range(10), 4, 5)
+ []
+ """
+ if not is_sorted:
+ tests = sorted(tests)
+ chunk_size = len(tests) // modulo
+ not_used = len(tests) % modulo
+ shift = chunk_size + (modulo_index < not_used)
+ start = chunk_size * modulo_index + min(modulo_index, not_used)
+ end = start + shift
+ return [] if end > len(tests) else tests[start:end]
+
+
+def get_shuffled_chunk(tests, modulo, modulo_index, is_sorted=False):
+ """
+ >>> get_shuffled_chunk(range(10), 4, 0)
+ [0, 4, 8]
+ >>> get_shuffled_chunk(range(10), 4, 1)
+ [1, 5, 9]
+ >>> get_shuffled_chunk(range(10), 4, 2)
+ [2, 6]
+ >>> get_shuffled_chunk(range(10), 4, 3)
+ [3, 7]
+ >>> get_shuffled_chunk(range(10), 4, 4)
+ []
+ >>> get_shuffled_chunk(range(10), 4, 5)
+ []
+ """
+ if not is_sorted:
+ tests = sorted(tests)
+ result_tests = []
+ for i, test in enumerate(tests):
+ if i % modulo == modulo_index:
+ result_tests.append(test)
+ return result_tests
+
+
+def get_splitted_tests(test_entities, modulo, modulo_index, partition_mode, is_sorted=False):
+ if partition_mode == 'SEQUENTIAL':
+ return get_sequential_chunk(test_entities, modulo, modulo_index, is_sorted)
+ elif partition_mode == 'MODULO':
+ return get_shuffled_chunk(test_entities, modulo, modulo_index, is_sorted)
+ else:
+ raise ValueError("detected unknown partition mode: {}".format(partition_mode))
+
+
+def filter_tests_by_modulo(test_classes, modulo, modulo_index, split_by_tests, partition_mode="SEQUENTIAL"):
+ """
+ >>> test_classes = {x: [x] for x in range(20)}
+ >>> filter_tests_by_modulo(test_classes, 4, 0, False)
+ {0: [0], 1: [1], 2: [2], 3: [3], 4: [4]}
+ >>> filter_tests_by_modulo(test_classes, 4, 1, False)
+ {8: [8], 9: [9], 5: [5], 6: [6], 7: [7]}
+ >>> filter_tests_by_modulo(test_classes, 4, 2, False)
+ {10: [10], 11: [11], 12: [12], 13: [13], 14: [14]}
+
+ >>> dict(filter_tests_by_modulo(test_classes, 4, 0, True))
+ {0: [0], 1: [1], 2: [2], 3: [3], 4: [4]}
+ >>> dict(filter_tests_by_modulo(test_classes, 4, 1, True))
+ {8: [8], 9: [9], 5: [5], 6: [6], 7: [7]}
+ """
+ if split_by_tests:
+ tests = get_splitted_tests(flatten_tests(test_classes), modulo, modulo_index, partition_mode)
+ test_classes = collections.defaultdict(list)
+ for class_name, test_name in tests:
+ test_classes[class_name].append(test_name)
+ return test_classes
+ else:
+ target_classes = get_splitted_tests(test_classes, modulo, modulo_index, partition_mode)
+ return {class_name: test_classes[class_name] for class_name in target_classes}
diff --git a/library/python/testing/yatest_lib/tests/test_external.py b/library/python/testing/yatest_lib/tests/test_external.py
new file mode 100644
index 0000000000..18cb560b17
--- /dev/null
+++ b/library/python/testing/yatest_lib/tests/test_external.py
@@ -0,0 +1,20 @@
+import enum
+import pytest
+
+from yatest_lib import external
+
+
+class MyEnum(enum.Enum):
+ VAL1 = 1
+ VAL2 = 2
+
+
+@pytest.mark.parametrize("data, expected_val, expected_type", [
+ ({}, {}, dict),
+ (MyEnum.VAL1, "MyEnum.VAL1", str),
+ ({MyEnum.VAL1: MyEnum.VAL2}, {"MyEnum.VAL1": "MyEnum.VAL2"}, dict),
+])
+def test_serialize(data, expected_val, expected_type):
+ data = external.serialize(data)
+ assert expected_type == type(data), data
+ assert expected_val == data
diff --git a/library/python/testing/yatest_lib/tests/test_testsplitter.py b/library/python/testing/yatest_lib/tests/test_testsplitter.py
new file mode 100644
index 0000000000..394bfe5a74
--- /dev/null
+++ b/library/python/testing/yatest_lib/tests/test_testsplitter.py
@@ -0,0 +1,103 @@
+# coding: utf-8
+from yatest_lib import test_splitter
+
+
+def get_chunks(tests, modulo, mode):
+ chunks = []
+ if mode == "MODULO":
+ for modulo_index in range(modulo):
+ chunks.append(test_splitter.get_shuffled_chunk(tests, modulo, modulo_index))
+ elif mode == "SEQUENTIAL":
+ for modulo_index in range(modulo):
+ chunks.append(test_splitter.get_sequential_chunk(tests, modulo, modulo_index))
+ else:
+ raise ValueError("no such mode")
+ return chunks
+
+
+def check_not_intersect(chunk_list):
+ test_set = set()
+ total_size = 0
+ for tests in chunk_list:
+ total_size += len(tests)
+ test_set.update(tests)
+ return total_size == len(test_set)
+
+
+def check_max_diff(chunk_list):
+ return max(map(len, chunk_list)) - min(map(len, chunk_list))
+
+
+def test_lot_of_chunks():
+ for chunk_count in range(10, 20):
+ for tests_count in range(chunk_count):
+ chunks = get_chunks(range(tests_count), chunk_count, "SEQUENTIAL")
+ assert check_not_intersect(chunks)
+ assert check_max_diff(chunks) <= 1
+ assert chunks.count([]) == chunk_count - tests_count
+ assert len(chunks) == chunk_count
+ chunks = get_chunks(range(tests_count), chunk_count, "MODULO")
+ assert check_not_intersect(chunks)
+ assert check_max_diff(chunks) <= 1
+ assert chunks.count([]) == chunk_count - tests_count
+ assert len(chunks) == chunk_count
+
+
+def test_lot_of_tests():
+ for tests_count in range(10, 20):
+ for chunk_count in range(2, tests_count):
+ chunks = get_chunks(range(tests_count), chunk_count, "SEQUENTIAL")
+ assert check_not_intersect(chunks)
+ assert check_max_diff(chunks) <= 1
+ assert len(chunks) == chunk_count
+ chunks = get_chunks(range(tests_count), chunk_count, "MODULO")
+ assert check_not_intersect(chunks)
+ assert check_max_diff(chunks) <= 1
+ assert len(chunks) == chunk_count
+
+
+def prime_chunk_count():
+ for chunk_count in [7, 11, 13, 17, 23, 29]:
+ for tests_count in range(chunk_count):
+ chunks = get_chunks(range(tests_count), chunk_count, "SEQUENTIAL")
+ assert check_not_intersect(chunks)
+ assert check_max_diff(chunks) <= 1
+ assert len(chunks) == chunk_count
+ chunks = get_chunks(range(tests_count), chunk_count, "MODULO")
+ assert check_not_intersect(chunks)
+ assert check_max_diff(chunks) <= 1
+ assert len(chunks) == chunk_count
+
+
+def get_divisors(number):
+ divisors = []
+ for d in range(1, number + 1):
+ if number % d == 0:
+ divisors.append(d)
+ return divisors
+
+
+def equal_chunks():
+ for chunk_count in range(12, 31):
+ for tests_count in get_divisors(chunk_count):
+ chunks = get_chunks(range(tests_count), chunk_count, "SEQUENTIAL")
+ assert check_not_intersect(chunks)
+ assert check_max_diff(chunks) == 0
+ assert len(chunks) == chunk_count
+ chunks = get_chunks(range(tests_count), chunk_count, "MODULO")
+ assert check_not_intersect(chunks)
+ assert check_max_diff(chunks) == 0
+ assert len(chunks) == chunk_count
+
+
+def chunk_count_equal_tests_count():
+ for chunk_count in range(10, 20):
+ tests_count = chunk_count
+ chunks = get_chunks(range(tests_count), chunk_count, "SEQUENTIAL")
+ assert check_not_intersect(chunks)
+ assert check_max_diff(chunks) <= 1
+ assert len(chunks) == chunk_count
+ chunks = get_chunks(range(tests_count), chunk_count, "MODULO")
+ assert check_not_intersect(chunks)
+ assert check_max_diff(chunks) <= 1
+ assert len(chunks) == chunk_count
diff --git a/library/python/testing/yatest_lib/tests/ya.make b/library/python/testing/yatest_lib/tests/ya.make
new file mode 100644
index 0000000000..8586c6ef7d
--- /dev/null
+++ b/library/python/testing/yatest_lib/tests/ya.make
@@ -0,0 +1,14 @@
+OWNER(g:yatest)
+
+PY23_TEST()
+
+PEERDIR(
+ library/python/testing/yatest_lib
+)
+
+TEST_SRCS(
+ test_external.py
+ test_testsplitter.py
+)
+
+END()
diff --git a/library/python/testing/yatest_lib/tools.py b/library/python/testing/yatest_lib/tools.py
new file mode 100644
index 0000000000..b72d79c162
--- /dev/null
+++ b/library/python/testing/yatest_lib/tools.py
@@ -0,0 +1,64 @@
+import six
+import sys
+
+
+def to_utf8(value):
+ """
+ Converts value to string encoded into utf-8
+ :param value:
+ :return:
+ """
+ if sys.version_info[0] < 3:
+ if not isinstance(value, basestring): # noqa
+ value = unicode(value) # noqa
+ if type(value) == str:
+ value = value.decode("utf-8", errors="ignore")
+ return value.encode('utf-8', 'ignore')
+ else:
+ return str(value)
+
+
+def trim_string(s, max_bytes):
+ """
+ Adjusts the length of the string s in order to fit it
+ into max_bytes bytes of storage after encoding as UTF-8.
+ Useful when cutting filesystem paths.
+ :param s: unicode string
+ :param max_bytes: number of bytes
+ :return the prefix of s
+ """
+ if isinstance(s, six.text_type):
+ return _trim_unicode_string(s, max_bytes)
+
+ if isinstance(s, six.binary_type):
+ if len(s) <= max_bytes:
+ return s
+ s = s.decode('utf-8', errors='ignore')
+ s = _trim_unicode_string(s, max_bytes)
+ s = s.encode('utf-8', errors='ignore')
+ return s
+
+ raise TypeError('a string is expected')
+
+
+def _trim_unicode_string(s, max_bytes):
+ if len(s) * 4 <= max_bytes:
+ # UTF-8 uses at most 4 bytes per character
+ return s
+
+ result = []
+ cur_byte_length = 0
+
+ for ch in s:
+ cur_byte_length += len(ch.encode('utf-8'))
+ if cur_byte_length > max_bytes:
+ break
+ result.append(ch)
+
+ return ''.join(result)
+
+
+def to_str(s):
+ if six.PY2 and isinstance(s, six.text_type):
+ return s.encode('utf8')
+ return s
diff --git a/library/python/testing/yatest_lib/ya.make b/library/python/testing/yatest_lib/ya.make
new file mode 100644
index 0000000000..342bae82ba
--- /dev/null
+++ b/library/python/testing/yatest_lib/ya.make
@@ -0,0 +1,26 @@
+OWNER(g:yatest)
+
+PY23_LIBRARY()
+
+PY_SRCS(
+ NAMESPACE
+ yatest_lib
+ external.py
+ test_splitter.py
+ tools.py
+ ya.py
+)
+
+PEERDIR(
+ contrib/python/six
+)
+
+IF(PYTHON2)
+ PEERDIR(
+ contrib/python/enum34
+ )
+ENDIF()
+
+END()
+
+RECURSE_FOR_TESTS(tests)
diff --git a/library/python/testing/yatest_lib/ya.py b/library/python/testing/yatest_lib/ya.py
new file mode 100644
index 0000000000..c13b58a19f
--- /dev/null
+++ b/library/python/testing/yatest_lib/ya.py
@@ -0,0 +1,239 @@
+import os
+import sys
+import logging
+import json
+
+from .tools import to_str
+from .external import ExternalDataInfo
+
+
+TESTING_OUT_DIR_NAME = "testing_out_stuff" # XXX import from test.const
+
+yatest_logger = logging.getLogger("ya.test")
+
+
+class RunMode(object):
+ Run = "run"
+ List = "list"
+
+
+class TestMisconfigurationException(Exception):
+ pass
+
+
+class Ya(object):
+ """
+ Adds integration with ya, helps in finding dependencies
+ """
+
+ def __init__(
+ self,
+ mode=None,
+ source_root=None,
+ build_root=None,
+ dep_roots=None,
+ output_dir=None,
+ test_params=None,
+ context=None,
+ python_path=None,
+ valgrind_path=None,
+ gdb_path=None,
+ data_root=None,
+ ):
+ context_file_path = os.environ.get("YA_TEST_CONTEXT_FILE", None)
+ if context_file_path:
+ with open(context_file_path, 'r') as afile:
+ test_context = json.load(afile)
+ context_runtime = test_context["runtime"]
+ context_internal = test_context.get("internal", {})
+ context_build = test_context.get("build", {})
+ else:
+ context_runtime = {}
+ context_internal = {}
+ context_build = {}
+ self._mode = mode
+ self._build_root = to_str(context_runtime.get("build_root", "")) or build_root
+ self._source_root = to_str(context_runtime.get("source_root", "")) or source_root or self._detect_source_root()
+ self._output_dir = to_str(context_runtime.get("output_path", "")) or output_dir or self._detect_output_root()
+ if not self._output_dir:
+ raise Exception("Run ya make -t before running test binary")
+ if not self._source_root:
+ logging.warning("Source root was not set neither determined, use --source-root to set it explicitly")
+ if not self._build_root:
+ if self._source_root:
+ self._build_root = self._source_root
+ else:
+ logging.warning("Build root was not set neither determined, use --build-root to set it explicitly")
+
+ if data_root:
+ self._data_root = data_root
+ elif self._source_root:
+ self._data_root = os.path.abspath(os.path.join(self._source_root, "..", "arcadia_tests_data"))
+
+ self._dep_roots = dep_roots
+
+ self._python_path = to_str(context_runtime.get("python_bin", "")) or python_path
+ self._valgrind_path = valgrind_path
+ self._gdb_path = to_str(context_runtime.get("gdb_bin", "")) or gdb_path
+ self._test_params = {}
+ self._context = {}
+ self._test_item_node_id = None
+
+ ram_drive_path = to_str(context_runtime.get("ram_drive_path", ""))
+ if ram_drive_path:
+ self._test_params["ram_drive_path"] = ram_drive_path
+ if test_params:
+ self._test_params.update(dict(x.split('=', 1) for x in test_params))
+ self._test_params.update(context_runtime.get("test_params", {}))
+
+ self._context["project_path"] = context_runtime.get("project_path")
+ self._context["modulo"] = context_runtime.get("split_count", 1)
+ self._context["modulo_index"] = context_runtime.get("split_index", 0)
+ self._context["work_path"] = context_runtime.get("work_path")
+
+ self._context["sanitize"] = context_build.get("sanitizer")
+ self._context["ya_trace_path"] = context_internal.get("trace_file")
+
+ self._env_file = context_internal.get("env_file")
+
+ if context:
+ self._context.update(context)
+
+ @property
+ def source_root(self):
+ return self._source_root
+
+ @property
+ def data_root(self):
+ return self._data_root
+
+ @property
+ def build_root(self):
+ return self._build_root
+
+ @property
+ def dep_roots(self):
+ return self._dep_roots
+
+ @property
+ def output_dir(self):
+ return self._output_dir
+
+ @property
+ def python_path(self):
+ return self._python_path or sys.executable
+
+ @property
+ def valgrind_path(self):
+ if not self._valgrind_path:
+ raise ValueError("path to valgrind was not pass correctly, use --valgrind-path to fix it")
+ return self._valgrind_path
+
+ @property
+ def gdb_path(self):
+ return self._gdb_path
+
+ @property
+ def env_file(self):
+ return self._env_file
+
+ def get_binary(self, *path):
+ assert self._build_root, "Build root was not set neither determined, use --build-root to set it explicitly"
+ path = list(path)
+ if os.name == "nt":
+ if not path[-1].endswith(".exe"):
+ path[-1] += ".exe"
+
+ target_dirs = [self.build_root]
+ # Search for binaries within PATH dirs to be able to get path to the binaries specified by basename for exectests
+ if 'PATH' in os.environ:
+ target_dirs += os.environ['PATH'].split(':')
+
+ for target_dir in target_dirs:
+ binary_path = os.path.join(target_dir, *path)
+ if os.path.exists(binary_path):
+ yatest_logger.debug("Binary was found by %s", binary_path)
+ return binary_path
+
+ error_message = "Cannot find binary '{binary}': make sure it was added in the DEPENDS section".format(binary=path)
+ yatest_logger.debug(error_message)
+ if self._mode == RunMode.Run:
+ raise TestMisconfigurationException(error_message)
+
+ def file(self, path, diff_tool=None, local=False, diff_file_name=None, diff_tool_timeout=None):
+ return ExternalDataInfo.serialize_file(path, diff_tool=diff_tool, local=local, diff_file_name=diff_file_name, diff_tool_timeout=diff_tool_timeout)
+
+ def get_param(self, key, default=None):
+ return self._test_params.get(key, default)
+
+ def get_param_dict_copy(self):
+ return dict(self._test_params)
+
+ def get_context(self, key):
+ return self._context.get(key)
+
+ def _detect_source_root(self):
+ root = None
+ try:
+ import library.python.find_root
+ # try to determine source root from cwd
+ cwd = os.getcwd()
+ root = library.python.find_root.detect_root(cwd)
+
+ if not root:
+ # try to determine root pretending we are in the test work dir made from --keep-temps run
+ env_subdir = os.path.join("environment", "arcadia")
+ root = library.python.find_root.detect_root(cwd, detector=lambda p: os.path.exists(os.path.join(p, env_subdir)))
+ except ImportError:
+ logging.warning("Unable to import library.python.find_root")
+
+ return root
+
+ def _detect_output_root(self):
+
+ # if run from kept test working dir
+ if os.path.exists(TESTING_OUT_DIR_NAME):
+ return TESTING_OUT_DIR_NAME
+
+ # if run from source dir
+ if sys.version_info.major == 3:
+ test_results_dir = "py3test"
+ else:
+ test_results_dir = "pytest"
+
+ test_results_output_path = os.path.join("test-results", test_results_dir, TESTING_OUT_DIR_NAME)
+ if os.path.exists(test_results_output_path):
+ return test_results_output_path
+
+ if os.path.exists(os.path.dirname(test_results_output_path)):
+ os.mkdir(test_results_output_path)
+ return test_results_output_path
+
+ return None
+
+ def set_test_item_node_id(self, node_id):
+ self._test_item_node_id = node_id
+
+ def get_test_item_node_id(self):
+ assert self._test_item_node_id
+ return self._test_item_node_id
+
+ @property
+ def pytest_config(self):
+ if not hasattr(self, "_pytest_config"):
+ import library.python.pytest.plugins.ya as ya_plugin
+ self._pytest_config = ya_plugin.pytest_config
+ return self._pytest_config
+
+ def set_metric_value(self, name, val):
+ node_id = self.get_test_item_node_id()
+ if node_id not in self.pytest_config.test_metrics:
+ self.pytest_config.test_metrics[node_id] = {}
+
+ self.pytest_config.test_metrics[node_id][name] = val
+
+ def get_metric_value(self, name, default=None):
+ res = self.pytest_config.test_metrics.get(self.get_test_item_node_id(), {}).get(name)
+ if res is None:
+ return default
+ return res