diff options
author | Nikita Kozlovskiy <nikitka@gmail.com> | 2023-08-16 14:39:21 +0300 |
---|---|---|
committer | nkozlovskiy <nmk@ydb.tech> | 2023-08-16 16:15:08 +0300 |
commit | 193c3861dc3bed68a1d0a394effdda630d0eb551 (patch) | |
tree | be90a52ef8b2b6eae122641e3c6b5d32748afca9 | |
parent | 8662d99f68311ede697154b9fb02e3fd9e9ad52e (diff) | |
download | ydb-193c3861dc3bed68a1d0a394effdda630d0eb551.tar.gz |
CI: group tests by test shard name
CI: group tests by test shard name
Pull Request resolved: #332
-rw-r--r-- | .github/actions/test/action.yml | 11 | ||||
-rw-r--r-- | .github/config/muted_functest.txt | 30 | ||||
-rw-r--r-- | .github/config/muted_shard.txt | 4 | ||||
-rw-r--r-- | .github/config/muted_test.txt | 57 | ||||
-rwxr-xr-x | .github/scripts/tests/attach-logs.py | 11 | ||||
-rwxr-xr-x | .github/scripts/tests/ctest-postprocess.py | 30 | ||||
-rwxr-xr-x | .github/scripts/tests/junit-postprocess.py | 86 | ||||
-rw-r--r-- | .github/scripts/tests/mute_utils.py | 83 | ||||
-rwxr-xr-x | .github/scripts/tests/pytest-postprocess.py | 75 | ||||
-rw-r--r-- | .mapping.json | 1 |
10 files changed, 227 insertions, 161 deletions
diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index 8637015b83..1a7059a2ab 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -55,7 +55,7 @@ runs: echo "testfilterfile=$(pwd)/.github/config/muted_test.txt" >> $GITHUB_OUTPUT echo "testshardfilterfile=$(pwd)/.github/config/muted_shard.txt" >> $GITHUB_OUTPUT echo "functestfilterfile=$(pwd)/.github/config/muted_functest.txt" >> $GITHUB_OUTPUT - echo "pytest-logfilename=${{inputs.log_suffix}}-pytest-stdout.gz" >> $GITHUB_OUTPUT + echo "pytest-logfilename=${{inputs.log_suffix}}-pytest-stdout.log" >> $GITHUB_OUTPUT - name: configure s3cmd shell: bash @@ -169,8 +169,6 @@ runs: .github/scripts/tests/attach-logs.py \ --url-prefix $S3_URL_PREFIX/logs/ \ - --filter-shard-file ${{steps.init.outputs.testshardfilterfile}} \ - --filter-test-file ${{steps.init.outputs.testfilterfile}} \ --ctest-report $TESTREPDIR/suites/ctest_report.xml \ --junit-reports-path $TESTREPDIR/unittests/ \ --decompress \ @@ -250,7 +248,7 @@ runs: --source-root $source_root \ --build-root $build_root \ --output-dir $TMPDIR/pytest/ \ - . | tee $WORKDIR/pytest-short.log + . | tee $ARTIFACTS_DIR/${{steps.init.outputs.pytest-logfilename}} # --artifacts-dir $ARTIFACTS_DIR/pytest/ \ # --artifacts-url $S3_URL_PREFIX/pytest/ \ @@ -261,9 +259,8 @@ runs: run: | echo "::group::junit-postprocess" - .github/scripts/tests/junit-postprocess.py \ + .github/scripts/tests/pytest-postprocess.py \ --filter-file ${{ steps.init.outputs.functestfilterfile }} \ - --no-attach-filename \ $PYTESTREPDIR/ echo "::endgroup::" @@ -282,7 +279,7 @@ runs: testmo automation:run:submit-thread \ --instance "$TESTMO_URL" --run-id ${{steps.th.outputs.runid}} \ --results "$PYTESTREPDIR/*.xml" \ - -- cat $WORKDIR/pytest-short.log + -- cat $ARTIFACTS_DIR/${{steps.init.outputs.pytest-logfilename}} - name: Test history run complete if: always() && inputs.testman_token diff --git a/.github/config/muted_functest.txt b/.github/config/muted_functest.txt index 281e2d5aeb..dc3641b535 100644 --- a/.github/config/muted_functest.txt +++ b/.github/config/muted_functest.txt @@ -1,19 +1,17 @@ -::suite_tests.test_postgres -sqs.common.test_queues_managing.TestQueuesManagingWithTenant -sqs.common.test_queues_managing.TestQueuesManagingWithPathTestQueuesManagingWithPath -sqs.common.test_queue_attributes_validation.TestQueueAttributesValidation -sqs.common.test_queues_managing.TestQueuesManagingWithTenant -sqs.common.test_queues_managing.TestQueuesManagingWithPathTestQueuesManagingWithPath -sqs.with_quotas.test_quoting.TestSqsQuotingWithKesus -sqs.large.test_leader_start_inflight.TestSqsMultinodeCluster -sqs.messaging.test_fifo_messaging.TestSqsFifoMessagingWithTenant -sqs.messaging.test_fifo_messaging.TestSqsFifoMessagingWithPath ::sqs.cloud.test_yandex_cloud_mode +::suite_tests.test_postgres ::suite_tests.test_sql_logic ::suite_tests.test_stream_query -tenants.test_tenants.TestTenants -tenants.test_tenants -tenants.test_dynamic_tenants -tenants.test_storage_config.TestStorageConfig -ydb_cli.test_ydb_impex.TestImpex -suite_tests.test_stream_query.TestStreamQuery::test_sql_suite[results-window.test] +ydb/tests/functional/sqs/common/test_queue_attributes_validation.py::TestQueueAttributesValidation::* +ydb/tests/functional/sqs/common/test_queues_managing.py::TestQueuesManagingWithPathTestQueuesManagingWithPath::* +ydb/tests/functional/sqs/common/test_queues_managing.py::TestQueuesManagingWithPathTestQueuesManagingWithPath::* +ydb/tests/functional/sqs/common/test_queues_managing.py::TestQueuesManagingWithTenant::* +ydb/tests/functional/sqs/common/test_queues_managing.py::TestQueuesManagingWithTenant::* +ydb/tests/functional/sqs/messaging/test_fifo_messaging.py::TestSqsFifoMessagingWithTenant::* +ydb/tests/functional/sqs/multinode/test_multinode_cluster.py::TestSqsMultinodeCluster::* +ydb/tests/functional/sqs/with_quotas/test_quoting.py::TestSqsQuotingWithKesus::* +ydb/tests/functional/suite_tests/test_stream_query.py::TestStreamQuery::test_sql_suite[results-window.test] +ydb/tests/functional/tenants/test_dynamic_tenants.py::* +ydb/tests/functional/tenants/test_storage_config.py::TestStorageConfig::* +ydb/tests/functional/tenants/test_tenants.py::* +ydb/tests/functional/ydb_cli/test_ydb_impex.py::TestImpex::* diff --git a/.github/config/muted_shard.txt b/.github/config/muted_shard.txt index 07a2b0da86..53513f0f12 100644 --- a/.github/config/muted_shard.txt +++ b/.github/config/muted_shard.txt @@ -1,4 +1,4 @@ +ydb-core-blobstorage-pdisk-ut_2 ydb-core-blobstorage-ut_blobstorage_9 -ydb-core-tx-schemeshard-ut_replication_reboots_0 ydb-core-blobstorage-ut_vdisk2_0 -ydb-core-blobstorage-pdisk-ut_2 +ydb-core-tx-schemeshard-ut_replication_reboots_0 diff --git a/.github/config/muted_test.txt b/.github/config/muted_test.txt index 66c5b9c1e3..df05c5083a 100644 --- a/.github/config/muted_test.txt +++ b/.github/config/muted_test.txt @@ -1,28 +1,29 @@ -KqpFederatedQuery -KqpSpillingFileTests::StartError -KqpScanSpilling::SelfJoin -YdbWorkloadTopic -YdbWorkloadTransferTopicToTable -SpaceCheckForDiskReassign::Basic -TBsHuge -SpaceCheckForDiskReassign -IntermediateDirsReboots::CreateKesusWithIntermediateDirs -TReplicationWithRebootsTests::CreateDropRecreate -TSectorMap -TMiniKQLAllocTest::TestDeallocated -ConsistentIndexRead::InteractiveTx -KqpExtTest::SecondaryIndexSelectUsingScripting -KqpQuerySession::NoLocalAttach -Describe::Statistics -CountingEvents::Get_Mirror3of4 -TPDiskFIT -KqpScanArrowInChanels::AllTypesColumns -KqpScanArrowFormat::AllTypesColumnsCellvec -TEST_BACKTRACE_AND_SYMBOLIZE::TEST_NO_KIKIMR -YdbSdkSessionsPool::StressTestSync10 -TYardTest::TestLogMultipleWriteRead -TYardTest::TestMultiYardLogMultipleWriteRead -OperationLog::ConcurrentWrites -TPDiskTest::TestMultipleLogSpliceNonceJump -TCmsTest -TSentinelTests +ydb-apps-ydb-ut-workload-transfer-topic/YdbWorkloadTransferTopicToTable::* +ydb-apps-ydb-ut/YdbWorkloadTopic::* +ydb-core-blobstorage-pdisk-ut/TPDiskTest::TestMultipleLogSpliceNonceJump +ydb-core-blobstorage-pdisk-ut/TYardTest::TestLogMultipleWriteRead +ydb-core-blobstorage-pdisk-ut/TYardTest::TestMultiYardLogMultipleWriteRead +ydb-core-blobstorage-ut/TSectorMap::* +ydb-core-blobstorage-ut_blobstorage/CountingEvents::Get_Mirror3of4 +ydb-core-blobstorage-ut_blobstorage/SpaceCheckForDiskReassign::* +ydb-core-blobstorage-ut_pdiskfit-ut/TPDiskFIT::* +ydb-core-blobstorage-ut_vdisk/TBsHuge::* +ydb-core-cms-ut/TCmsTest::* +ydb-core-cms-ut_sentinel/TSentinelTests::* +ydb-core-cms-ut_sentinel/TSentinelTests::PDiskErrorState +ydb-core-cms-ut_sentinel/TSentinelTests::PDiskFaultyState +ydb-core-debug_tools-ut/OperationLog::ConcurrentWrites +ydb-core-kqp-runtime-ut/KqpSpillingFileTests::StartError +ydb-core-kqp-ut-arrow/KqpScanArrowFormat::AllTypesColumnsCellvec +ydb-core-kqp-ut-arrow/KqpScanArrowInChanels::AllTypesColumns +ydb-core-kqp-ut-federated_query/KqpFederatedQuery::* +ydb-core-kqp-ut-spilling/KqpScanSpilling::SelfJoin +ydb-core-tx-schemeshard-ut_reboots/IntermediateDirsReboots::CreateKesusWithIntermediateDirs +ydb-core-tx-schemeshard-ut_replication_reboots/TReplicationWithRebootsTests::CreateDropRecreate +ydb-library-yql-minikql-ut/TMiniKQLAllocTest::TestDeallocated +ydb-library-yql-utils-backtrace-ut/TEST_BACKTRACE_AND_SYMBOLIZE::TEST_NO_KIKIMR +ydb-public-sdk-cpp-client-ydb_topic-ut/Describe::Statistics +ydb-services-ydb-sdk_sessions_pool_ut/YdbSdkSessionsPool::StressTestSync10 +ydb-tests-functional-kqp-kqp_indexes/ConsistentIndexRead::InteractiveTx +ydb-tests-functional-kqp-kqp_indexes/KqpExtTest::SecondaryIndexSelectUsingScripting +ydb-tests-functional-kqp-kqp_query_session/KqpQuerySession::NoLocalAttach diff --git a/.github/scripts/tests/attach-logs.py b/.github/scripts/tests/attach-logs.py index 0f4a5bd785..7889464ff3 100755 --- a/.github/scripts/tests/attach-logs.py +++ b/.github/scripts/tests/attach-logs.py @@ -96,7 +96,12 @@ def attach_to_unittests(ctest_log: CTestLog, unit_path): log_name = os.path.splitext(os.path.basename(fn))[0] common_shard_name = fn_shard_part_re.sub("", log_name) found_tests = all_found_tests.setdefault(common_shard_name, []) - tree = ET.parse(fn) + try: + tree = ET.parse(fn) + except ET.ParseError as e: + print(f"Unable to parse {fn}: {e}") + continue + root = tree.getroot() changed = False @@ -119,7 +124,7 @@ def attach_to_unittests(ctest_log: CTestLog, unit_path): if not extra_logs: continue - fn = f"_{shard}_not_found.xml" + fn = f"{shard}-0000.xml" print(f"create {fn}") testcases = [create_error_testcase(t.shard.name, t.classname, t.method, t.fn, t.url) for t in extra_logs] @@ -131,8 +136,6 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument("--url-prefix", default="./") parser.add_argument("--decompress", action="store_true", default=False, help="decompress ctest log") - parser.add_argument("--filter-test-file", required=False) - parser.add_argument("--filter-shard-file", required=False) parser.add_argument("--ctest-report") parser.add_argument("--junit-reports-path") parser.add_argument("ctest_log") diff --git a/.github/scripts/tests/ctest-postprocess.py b/.github/scripts/tests/ctest-postprocess.py index 10eacf6767..8d3df91f6c 100755 --- a/.github/scripts/tests/ctest-postprocess.py +++ b/.github/scripts/tests/ctest-postprocess.py @@ -1,16 +1,27 @@ #!/usr/bin/env python3 import argparse +import re from typing import TextIO import xml.etree.ElementTree as ET from log_parser import ctest_log_parser, log_reader -from mute_utils import mute_target, remove_failure, update_suite_info, MutedShardCheck +from mute_utils import mute_target, remove_failure, update_suite_info, MuteTestCheck def find_targets_to_remove(log_fp): return {target for target, reason, _ in ctest_log_parser(log_fp) if reason == "Failed"} +shard_suffix_re = re.compile(r"_\d+$") + + +def strip_shardname(testcase): + name = testcase.get('classname') + classname = shard_suffix_re.sub('', name) + + testcase.set('classname', classname) + + def postprocess_ctest(log_fp: TextIO, ctest_junit_report, is_mute_shard, dry_run): to_remove = find_targets_to_remove(log_fp) tree = ET.parse(ctest_junit_report) @@ -20,6 +31,8 @@ def postprocess_ctest(log_fp: TextIO, ctest_junit_report, is_mute_shard, dry_run for testcase in root.findall("testcase"): target = testcase.attrib["classname"] + strip_shardname(testcase) + if is_mute_shard(target): if mute_target(testcase): print(f"mute {target}") @@ -31,13 +44,12 @@ def postprocess_ctest(log_fp: TextIO, ctest_junit_report, is_mute_shard, dry_run n_remove_failures += 1 remove_failure(testcase) - if n_remove_failures: - update_suite_info(root, n_remove_failures, n_skipped=n_skipped) - print(f"{'(dry-run) ' if dry_run else ''}update {ctest_junit_report}") - if not dry_run: - tree.write(ctest_junit_report, xml_declaration=True, encoding="UTF-8") - else: - print("nothing to remove") + update_suite_info(root, n_remove_failures, n_skipped=n_skipped) + + print(f"{'(dry-run) ' if dry_run else ''}update {ctest_junit_report}") + + if not dry_run: + tree.write(ctest_junit_report, xml_declaration=True, encoding="UTF-8") def main(): @@ -50,7 +62,7 @@ def main(): args = parser.parse_args() log = log_reader(args.ctest_log, args.decompress) - is_mute_shard = MutedShardCheck(args.filter_file) + is_mute_shard = MuteTestCheck(args.filter_file) postprocess_ctest(log, args.ctest_junit_report, is_mute_shard, args.dry_run) diff --git a/.github/scripts/tests/junit-postprocess.py b/.github/scripts/tests/junit-postprocess.py index 150cdfb09e..df8d07b0f3 100755 --- a/.github/scripts/tests/junit-postprocess.py +++ b/.github/scripts/tests/junit-postprocess.py @@ -1,80 +1,72 @@ #!/usr/bin/env python3 -import os -import glob import argparse +import glob +import os +import re import xml.etree.ElementTree as ET -from mute_utils import mute_target, update_suite_info, MutedTestCheck -from junit_utils import add_junit_property +from mute_utils import MuteTestCheck, mute_target, recalc_suite_info -def case_iterator(root): - for case in root.findall("testcase"): - cls, method = case.attrib["classname"], case.attrib["name"] - yield case, cls, method +shard_suffix_re = re.compile(r"-\d+$") -def attach_filename(testcase, filename): - shardname = os.path.splitext(filename)[0] - add_junit_property(testcase, "shard", shardname) +def update_testname(fn, testcase): + shardname = os.path.splitext(os.path.basename(fn))[0] + shardname = shard_suffix_re.sub("", shardname) + clsname = testcase.get("classname") + tstname = testcase.get("name") + testcase.set("classname", shardname) -def postprocess_junit(is_mute_test, folder, no_attach_filename, dry_run): - for fn in glob.glob(os.path.join(folder, "*.xml")): - tree = ET.parse(fn) - root = tree.getroot() - total_err = total_fail = 0 + testcase.set("name", f"{clsname}::{tstname}") + testcase.set("id", f"{shardname}_{clsname}_{tstname}") + + return f"{shardname}/{clsname}::{tstname}" - for suite in root.findall("testsuite"): - fail_cnt = error_cnt = 0 - for case, cls, method in case_iterator(suite): - if not no_attach_filename: - attach_filename(case, os.path.basename(fn)) +def postprocess_yunit(fn, mute_check: MuteTestCheck, dry_run): + try: + tree = ET.parse(fn) + except ET.ParseError as e: + print(f"Unable to parse {fn}: {e}") + return + + root = tree.getroot() - if is_mute_test(cls, method): - if mute_target(case): - print(f"mute {cls}::{method}") - fail_cnt += 1 - elif mute_target(case, "error"): - print(f"mute error {cls}::{method}") - error_cnt += 1 + for testsuite in root.findall("testsuite"): + need_recalc = False + for testcase in testsuite.findall("testcase"): + new_name = update_testname(fn, testcase) - if fail_cnt or error_cnt: - update_suite_info(suite, n_remove_failures=fail_cnt, n_remove_errors=error_cnt, - n_skipped=fail_cnt + error_cnt) - total_err += error_cnt - total_fail += fail_cnt + if mute_check(new_name) and mute_target(testcase): + print(f"mute {new_name}") + need_recalc = True - if total_fail or total_err: - update_suite_info(root, n_remove_errors=total_err, n_remove_failures=total_fail, - n_skipped=total_err + total_fail) + if need_recalc: + recalc_suite_info(testsuite) - print(f"{'(dry-run) ' if dry_run else ''}patch {fn}") + print(f"{'(dry-run) ' if dry_run else ''}save {fn}") - if not dry_run: - tree.write(fn, xml_declaration=True, encoding="UTF-8") + if not dry_run: + tree.write(fn, xml_declaration=True, encoding="UTF-8") def main(): parser = argparse.ArgumentParser() parser.add_argument("--filter-file", required=True) - parser.add_argument("--no-attach-filename", action="store_true", default=False) parser.add_argument("--dry-run", action="store_true", default=False) parser.add_argument("yunit_path") + args = parser.parse_args() if not os.path.isdir(args.yunit_path): print(f"{args.yunit_path} is not a directory, exit") raise SystemExit(-1) - # FIXME: add gtest filter file ? - is_mute_test = MutedTestCheck(args.filter_file) - - if not is_mute_test.has_rules: - print("nothing to mute") - return + mute_check = MuteTestCheck(args.filter_file) - postprocess_junit(is_mute_test, args.yunit_path, args.no_attach_filename, args.dry_run) + for fn in glob.glob(os.path.join(args.yunit_path, "*.xml")): + postprocess_yunit(fn, mute_check, args.dry_run) if __name__ == "__main__": diff --git a/.github/scripts/tests/mute_utils.py b/.github/scripts/tests/mute_utils.py index 5d31137fab..a096b407a6 100644 --- a/.github/scripts/tests/mute_utils.py +++ b/.github/scripts/tests/mute_utils.py @@ -1,63 +1,50 @@ import operator +import re import xml.etree.ElementTree as ET from junit_utils import add_junit_property -class MutedTestCheck: - def __init__(self, fn=None): - self.classes = set() - self.methods = set() +def pattern_to_re(pattern): + res = [] + for c in pattern: + if c == '*': + res.append('.*') + else: + res.append(re.escape(c)) - if fn: - self.populate(fn) + return f"(?:^{''.join(res)}$)" - def populate(self, fn): - with open(fn, "r") as fp: - for line in fp: - line = line.strip() - if not line: - continue - if "::" in line: - cls, method = line.split("::", maxsplit=1) - self.methods.add((cls, method)) - else: - self.classes.add(line) - - def __call__(self, cls, method=None): - if cls in self.classes: - return True - - if method and (cls, method) in self.methods: - return True - - return False - @property - def has_rules(self): - return len(self.classes) or len(self.methods) +class MuteTestCheck: + def __init__(self, fn): + self.regexps = [] - -class MutedShardCheck: - def __init__(self, fn=None): - self.muted = set() - if fn: - self.populate(fn) - - def populate(self, fn): - with open(fn, "rt") as fp: + with open(fn, 'r') as fp: for line in fp: - target = line.strip() - if target: - self.muted.add(target) - - def __call__(self, target): - return target in self.muted + line = line.strip() + pattern = pattern_to_re(line) + + try: + self.regexps.append(re.compile(pattern)) + except re.error: + print(f"Unable to compile regex {pattern!r}") + raise + + def __call__(self, fullname): + for r in self.regexps: + if r.match(fullname): + return True + return False -def mute_target(node, node_name="failure"): - failure = node.find(node_name) +def mute_target(node): + for node_name in ('failure', 'error'): + failure = node.find(node_name) + # print('failure', node_name, node, failure) - if failure is None: + if failure is not None: + break + else: return False msg = failure.get("message") @@ -117,7 +104,7 @@ def recalc_suite_info(suite): for case in suite.findall("testcase"): tests += 1 - elapsed += float(case.get("time")) + elapsed += float(case.get("time", 0)) if case.find("skipped"): skipped += 1 if case.find("failure"): diff --git a/.github/scripts/tests/pytest-postprocess.py b/.github/scripts/tests/pytest-postprocess.py new file mode 100755 index 0000000000..9c0873691f --- /dev/null +++ b/.github/scripts/tests/pytest-postprocess.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +import argparse +import glob +import os +import xml.etree.ElementTree as ET +from mute_utils import MuteTestCheck, mute_target, recalc_suite_info +from junit_utils import get_property_value + + +def update_testname(testcase): + filename = get_property_value(testcase, "filename") + + clsname = testcase.get("classname") + tstname = testcase.get("name") + + if filename is None: + return f"{clsname}::{tstname}" + + filename = filename.split("/") + test_fn = filename[-1] + folder = "/".join(filename[:-1]) + + testcase.set("classname", folder) + + clsname = clsname.split(".")[-1] + + test_name = f"{test_fn}::{clsname}::{tstname}" + + testcase.set("name", test_name) + testcase.set("id", f"{folder}_{test_fn}_{clsname}_{tstname}") + + return f"{folder}/{test_name}" + + +def postprocess_pytest(fn, mute_check, dry_run): + tree = ET.parse(fn) + root = tree.getroot() + + for testsuite in root.findall("testsuite"): + need_recalc = False + for testcase in testsuite.findall("testcase"): + new_name = update_testname(testcase) + if mute_check(new_name) and mute_target(testcase): + print(f"mute {new_name}") + need_recalc = True + + if need_recalc: + recalc_suite_info(testsuite) + + print(f"{'(dry-run) ' if dry_run else ''}save {fn}") + + if not dry_run: + tree.write(fn, xml_declaration=True, encoding="UTF-8") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--filter-file", required=True) + parser.add_argument("--dry-run", action="store_true", default=False) + parser.add_argument("pytest_xml_path") + + args = parser.parse_args() + + if not os.path.isdir(args.pytest_xml_path): + print(f"{args.pytest_xml_path} is not a directory, exit") + raise SystemExit(-1) + + mute_check = MuteTestCheck(args.filter_file) + + for fn in glob.glob(os.path.join(args.pytest_xml_path, "*.xml")): + postprocess_pytest(fn, mute_check, args.dry_run) + + +if __name__ == "__main__": + main() diff --git a/.mapping.json b/.mapping.json index 2903369075..81f6d1c2e5 100644 --- a/.mapping.json +++ b/.mapping.json @@ -24,6 +24,7 @@ ".github/scripts/tests/junit_utils.py":"ydb/github_toplevel/.github/scripts/tests/junit_utils.py", ".github/scripts/tests/log_parser.py":"ydb/github_toplevel/.github/scripts/tests/log_parser.py", ".github/scripts/tests/mute_utils.py":"ydb/github_toplevel/.github/scripts/tests/mute_utils.py", + ".github/scripts/tests/pytest-postprocess.py":"ydb/github_toplevel/.github/scripts/tests/pytest-postprocess.py", ".github/workflows/allowed_dirs.yml":"ydb/github_toplevel/.github/workflows/allowed_dirs.yml", ".github/workflows/build_and_test_ondemand.yml":"ydb/github_toplevel/.github/workflows/build_and_test_ondemand.yml", ".github/workflows/build_and_test_provisioned.yml":"ydb/github_toplevel/.github/workflows/build_and_test_provisioned.yml", |