2022-07-13 02:34:02 +00:00
|
|
|
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
|
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
"""
|
|
|
|
This file is used in CI generate binary files for different kinds of apps
|
|
|
|
"""
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import os
|
|
|
|
import sys
|
2023-05-24 02:53:57 +00:00
|
|
|
import typing as t
|
2023-01-03 06:14:49 +00:00
|
|
|
import unittest
|
2022-07-13 02:34:02 +00:00
|
|
|
from collections import defaultdict
|
|
|
|
from pathlib import Path
|
|
|
|
|
2022-09-06 06:58:16 +00:00
|
|
|
import yaml
|
2022-07-13 02:34:02 +00:00
|
|
|
from idf_build_apps import LOGGER, App, build_apps, find_apps, setup_logging
|
2022-09-06 06:58:16 +00:00
|
|
|
from idf_build_apps.constants import SUPPORTED_TARGETS
|
|
|
|
from idf_ci_utils import IDF_PATH, PytestApp, get_pytest_cases, get_ttfw_app_paths
|
2022-07-13 02:34:02 +00:00
|
|
|
|
2023-01-03 06:14:17 +00:00
|
|
|
CI_ENV_VARS = {
|
|
|
|
'EXTRA_CFLAGS': '-Werror -Werror=deprecated-declarations -Werror=unused-variable '
|
2023-05-24 02:53:57 +00:00
|
|
|
'-Werror=unused-but-set-variable -Werror=unused-function -Wstrict-prototypes',
|
2023-01-03 06:14:17 +00:00
|
|
|
'EXTRA_CXXFLAGS': '-Werror -Werror=deprecated-declarations -Werror=unused-variable '
|
2023-05-24 02:53:57 +00:00
|
|
|
'-Werror=unused-but-set-variable -Werror=unused-function',
|
2023-01-03 06:14:17 +00:00
|
|
|
'LDGEN_CHECK_MAPPING': '1',
|
|
|
|
}
|
|
|
|
|
2022-07-13 02:34:02 +00:00
|
|
|
|
|
|
|
def get_pytest_apps(
|
2023-05-24 02:53:57 +00:00
|
|
|
paths: t.List[str],
|
2022-07-13 02:34:02 +00:00
|
|
|
target: str,
|
2023-05-24 02:53:57 +00:00
|
|
|
config_rules_str: t.List[str],
|
2022-07-13 02:34:02 +00:00
|
|
|
marker_expr: str,
|
2022-11-29 08:14:26 +00:00
|
|
|
filter_expr: str,
|
2022-07-13 02:34:02 +00:00
|
|
|
preserve_all: bool = False,
|
2023-05-24 02:53:57 +00:00
|
|
|
extra_default_build_targets: t.Optional[t.List[str]] = None,
|
|
|
|
modified_components: t.Optional[t.List[str]] = None,
|
|
|
|
modified_files: t.Optional[t.List[str]] = None,
|
|
|
|
ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = None,
|
|
|
|
) -> t.List[App]:
|
2022-11-29 08:14:26 +00:00
|
|
|
pytest_cases = get_pytest_cases(paths, target, marker_expr, filter_expr)
|
2022-07-13 02:34:02 +00:00
|
|
|
|
2023-05-24 02:53:57 +00:00
|
|
|
_paths: t.Set[str] = set()
|
2022-07-18 02:17:54 +00:00
|
|
|
test_related_app_configs = defaultdict(set)
|
2022-07-13 02:34:02 +00:00
|
|
|
for case in pytest_cases:
|
|
|
|
for app in case.apps:
|
|
|
|
_paths.add(app.path)
|
2022-07-18 02:17:54 +00:00
|
|
|
|
|
|
|
if os.getenv('INCLUDE_NIGHTLY_RUN') == '1':
|
|
|
|
test_related_app_configs[app.path].add(app.config)
|
|
|
|
elif os.getenv('NIGHTLY_RUN') == '1':
|
|
|
|
if case.nightly_run:
|
|
|
|
test_related_app_configs[app.path].add(app.config)
|
|
|
|
else:
|
|
|
|
if not case.nightly_run:
|
|
|
|
test_related_app_configs[app.path].add(app.config)
|
2022-07-13 02:34:02 +00:00
|
|
|
|
2023-05-24 02:53:57 +00:00
|
|
|
if not extra_default_build_targets:
|
|
|
|
extra_default_build_targets = []
|
|
|
|
|
2022-07-13 02:34:02 +00:00
|
|
|
app_dirs = list(_paths)
|
|
|
|
if not app_dirs:
|
|
|
|
raise RuntimeError('No apps found')
|
|
|
|
|
|
|
|
LOGGER.info(f'Found {len(app_dirs)} apps')
|
|
|
|
app_dirs.sort()
|
|
|
|
|
|
|
|
apps = find_apps(
|
|
|
|
app_dirs,
|
|
|
|
target=target,
|
2023-04-06 06:24:20 +00:00
|
|
|
build_dir='build_@t_@w',
|
2022-07-13 02:34:02 +00:00
|
|
|
config_rules_str=config_rules_str,
|
|
|
|
build_log_path='build_log.txt',
|
2023-04-06 06:24:20 +00:00
|
|
|
size_json_path='size.json',
|
2022-07-13 02:34:02 +00:00
|
|
|
check_warnings=True,
|
2023-05-24 02:53:57 +00:00
|
|
|
manifest_rootpath=IDF_PATH,
|
2022-09-06 06:58:16 +00:00
|
|
|
manifest_files=[str(p) for p in Path(IDF_PATH).glob('**/.build-test-rules.yml')],
|
|
|
|
default_build_targets=SUPPORTED_TARGETS + extra_default_build_targets,
|
2023-05-24 02:53:57 +00:00
|
|
|
modified_components=modified_components,
|
|
|
|
modified_files=modified_files,
|
|
|
|
ignore_app_dependencies_filepatterns=ignore_app_dependencies_filepatterns,
|
2022-07-13 02:34:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
for app in apps:
|
2022-07-18 02:17:54 +00:00
|
|
|
is_test_related = app.config_name in test_related_app_configs[app.app_dir]
|
2022-07-13 02:34:02 +00:00
|
|
|
if not preserve_all and not is_test_related:
|
|
|
|
app.preserve = False
|
|
|
|
|
2023-04-06 06:24:20 +00:00
|
|
|
if app.target == 'linux':
|
|
|
|
app._size_json_path = None # no esp_idf_size for linux target
|
|
|
|
|
2022-07-13 02:34:02 +00:00
|
|
|
return apps # type: ignore
|
|
|
|
|
|
|
|
|
|
|
|
def get_cmake_apps(
|
2023-05-24 02:53:57 +00:00
|
|
|
paths: t.List[str],
|
2022-07-13 02:34:02 +00:00
|
|
|
target: str,
|
2023-05-24 02:53:57 +00:00
|
|
|
config_rules_str: t.List[str],
|
2022-07-13 02:34:02 +00:00
|
|
|
preserve_all: bool = False,
|
2023-05-24 02:53:57 +00:00
|
|
|
extra_default_build_targets: t.Optional[t.List[str]] = None,
|
|
|
|
modified_components: t.Optional[t.List[str]] = None,
|
|
|
|
modified_files: t.Optional[t.List[str]] = None,
|
|
|
|
ignore_app_dependencies_filepatterns: t.Optional[t.List[str]] = None,
|
|
|
|
) -> t.List[App]:
|
2022-07-13 02:34:02 +00:00
|
|
|
ttfw_app_dirs = get_ttfw_app_paths(paths, target)
|
2023-04-06 06:24:20 +00:00
|
|
|
|
2022-07-13 02:34:02 +00:00
|
|
|
apps = find_apps(
|
|
|
|
paths,
|
|
|
|
recursive=True,
|
|
|
|
target=target,
|
|
|
|
build_dir='build_@t_@w',
|
|
|
|
config_rules_str=config_rules_str,
|
|
|
|
build_log_path='build_log.txt',
|
|
|
|
size_json_path='size.json',
|
|
|
|
check_warnings=True,
|
|
|
|
preserve=False,
|
2023-05-24 02:53:57 +00:00
|
|
|
manifest_rootpath=IDF_PATH,
|
2022-09-06 06:58:16 +00:00
|
|
|
manifest_files=[str(p) for p in Path(IDF_PATH).glob('**/.build-test-rules.yml')],
|
|
|
|
default_build_targets=SUPPORTED_TARGETS + extra_default_build_targets,
|
2023-05-24 02:53:57 +00:00
|
|
|
modified_components=modified_components,
|
|
|
|
modified_files=modified_files,
|
|
|
|
ignore_app_dependencies_filepatterns=ignore_app_dependencies_filepatterns,
|
2022-07-13 02:34:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
apps_for_build = []
|
2022-09-06 06:58:16 +00:00
|
|
|
pytest_cases_apps = [app for case in get_pytest_cases(paths, target) for app in case.apps]
|
2022-07-13 02:34:02 +00:00
|
|
|
for app in apps:
|
|
|
|
if preserve_all or app.app_dir in ttfw_app_dirs: # relpath
|
|
|
|
app.preserve = True
|
|
|
|
|
2022-09-06 06:58:16 +00:00
|
|
|
if PytestApp(os.path.realpath(app.app_dir), app.target, app.config_name) in pytest_cases_apps:
|
2022-07-13 02:34:02 +00:00
|
|
|
LOGGER.debug('Skipping build app with pytest scripts: %s', app)
|
|
|
|
continue
|
|
|
|
|
2023-04-06 06:24:20 +00:00
|
|
|
if app.target == 'linux':
|
|
|
|
app._size_json_path = None # no esp_idf_size for linux target
|
|
|
|
|
2022-07-13 02:34:02 +00:00
|
|
|
apps_for_build.append(app)
|
|
|
|
|
|
|
|
return apps_for_build
|
|
|
|
|
|
|
|
|
|
|
|
APPS_BUILD_PER_JOB = 30
|
|
|
|
|
|
|
|
|
|
|
|
def main(args: argparse.Namespace) -> None:
|
2023-05-24 02:53:57 +00:00
|
|
|
extra_default_build_targets: t.List[str] = []
|
2022-09-06 06:58:16 +00:00
|
|
|
if args.default_build_test_rules:
|
|
|
|
with open(args.default_build_test_rules) as fr:
|
|
|
|
configs = yaml.safe_load(fr)
|
|
|
|
|
|
|
|
if configs:
|
|
|
|
extra_default_build_targets = configs.get('extra_default_build_targets') or []
|
|
|
|
|
2022-07-13 02:34:02 +00:00
|
|
|
if args.pytest_apps:
|
|
|
|
LOGGER.info('Only build apps with pytest scripts')
|
|
|
|
apps = get_pytest_apps(
|
2022-09-06 06:58:16 +00:00
|
|
|
args.paths,
|
|
|
|
args.target,
|
|
|
|
args.config,
|
|
|
|
args.marker_expr,
|
2022-11-29 08:14:26 +00:00
|
|
|
args.filter_expr,
|
2022-09-06 06:58:16 +00:00
|
|
|
args.preserve_all,
|
|
|
|
extra_default_build_targets,
|
2023-05-24 02:53:57 +00:00
|
|
|
args.modified_components,
|
|
|
|
args.modified_files,
|
|
|
|
args.ignore_app_dependencies_filepatterns,
|
2022-07-13 02:34:02 +00:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
LOGGER.info('build apps. will skip pytest apps with pytest scripts')
|
2022-09-06 06:58:16 +00:00
|
|
|
apps = get_cmake_apps(
|
|
|
|
args.paths,
|
|
|
|
args.target,
|
|
|
|
args.config,
|
|
|
|
args.preserve_all,
|
|
|
|
extra_default_build_targets,
|
2023-05-24 02:53:57 +00:00
|
|
|
args.modified_components,
|
|
|
|
args.modified_files,
|
|
|
|
args.ignore_app_dependencies_filepatterns,
|
2022-09-06 06:58:16 +00:00
|
|
|
)
|
2022-07-13 02:34:02 +00:00
|
|
|
|
|
|
|
LOGGER.info('Found %d apps after filtering', len(apps))
|
|
|
|
LOGGER.info(
|
|
|
|
'Suggest setting the parallel count to %d for this build job',
|
|
|
|
len(apps) // APPS_BUILD_PER_JOB + 1,
|
|
|
|
)
|
|
|
|
|
|
|
|
if args.extra_preserve_dirs:
|
|
|
|
for app in apps:
|
|
|
|
if app.preserve:
|
|
|
|
continue
|
|
|
|
for extra_preserve_dir in args.extra_preserve_dirs:
|
2022-07-25 07:54:49 +00:00
|
|
|
abs_extra_preserve_dir = Path(extra_preserve_dir).resolve()
|
|
|
|
abs_app_dir = Path(app.app_dir).resolve()
|
2022-09-06 06:58:16 +00:00
|
|
|
if abs_extra_preserve_dir == abs_app_dir or abs_extra_preserve_dir in abs_app_dir.parents:
|
2022-07-13 02:34:02 +00:00
|
|
|
app.preserve = True
|
|
|
|
|
2023-05-24 02:53:57 +00:00
|
|
|
res = build_apps(
|
|
|
|
apps,
|
|
|
|
parallel_count=args.parallel_count,
|
|
|
|
parallel_index=args.parallel_index,
|
|
|
|
dry_run=False,
|
|
|
|
build_verbose=args.build_verbose,
|
|
|
|
keep_going=True,
|
|
|
|
collect_size_info='size_info.txt',
|
|
|
|
collect_app_info='list_job_@p.txt',
|
|
|
|
ignore_warning_strs=args.ignore_warning_str,
|
|
|
|
ignore_warning_file=args.ignore_warning_file,
|
|
|
|
copy_sdkconfig=args.copy_sdkconfig,
|
|
|
|
modified_components=args.modified_components,
|
|
|
|
modified_files=args.modified_files,
|
|
|
|
ignore_app_dependencies_filepatterns=args.ignore_app_dependencies_filepatterns,
|
2022-07-13 02:34:02 +00:00
|
|
|
)
|
|
|
|
|
2023-05-24 02:53:57 +00:00
|
|
|
if isinstance(res, tuple):
|
|
|
|
sys.exit(res[0])
|
|
|
|
else:
|
|
|
|
sys.exit(res)
|
|
|
|
|
2022-07-13 02:34:02 +00:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
description='Build all the apps for different test types. Will auto remove those non-test apps binaries',
|
|
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
|
|
)
|
|
|
|
parser.add_argument('paths', nargs='+', help='Paths to the apps to build.')
|
|
|
|
parser.add_argument(
|
|
|
|
'-t',
|
|
|
|
'--target',
|
2022-11-29 08:14:26 +00:00
|
|
|
default='all',
|
|
|
|
help='Build apps for given target',
|
2022-07-13 02:34:02 +00:00
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--config',
|
|
|
|
default=['sdkconfig.ci=default', 'sdkconfig.ci.*=', '=default'],
|
2023-05-24 06:28:26 +00:00
|
|
|
nargs='+',
|
2022-07-13 02:34:02 +00:00
|
|
|
help='Adds configurations (sdkconfig file names) to build. This can either be '
|
|
|
|
'FILENAME[=NAME] or FILEPATTERN. FILENAME is the name of the sdkconfig file, '
|
|
|
|
'relative to the project directory, to be used. Optional NAME can be specified, '
|
|
|
|
'which can be used as a name of this configuration. FILEPATTERN is the name of '
|
|
|
|
'the sdkconfig file, relative to the project directory, with at most one wildcard. '
|
|
|
|
'The part captured by the wildcard is used as the name of the configuration.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'-v',
|
|
|
|
'--verbose',
|
|
|
|
action='count',
|
|
|
|
help='Increase the LOGGER level of the script. Can be specified multiple times.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--build-verbose',
|
|
|
|
action='store_true',
|
|
|
|
help='Enable verbose output from build system.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--preserve-all',
|
|
|
|
action='store_true',
|
|
|
|
help='Preserve the binaries for all apps when specified.',
|
|
|
|
)
|
2022-09-06 06:58:16 +00:00
|
|
|
parser.add_argument('--parallel-count', default=1, type=int, help='Number of parallel build jobs.')
|
2022-07-13 02:34:02 +00:00
|
|
|
parser.add_argument(
|
|
|
|
'--parallel-index',
|
|
|
|
default=1,
|
|
|
|
type=int,
|
|
|
|
help='Index (1-based) of the job, out of the number specified by --parallel-count.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--collect-size-info',
|
|
|
|
help='If specified, the test case name and size info json will be written to this file',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--collect-app-info',
|
|
|
|
help='If specified, the test case name and app info json will be written to this file',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--ignore-warning-str',
|
2023-05-24 06:28:26 +00:00
|
|
|
nargs='+',
|
2023-05-24 02:53:57 +00:00
|
|
|
help='Ignore the warning string that match the specified regex in the build output. space-separated list',
|
2022-07-13 02:34:02 +00:00
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--ignore-warning-file',
|
|
|
|
default=os.path.join(IDF_PATH, 'tools', 'ci', 'ignore_build_warnings.txt'),
|
|
|
|
type=argparse.FileType('r'),
|
|
|
|
help='Ignore the warning strings in the specified file. Each line should be a regex string.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--copy-sdkconfig',
|
|
|
|
action='store_true',
|
|
|
|
help='Copy the sdkconfig file to the build directory.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
2022-07-25 07:54:49 +00:00
|
|
|
'--extra-preserve-dirs',
|
|
|
|
nargs='+',
|
|
|
|
help='also preserve binaries of the apps under the specified dirs',
|
2022-07-13 02:34:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
'--pytest-apps',
|
|
|
|
action='store_true',
|
|
|
|
help='Only build apps with pytest scripts. Will build apps without pytest scripts if this flag is unspecified.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'-m',
|
|
|
|
'--marker-expr',
|
|
|
|
default='not host_test', # host_test apps would be built and tested under the same job
|
|
|
|
help='only build tests matching given mark expression. For example: -m "host_test and generic". Works only'
|
|
|
|
'for pytest',
|
|
|
|
)
|
2022-11-29 08:14:26 +00:00
|
|
|
parser.add_argument(
|
|
|
|
'-k',
|
|
|
|
'--filter-expr',
|
|
|
|
help='only build tests matching given filter expression. For example: -k "test_hello_world". Works only'
|
|
|
|
'for pytest',
|
|
|
|
)
|
2022-09-06 06:58:16 +00:00
|
|
|
parser.add_argument(
|
|
|
|
'--default-build-test-rules',
|
|
|
|
default=os.path.join(IDF_PATH, '.gitlab', 'ci', 'default-build-test-rules.yml'),
|
|
|
|
help='default build test rules config file',
|
|
|
|
)
|
2023-01-03 06:14:17 +00:00
|
|
|
parser.add_argument(
|
|
|
|
'--skip-setting-flags',
|
|
|
|
action='store_true',
|
|
|
|
help='by default this script would set the build flags exactly the same as the CI ones. '
|
|
|
|
'Set this flag to use your local build flags.',
|
|
|
|
)
|
2023-05-24 02:53:57 +00:00
|
|
|
parser.add_argument(
|
|
|
|
'--modified-components',
|
|
|
|
nargs='*',
|
|
|
|
default=None,
|
|
|
|
help='space-separated list which specifies the modified components. app with `depends_components` set in the '
|
|
|
|
'corresponding manifest files would only be built if depends on any of the specified components.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--modified-files',
|
|
|
|
nargs='*',
|
|
|
|
default=None,
|
|
|
|
help='space-separated list which specifies the modified files. app with `depends_filepatterns` set in the '
|
|
|
|
'corresponding manifest files would only be built if any of the specified file pattern matches any of the '
|
|
|
|
'specified modified files.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'-if',
|
|
|
|
'--ignore-app-dependencies-filepatterns',
|
|
|
|
nargs='*',
|
|
|
|
default=None,
|
|
|
|
help='space-separated list which specifies the file patterns used for ignoring checking the app dependencies. '
|
|
|
|
'The `depends_components` and `depends_filepatterns` set in the manifest files will be ignored when any of the '
|
|
|
|
'specified file patterns matches any of the modified files. Must be used together with --modified-files',
|
|
|
|
)
|
2022-07-13 02:34:02 +00:00
|
|
|
|
|
|
|
arguments = parser.parse_args()
|
|
|
|
|
|
|
|
setup_logging(arguments.verbose)
|
2023-01-03 06:14:17 +00:00
|
|
|
|
|
|
|
# skip setting flags in CI
|
|
|
|
if not arguments.skip_setting_flags and not os.getenv('CI_JOB_ID'):
|
|
|
|
for _k, _v in CI_ENV_VARS.items():
|
|
|
|
os.environ[_k] = _v
|
|
|
|
LOGGER.info(f'env var {_k} set to "{_v}"')
|
|
|
|
|
2023-05-24 02:53:57 +00:00
|
|
|
if os.getenv('IS_MR_PIPELINE') == '0' or os.getenv('BUILD_AND_TEST_ALL_APPS') == '1':
|
|
|
|
# if it's not MR pipeline or env var BUILD_AND_TEST_ALL_APPS=1,
|
|
|
|
# remove component dependency related arguments
|
|
|
|
if 'modified_components' in arguments:
|
|
|
|
arguments.modified_components = None
|
|
|
|
if 'modified_files' in arguments:
|
|
|
|
arguments.modified_files = None
|
|
|
|
|
|
|
|
# file patterns to tigger full build
|
|
|
|
if 'modified_components' in arguments and not arguments.ignore_app_dependencies_filepatterns:
|
|
|
|
arguments.ignore_app_dependencies_filepatterns = [
|
|
|
|
# tools
|
|
|
|
'tools/cmake/**/*',
|
|
|
|
'tools/tools.json',
|
|
|
|
# components
|
|
|
|
'components/cxx/**/*',
|
|
|
|
'components/esp_common/**/*',
|
|
|
|
'components/esp_hw_support/**/*',
|
|
|
|
'components/esp_rom/**/*',
|
|
|
|
'components/esp_system/**/*',
|
|
|
|
'components/esp_timer/**/*',
|
|
|
|
'components/freertos/**/*',
|
|
|
|
'components/hal/**/*',
|
|
|
|
'components/heap/**/*',
|
|
|
|
'components/log/**/*',
|
|
|
|
'components/newlib/**/*',
|
|
|
|
'components/riscv/**/*',
|
|
|
|
'components/soc/**/*',
|
|
|
|
'components/xtensa/**/*',
|
|
|
|
]
|
|
|
|
|
2022-07-13 02:34:02 +00:00
|
|
|
main(arguments)
|
2023-01-03 06:14:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TestParsingShellScript(unittest.TestCase):
|
|
|
|
"""
|
|
|
|
This test case is run in CI jobs to make sure the CI build flags is the same as the ones recorded in CI_ENV_VARS
|
|
|
|
"""
|
|
|
|
|
|
|
|
def test_parse_result(self) -> None:
|
|
|
|
for k, v in CI_ENV_VARS.items():
|
|
|
|
self.assertEqual(os.getenv(k), v)
|