Merge branch 'ci/known_generate_target_test_issues' into 'master'

ci: add known warnings while generating the target test jobs

Closes IDFCI-1941

See merge request espressif/esp-idf!28551
pull/13114/head
Fu Hanxi 2024-01-30 00:25:24 +08:00
commit 7913c42996
13 zmienionych plików z 297 dodań i 87 usunięć

Wyświetl plik

@ -287,6 +287,7 @@ generate_build_child_pipeline:
dependencies: # set dependencies to null to avoid missing artifacts issue
needs:
- pipeline_variables
- check_test_cases_env_markers_and_required_runners
artifacts:
paths:
- build_child_pipeline.yml

Wyświetl plik

@ -170,3 +170,9 @@ pipeline_variables:
artifacts:
reports:
dotenv: pipeline.env
check_test_cases_env_markers_and_required_runners:
extends:
- .pre_check_template
script:
- python tools/ci/dynamic_pipelines/scripts/generate_target_test_child_pipeline.py --check

Wyświetl plik

@ -157,13 +157,19 @@ repos:
additional_dependencies:
- PyYAML == 5.3.1
- idf-build-apps~=2.0
- id: sort-build-test-rules-ymls
name: sort .build-test-rules.yml files
entry: tools/ci/check_build_test_rules.py sort-yaml
- id: sort-yaml-files
name: sort yaml files
entry: tools/ci/sort_yaml.py
language: python
files: '\.build-test-rules\.yml'
files: '\.build-test-rules\.yml$|known_generate_test_child_pipeline_warnings\.yml$'
additional_dependencies:
- ruamel.yaml
- id: sort-yaml-test
name: sort yaml test
entry: python -m unittest tools/ci/sort_yaml.py
language: python
files: 'tools/ci/sort_yaml\.py$'
additional_dependencies:
- PyYAML == 5.3.1
- ruamel.yaml
- id: check-build-test-rules-path-exists
name: check path in .build-test-rules.yml exists

Wyświetl plik

@ -11,8 +11,6 @@
import os
import sys
import gitlab
if os.path.join(os.path.dirname(__file__), 'tools', 'ci') not in sys.path:
sys.path.append(os.path.join(os.path.dirname(__file__), 'tools', 'ci'))
@ -161,22 +159,8 @@ def app_downloader(pipeline_id: t.Optional[str]) -> t.Optional[AppDownloader]:
logging.info('Downloading build report from the build pipeline %s', pipeline_id)
test_app_presigned_urls_file = None
try:
gl = gitlab_api.Gitlab(os.getenv('CI_PROJECT_ID', 'espressif/esp-idf'))
except gitlab.exceptions.GitlabAuthenticationError:
msg = """To download artifacts from gitlab, please create ~/.python-gitlab.cfg with the following content:
[global]
default = internal
ssl_verify = true
timeout = 5
[internal]
url = <OUR INTERNAL HTTPS SERVER URL>
private_token = <YOUR PERSONAL ACCESS TOKEN>
api_version = 4
"""
raise SystemExit(msg)
gl = gitlab_api.Gitlab(os.getenv('CI_PROJECT_ID', 'espressif/esp-idf'))
for child_pipeline in gl.project.pipelines.get(pipeline_id, lazy=True).bridges.list(iterator=True):
if child_pipeline.name == 'build_child_pipeline':

Wyświetl plik

@ -6,7 +6,6 @@ import inspect
import os
import re
import sys
from io import StringIO
from pathlib import Path
from typing import Dict
from typing import List
@ -345,38 +344,6 @@ def check_test_scripts(
sys.exit(exit_code)
def sort_yaml(files: List[str]) -> None:
from ruamel.yaml import YAML, CommentedMap
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
yaml.width = 4096 # avoid wrap lines
exit_code = 0
for f in files:
with open(f) as fr:
file_s = fr.read()
fr.seek(0)
file_d: CommentedMap = yaml.load(fr)
sorted_yaml = CommentedMap(dict(sorted(file_d.items())))
file_d.copy_attributes(sorted_yaml)
with StringIO() as s:
yaml.dump(sorted_yaml, s)
string = s.getvalue()
if string != file_s:
with open(f, 'w') as fw:
fw.write(string)
print(
f'Sorted yaml file {f}. Please take a look. sometimes the format is a bit messy'
)
exit_code = 1
sys.exit(exit_code)
def check_exist() -> None:
exit_code = 0
@ -422,9 +389,6 @@ if __name__ == '__main__':
help='default build test rules config file',
)
_sort_yaml = action.add_parser('sort-yaml')
_sort_yaml.add_argument('files', nargs='+', help='all specified yaml files')
_check_exist = action.add_parser('check-exist')
arg = parser.parse_args()
@ -434,9 +398,7 @@ if __name__ == '__main__':
os.path.join(os.path.dirname(__file__), '..', '..')
)
if arg.action == 'sort-yaml':
sort_yaml(arg.files)
elif arg.action == 'check-exist':
if arg.action == 'check-exist':
check_exist()
else:
check_dirs = set()

Wyświetl plik

@ -1,6 +1,5 @@
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
from idf_ci_utils import IDF_PATH
@ -31,3 +30,7 @@ REPORT_TEMPLATE_FILEPATH = os.path.join(
)
BUILD_ONLY_LABEL = 'For Maintainers: Only Build Tests'
KNOWN_GENERATE_TEST_CHILD_PIPELINE_WARNINGS_FILEPATH = os.path.join(
IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'known_generate_test_child_pipeline_warnings.yml'
)

Wyświetl plik

@ -1,28 +1,36 @@
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
"""This file is used for generating the child pipeline for target test jobs.
1. Check the build jobs' artifacts to get the built apps' information.
2. Post the Build Report if it's running in an MR pipeline.
3. Generate the child pipeline for target test jobs.
"""
import argparse
import glob
import logging
import os
import typing as t
from collections import Counter, defaultdict
from collections import Counter
from collections import defaultdict
import __init__ # noqa: F401 # inject the system path
from dynamic_pipelines.constants import (BUILD_ONLY_LABEL, DEFAULT_CASES_TEST_PER_JOB,
DEFAULT_TARGET_TEST_CHILD_PIPELINE_FILEPATH, DEFAULT_TEST_PATHS)
from dynamic_pipelines.models import EmptyJob, Job, TargetTestJob
import yaml
from dynamic_pipelines.constants import BUILD_ONLY_LABEL
from dynamic_pipelines.constants import DEFAULT_CASES_TEST_PER_JOB
from dynamic_pipelines.constants import DEFAULT_TARGET_TEST_CHILD_PIPELINE_FILEPATH
from dynamic_pipelines.constants import DEFAULT_TEST_PATHS
from dynamic_pipelines.constants import KNOWN_GENERATE_TEST_CHILD_PIPELINE_WARNINGS_FILEPATH
from dynamic_pipelines.models import EmptyJob
from dynamic_pipelines.models import Job
from dynamic_pipelines.models import TargetTestJob
from dynamic_pipelines.utils import dump_jobs_to_yaml
from gitlab.v4.objects import Project
from gitlab_api import Gitlab
from idf_build_apps import App
from idf_build_apps.constants import BuildStatus
from idf_ci.app import import_apps_from_txt
from idf_pytest.script import get_all_apps
from idf_pytest.script import get_pytest_cases
@ -41,16 +49,23 @@ def get_tags_with_amount(s: str) -> t.List[str]:
return sorted(res)
def get_target_test_jobs(project: Project, paths: str, apps: t.List[App]) -> t.Tuple[t.List[Job], t.List[str]]:
def get_target_test_jobs(
project: Project, paths: str, apps: t.List[App]
) -> t.Tuple[t.List[Job], t.List[str], t.Dict[str, t.List[str]]]:
"""
Return the target test jobs and the extra yaml files to include
"""
issues: t.Dict[str, t.List[str]] = {
'no_env_marker_test_cases': [],
'no_runner_tags': [],
}
if mr_labels := os.getenv('CI_MERGE_REQUEST_LABELS'):
print(f'MR labels: {mr_labels}')
if BUILD_ONLY_LABEL in mr_labels.split(','):
print('MR has build only label, skip generating target test child pipeline')
return [EmptyJob()], []
return [EmptyJob()], [], issues
pytest_cases = get_pytest_cases(
paths,
@ -61,7 +76,7 @@ def get_target_test_jobs(project: Project, paths: str, apps: t.List[App]) -> t.T
res = defaultdict(list)
for case in pytest_cases:
if not case.env_markers:
print(f'No env markers found for {case.item.originalname} in {case.path}. Ignoring...')
issues['no_env_marker_test_cases'].append(case.item.nodeid)
continue
res[(case.target_selector, tuple(sorted(case.env_markers)))].append(case)
@ -72,9 +87,10 @@ def get_target_test_jobs(project: Project, paths: str, apps: t.List[App]) -> t.T
# we don't need to get all runner, as long as we get one runner, it's fine
runner_list = project.runners.list(status='online', tag_list=','.join(runner_tags), get_all=False)
if not runner_list:
print(f'WARNING: No runner found with tag {",".join(runner_tags)}, ignoring the following test cases:')
issues['no_runner_tags'].append(','.join(runner_tags))
logging.warning(f'No runner found for {",".join(runner_tags)}, required by cases:')
for case in cases:
print(f' - {case.name}')
logging.warning(f' - {case.item.nodeid}')
continue
target_test_job = TargetTestJob(
@ -95,11 +111,48 @@ def get_target_test_jobs(project: Project, paths: str, apps: t.List[App]) -> t.T
else:
extra_include_yml = ['tools/ci/dynamic_pipelines/templates/generate_target_test_report.yml']
return target_test_jobs, extra_include_yml
issues['no_env_marker_test_cases'] = sorted(issues['no_env_marker_test_cases'])
issues['no_runner_tags'] = sorted(issues['no_runner_tags'])
return target_test_jobs, extra_include_yml, issues
def generate_target_test_child_pipeline(project: Project, paths: str, apps: t.List[App], output_filepath: str) -> None:
target_test_jobs, extra_include_yml = get_target_test_jobs(project, paths, apps)
def generate_target_test_child_pipeline(
project: Project,
paths: str,
apps: t.List[App],
output_filepath: str,
) -> None:
target_test_jobs, extra_include_yml, issues = get_target_test_jobs(project, paths, apps)
with open(KNOWN_GENERATE_TEST_CHILD_PIPELINE_WARNINGS_FILEPATH) as fr:
known_warnings_dict = yaml.safe_load(fr) or dict()
failed = False
known_no_env_marker_test_cases = set(known_warnings_dict.get('no_env_marker_test_cases', []))
no_env_marker_test_cases = set(issues['no_env_marker_test_cases'])
if no_env_marker_test_cases - known_no_env_marker_test_cases:
print('ERROR: NEW "no_env_marker_test_cases" detected:')
for case in no_env_marker_test_cases - known_no_env_marker_test_cases:
print(f' - {case}')
failed = True
known_no_runner_tags = set(known_warnings_dict.get('no_runner_tags', []))
no_runner_tags = set(issues['no_runner_tags'])
if no_runner_tags - known_no_runner_tags:
print('ERROR: NEW "no_runner_tags" detected:')
for tag in no_runner_tags - known_no_runner_tags:
print(f' - {tag}')
failed = True
if failed:
raise SystemExit(
f'Please fix the issue, '
f'or update the known warnings file: {KNOWN_GENERATE_TEST_CHILD_PIPELINE_WARNINGS_FILEPATH}'
)
dump_jobs_to_yaml(target_test_jobs, output_filepath, extra_include_yml)
print(f'Generate child pipeline yaml file {output_filepath} with {sum(j.parallel for j in target_test_jobs)} jobs')
@ -134,13 +187,37 @@ if __name__ == '__main__':
default=DEFAULT_TARGET_TEST_CHILD_PIPELINE_FILEPATH,
help='Output child pipeline file path',
)
parser.add_argument(
'--check',
action='store_true',
help='Check if the child pipeline could be generated successfully. '
'test cases without env marker or required unset runner will be printed out. '
'(Note: All apps and test cases will be checked)',
)
parser.add_argument(
'--app-info-filepattern',
default='list_job_*.txt',
help='glob pattern to specify the files that include built app info generated by '
'`idf-build-apps --collect-app-info ...`. will not raise ValueError when binary '
'paths not exist in local file system if not listed recorded in the app info.',
)
args = parser.parse_args()
app_list_filepattern = 'list_job_*.txt'
apps = []
for f in glob.glob(app_list_filepattern):
apps.extend(import_apps_from_txt(f))
gl_project = Gitlab(args.project_id).project
generate_target_test_child_pipeline(gl_project, args.paths, apps, args.output)
if args.check:
apps = list(get_all_apps(args.paths)[0]) # test related apps only
for app in apps:
app.build_status = BuildStatus.SUCCESS # pretend they are built successfully
else:
apps = []
for f in glob.glob(args.app_info_filepattern):
apps.extend(import_apps_from_txt(f))
generate_target_test_child_pipeline(
gl_project,
args.paths,
apps,
args.output,
)

Wyświetl plik

@ -0,0 +1,53 @@
no_env_marker_test_cases:
- components/fatfs/test_apps/flash_ro/pytest_fatfs_flash_ro.py::test_fatfs_flash_ro
- components/fatfs/test_apps/flash_wl/pytest_fatfs_flash_wl.py::test_fatfs_flash_wl_generic[default]
- components/fatfs/test_apps/flash_wl/pytest_fatfs_flash_wl.py::test_fatfs_flash_wl_generic[fastseek]
- components/fatfs/test_apps/flash_wl/pytest_fatfs_flash_wl.py::test_fatfs_flash_wl_generic[release]
- components/nvs_flash/test_apps/pytest_nvs_flash.py::test_nvs_flash[default]
- components/pthread/test_apps/pthread_psram_tests/pytest_pthread_psram_tests.py::test_pthread_psram
- components/vfs/test_apps/pytest_vfs.py::test_vfs_ccomp[ccomp]
- components/vfs/test_apps/pytest_vfs.py::test_vfs_default[default]
- components/vfs/test_apps/pytest_vfs.py::test_vfs_default[iram]
- examples/protocols/http_server/file_serving/pytest_http_server_file_serving.py::test_examples_protocol_http_server_file_serving[spiffs]
- examples/storage/fatfsgen/pytest_fatfsgen_example.py::test_examples_fatfsgen[test_read_only_partition_gen]
- examples/storage/fatfsgen/pytest_fatfsgen_example.py::test_examples_fatfsgen[test_read_only_partition_gen_default_dt]
- examples/storage/fatfsgen/pytest_fatfsgen_example.py::test_examples_fatfsgen[test_read_only_partition_gen_ln]
- examples/storage/fatfsgen/pytest_fatfsgen_example.py::test_examples_fatfsgen[test_read_only_partition_gen_ln_default_dt]
- examples/storage/fatfsgen/pytest_fatfsgen_example.py::test_examples_fatfsgen[test_read_write_partition_gen]
- examples/storage/fatfsgen/pytest_fatfsgen_example.py::test_examples_fatfsgen[test_read_write_partition_gen_default_dt]
- examples/storage/fatfsgen/pytest_fatfsgen_example.py::test_examples_fatfsgen[test_read_write_partition_gen_ln]
- examples/storage/fatfsgen/pytest_fatfsgen_example.py::test_examples_fatfsgen[test_read_write_partition_gen_ln_default_dt]
- examples/storage/nvs_rw_blob/pytest_nvs_rw_blob.py::test_examples_nvs_rw_blob
- examples/storage/nvs_rw_value/pytest_nvs_rw_value.py::test_examples_nvs_rw_value
- examples/storage/nvs_rw_value_cxx/pytest_nvs_rw_value_cxx.py::test_examples_nvs_rw_value_cxx
- examples/storage/nvsgen/pytest_nvsgen_example.py::test_nvsgen_example
- examples/storage/partition_api/partition_find/pytest_partition_find_example.py::test_partition_find_example
- examples/storage/partition_api/partition_mmap/pytest_partition_mmap_example.py::test_partition_mmap_example
- examples/storage/partition_api/partition_ops/pytest_partition_ops_example.py::test_partition_ops_example
- examples/storage/parttool/pytest_parttool_example.py::test_examples_parttool
- examples/storage/spiffs/pytest_spiffs_example.py::test_examples_spiffs
- examples/storage/spiffsgen/pytest_spiffsgen_example.py::test_spiffsgen_example
- examples/storage/wear_levelling/pytest_wear_levelling_example.py::test_wear_levelling_example
- tools/test_apps/system/panic/pytest_panic.py::test_gdbstub_coredump[gdbstub_coredump]
- tools/test_apps/system/panic/pytest_panic.py::test_panic_delay[panic_delay]
no_runner_tags:
- esp32,ip101
- esp32,psram,quad_psram
- esp32,quad_psram
- esp32c2,ethernet,xtal_40mhz
- esp32c2,ethernet_ota,xtal_40mhz
- esp32c2,jtag,xtal_40mhz
- esp32c3,Example_ShieldBox_Basic
- esp32c3,ethernet_flash_8m
- esp32c3,ethernet_ota
- esp32c3,sdcard_sdmode
- esp32c6,jtag
- esp32h2,jtag
- esp32s2,Example_ShieldBox_Basic
- esp32s2,ethernet_flash_8m
- esp32s2,ethernet_ota
- esp32s2,usb_host_flash_disk
- esp32s2,wifi_high_traffic
- esp32s3,Example_ShieldBox_Basic
- esp32s3,ethernet_flash_8m
- esp32s3,ethernet_ota

Wyświetl plik

@ -50,3 +50,4 @@ tools/ci/python_packages/gitlab_api.py
tools/ci/python_packages/idf_http_server_test/**/*
tools/ci/python_packages/idf_iperf_test_util/**/*
tools/esp_prov/**/*
tools/ci/sort_yaml.py

Wyświetl plik

@ -77,6 +77,7 @@ tools/ci/gitlab_yaml_linter.py
tools/ci/mirror-submodule-update.sh
tools/ci/multirun_with_pyenv.sh
tools/ci/push_to_github.sh
tools/ci/sort_yaml.py
tools/ci/test_autocomplete/test_autocomplete.py
tools/ci/test_configure_ci_environment.sh
tools/ci/test_reproducible_build.sh

Wyświetl plik

@ -1,6 +1,5 @@
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import argparse
import logging
import os
@ -11,7 +10,12 @@ import tempfile
import time
import zipfile
from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Union
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Union
import gitlab
@ -80,7 +84,24 @@ class Gitlab(object):
def _init_gitlab_inst(self, project_id: Optional[int], config_files: Optional[List[str]]) -> None:
gitlab_id = os.getenv('LOCAL_GITLAB_HTTPS_HOST') # if None, will use the default gitlab server
self.gitlab_inst = gitlab.Gitlab.from_config(gitlab_id=gitlab_id, config_files=config_files)
self.gitlab_inst.auth()
try:
self.gitlab_inst.auth()
except gitlab.exceptions.GitlabAuthenticationError:
msg = """To call gitlab apis locally, please create ~/.python-gitlab.cfg with the following content:
[global]
default = internal
ssl_verify = true
timeout = 5
[internal]
url = <OUR INTERNAL HTTPS SERVER URL>
private_token = <YOUR PERSONAL ACCESS TOKEN>
api_version = 4
"""
raise SystemExit(msg)
if project_id:
self.project = self.gitlab_inst.projects.get(project_id, lazy=True)
else:

Wyświetl plik

@ -0,0 +1,94 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
"""
Sort yaml file
Exit non-zero if any file is modified
"""
import io
import os
import sys
import tempfile
import unittest
from ruamel.yaml import CommentedMap
from ruamel.yaml import YAML
def sort_yaml(f: str) -> int:
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
yaml.width = 4096 # avoid wrap lines``
exit_code = 0
with open(f) as fr:
file_s = fr.read()
fr.seek(0)
try:
file_d: CommentedMap = yaml.load(fr)
except Exception as e:
print(f'Failed to load yaml file {f}: {e}')
return 1
# sort dict keys
sorted_yaml = CommentedMap(dict(sorted(file_d.items())))
file_d.copy_attributes(sorted_yaml)
# sort item
for k, v in sorted_yaml.items():
if isinstance(v, list):
sorted_yaml[k].sort()
with io.StringIO() as s:
yaml.dump(sorted_yaml, s)
string = s.getvalue()
if string != file_s:
with open(f, 'w') as fw:
fw.write(string)
print(f'Sorted yaml file {f}. Please take a look. sometimes the format is a bit messy')
exit_code = 1
return exit_code
class TestSortYaml(unittest.TestCase):
def test_sort_yaml(self) -> None:
_, test_yaml = tempfile.mkstemp()
with open(test_yaml, 'w') as fw:
fw.write(
'''no_runner: []
no_env_marker:
- 1
- 3 # foo
- 2 # bar'''
)
sort_yaml(fw.name)
try:
with open(test_yaml) as fr:
self.assertEqual(
fr.read(),
'''no_env_marker:
- 1
- 2 # bard
- 3 # foo
no_runner: []''',
)
except AssertionError:
print(f'Please check the sorted yaml file {test_yaml}')
else:
os.remove(test_yaml)
if __name__ == '__main__':
ret = 0
for _f in sys.argv[1:]:
exit_code = sort_yaml(_f)
if exit_code != 0 and ret == 0:
ret = exit_code
sys.exit(ret)

Wyświetl plik

@ -1,6 +1,5 @@
# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import os
import pytest
@ -11,6 +10,7 @@ PROMPT = 'test_intr_dump>'
@pytest.mark.esp32
@pytest.mark.qemu
@pytest.mark.host_test
def test_esp_intr_dump_nonshared(dut: Dut) -> None:
dut.expect_exact(PROMPT, timeout=10)
@ -24,6 +24,7 @@ def test_esp_intr_dump_nonshared(dut: Dut) -> None:
@pytest.mark.esp32
@pytest.mark.qemu
@pytest.mark.host_test
def test_esp_intr_dump_shared(dut: Dut) -> None:
dut.expect_exact(PROMPT, timeout=10)