Просмотр исходного кода

Now scan_tests will generate 'scan_<target>.json`

also updated CI yaml and shell scripts
Fu Hanxi 5 лет назад
Родитель
Сommit
b26d42afe3

+ 4 - 5
tools/build_apps.py

@@ -7,7 +7,7 @@
 import argparse
 import argparse
 import sys
 import sys
 import logging
 import logging
-from find_build_apps import BuildItem, BuildError, setup_logging, BUILD_SYSTEMS
+from find_build_apps import BuildItem, BuildError, setup_logging, BUILD_SYSTEMS, safe_exit_if_file_is_empty
 
 
 
 
 def main():
 def main():
@@ -33,8 +33,8 @@ def main():
         default=1,
         default=1,
         type=int,
         type=int,
         help="Number of parallel build jobs. Note that this script doesn't start the jobs, " +
         help="Number of parallel build jobs. Note that this script doesn't start the jobs, " +
-        "it needs to be executed multiple times with same value of --parallel-count and " +
-        "different values of --parallel-index.",
+             "it needs to be executed multiple times with same value of --parallel-count and " +
+             "different values of --parallel-index.",
     )
     )
     parser.add_argument(
     parser.add_argument(
         "--parallel-index",
         "--parallel-index",
@@ -71,11 +71,10 @@ def main():
         help="Name of the file to read the list of builds from. If not specified, read from stdin.",
         help="Name of the file to read the list of builds from. If not specified, read from stdin.",
     )
     )
     args = parser.parse_args()
     args = parser.parse_args()
-
     setup_logging(args)
     setup_logging(args)
 
 
+    safe_exit_if_file_is_empty(args.build_list.name)
     build_items = [BuildItem.from_json(line) for line in args.build_list]
     build_items = [BuildItem.from_json(line) for line in args.build_list]
-
     if not build_items:
     if not build_items:
         logging.error("Empty build list!")
         logging.error("Empty build list!")
         raise SystemExit(1)
         raise SystemExit(1)

+ 2 - 0
tools/ci/build_examples.sh

@@ -31,6 +31,7 @@ die() {
 [ -z ${BUILD_PATH} ] && die "BUILD_PATH is not set"
 [ -z ${BUILD_PATH} ] && die "BUILD_PATH is not set"
 [ -z ${IDF_TARGET} ] && die "IDF_TARGET is not set"
 [ -z ${IDF_TARGET} ] && die "IDF_TARGET is not set"
 [ -z ${EXAMPLE_TEST_BUILD_SYSTEM} ] && die "EXAMPLE_TEST_BUILD_SYSTEM is not set"
 [ -z ${EXAMPLE_TEST_BUILD_SYSTEM} ] && die "EXAMPLE_TEST_BUILD_SYSTEM is not set"
+[ -z ${SCAN_EXAMPLE_TEST_JSON} ] && die "SCAN_EXAMPLE_TEST_JSON is not set"
 [ -d ${LOG_PATH} ] || mkdir -p ${LOG_PATH}
 [ -d ${LOG_PATH} ] || mkdir -p ${LOG_PATH}
 [ -d ${BUILD_PATH} ] || mkdir -p ${BUILD_PATH}
 [ -d ${BUILD_PATH} ] || mkdir -p ${BUILD_PATH}
 
 
@@ -85,6 +86,7 @@ ${IDF_PATH}/tools/find_apps.py examples \
     --config 'sdkconfig.ci=default' \
     --config 'sdkconfig.ci=default' \
     --config 'sdkconfig.ci.*=' \
     --config 'sdkconfig.ci.*=' \
     --config '=default' \
     --config '=default' \
+    --scan-tests-json ${SCAN_EXAMPLE_TEST_JSON}
 
 
 # --config rules above explained:
 # --config rules above explained:
 # 1. If sdkconfig.ci exists, use it build the example with configuration name "default"
 # 1. If sdkconfig.ci exists, use it build the example with configuration name "default"

+ 2 - 0
tools/ci/build_test_apps.sh

@@ -29,6 +29,7 @@ die() {
 [ -z ${LOG_PATH} ] && die "LOG_PATH is not set"
 [ -z ${LOG_PATH} ] && die "LOG_PATH is not set"
 [ -z ${BUILD_PATH} ] && die "BUILD_PATH is not set"
 [ -z ${BUILD_PATH} ] && die "BUILD_PATH is not set"
 [ -z ${IDF_TARGET} ] && die "IDF_TARGET is not set"
 [ -z ${IDF_TARGET} ] && die "IDF_TARGET is not set"
+[ -z ${SCAN_CUSTOM_TEST_JSON} ] && die "SCAN_CUSTOM_TEST_JSON is not set"
 [ -d ${LOG_PATH} ] || mkdir -p ${LOG_PATH}
 [ -d ${LOG_PATH} ] || mkdir -p ${LOG_PATH}
 [ -d ${BUILD_PATH} ] || mkdir -p ${BUILD_PATH}
 [ -d ${BUILD_PATH} ] || mkdir -p ${BUILD_PATH}
 
 
@@ -74,6 +75,7 @@ ${IDF_PATH}/tools/find_apps.py tools/test_apps \
     --config 'sdkconfig.ci=default' \
     --config 'sdkconfig.ci=default' \
     --config 'sdkconfig.ci.*=' \
     --config 'sdkconfig.ci.*=' \
     --config '=default' \
     --config '=default' \
+    --scan-tests-json ${SCAN_CUSTOM_TEST_JSON}
 
 
 # --config rules above explained:
 # --config rules above explained:
 # 1. If sdkconfig.ci exists, use it build the example with configuration name "default"
 # 1. If sdkconfig.ci exists, use it build the example with configuration name "default"

+ 3 - 2
tools/ci/check_build_warnings.py

@@ -13,10 +13,10 @@ import logging
 import re
 import re
 
 
 try:
 try:
-    from find_build_apps import BuildItem, setup_logging
+    from find_build_apps import BuildItem, setup_logging, safe_exit_if_file_is_empty
 except ImportError:
 except ImportError:
     sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
     sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
-    from find_build_apps import BuildItem, setup_logging
+    from find_build_apps import BuildItem, setup_logging, safe_exit_if_file_is_empty
 
 
 WARNING_REGEX = re.compile(r"(?:error|warning)[^\w]", re.MULTILINE | re.IGNORECASE)
 WARNING_REGEX = re.compile(r"(?:error|warning)[^\w]", re.MULTILINE | re.IGNORECASE)
 
 
@@ -69,6 +69,7 @@ def main():
     args = parser.parse_args()
     args = parser.parse_args()
     setup_logging(args)
     setup_logging(args)
 
 
+    safe_exit_if_file_is_empty(args.build_list.name)
     build_items = [BuildItem.from_json(line) for line in args.build_list]
     build_items = [BuildItem.from_json(line) for line in args.build_list]
 
 
     if not build_items:
     if not build_items:

+ 9 - 2
tools/ci/config/build.yml

@@ -126,6 +126,8 @@ build_examples_make:
 # same as above, but for CMake
 # same as above, but for CMake
 .build_examples_cmake: &build_examples_cmake
 .build_examples_cmake: &build_examples_cmake
   extends: .build_examples_template
   extends: .build_examples_template
+  dependencies:
+    - scan_tests
   artifacts:
   artifacts:
     paths:
     paths:
       - build_examples/list.json
       - build_examples/list.json
@@ -142,6 +144,8 @@ build_examples_make:
     LOG_PATH: "${CI_PROJECT_DIR}/log_examples"
     LOG_PATH: "${CI_PROJECT_DIR}/log_examples"
     BUILD_PATH: "${CI_PROJECT_DIR}/build_examples"
     BUILD_PATH: "${CI_PROJECT_DIR}/build_examples"
     EXAMPLE_TEST_BUILD_SYSTEM: "cmake"
     EXAMPLE_TEST_BUILD_SYSTEM: "cmake"
+    SCAN_EXAMPLE_TEST_JSON: ${CI_PROJECT_DIR}/examples/test_configs/scan_${IDF_TARGET}.json
+
 
 
 build_examples_cmake_esp32:
 build_examples_cmake_esp32:
   extends: .build_examples_cmake
   extends: .build_examples_cmake
@@ -156,6 +160,8 @@ build_examples_cmake_esp32s2:
 .build_test_apps: &build_test_apps
 .build_test_apps: &build_test_apps
   extends: .build_template
   extends: .build_template
   stage: build
   stage: build
+  dependencies:
+    - scan_tests
   artifacts:
   artifacts:
     when: always
     when: always
     paths:
     paths:
@@ -171,8 +177,9 @@ build_examples_cmake_esp32s2:
       - $LOG_PATH
       - $LOG_PATH
     expire_in: 3 days
     expire_in: 3 days
   variables:
   variables:
-    LOG_PATH: "$CI_PROJECT_DIR/log_test_apps"
-    BUILD_PATH: "$CI_PROJECT_DIR/build_test_apps"
+    LOG_PATH: "${CI_PROJECT_DIR}/log_test_apps"
+    BUILD_PATH: "${CI_PROJECT_DIR}/build_test_apps"
+    SCAN_CUSTOM_TEST_JSON: ${CI_PROJECT_DIR}/tools/test_apps/test_configs/scan_${IDF_TARGET}.json
   only:
   only:
     variables:
     variables:
       - $BOT_TRIGGER_WITH_LABEL == null
       - $BOT_TRIGGER_WITH_LABEL == null

+ 27 - 0
tools/ci/config/pre_check.yml

@@ -190,3 +190,30 @@ check_public_headers:
   script:
   script:
     - python tools/ci/check_public_headers.py --jobs 4 --prefix xtensa-esp32-elf-
     - python tools/ci/check_public_headers.py --jobs 4 --prefix xtensa-esp32-elf-
 
 
+.scan_build_tests:
+  stage: pre_check
+  image: $CI_DOCKER_REGISTRY/ubuntu-test-env$BOT_DOCKER_IMAGE_TAG
+  tags:
+    - assign_test
+  variables:
+    CI_SCAN_TESTS_PY: ${CI_PROJECT_DIR}/tools/ci/python_packages/ttfw_idf/CIScanTests.py
+    TEST_CONFIG_FILE: ${CI_PROJECT_DIR}/tools/ci/config/target-test.yml
+
+scan_tests:
+  extends: .scan_build_tests
+  only:
+    variables:
+      - $BOT_LABEL_EXAMPLE_TEST
+      - $BOT_LABEL_CUSTOM_TEST
+  artifacts:
+    paths:
+      - $EXAMPLE_TEST_OUTPUT_DIR
+      - $TEST_APPS_OUTPUT_DIR
+  variables:
+    EXAMPLE_TEST_DIR: ${CI_PROJECT_DIR}/examples
+    EXAMPLE_TEST_OUTPUT_DIR: ${CI_PROJECT_DIR}/examples/test_configs
+    TEST_APPS_TEST_DIR: ${CI_PROJECT_DIR}/tools/test_apps
+    TEST_APPS_OUTPUT_DIR: ${CI_PROJECT_DIR}/tools/test_apps/test_configs
+  script:
+    - python $CI_SCAN_TESTS_PY example_test $EXAMPLE_TEST_DIR -c $TEST_CONFIG_FILE -o $EXAMPLE_TEST_OUTPUT_DIR
+    - python $CI_SCAN_TESTS_PY test_apps $TEST_APPS_TEST_DIR -c $TEST_CONFIG_FILE -o $TEST_APPS_OUTPUT_DIR

+ 5 - 1
tools/ci/python_packages/gitlab_api.py

@@ -124,7 +124,7 @@ class Gitlab(object):
 
 
         return raw_data_list
         return raw_data_list
 
 
-    def find_job_id(self, job_name, pipeline_id=None, job_status="success"):
+    def find_job_id(self, job_name, pipeline_id=None, job_status="success", suffix=None):
         """
         """
         Get Job ID from job name of specific pipeline
         Get Job ID from job name of specific pipeline
 
 
@@ -132,6 +132,7 @@ class Gitlab(object):
         :param pipeline_id: If None, will get pipeline id from CI pre-defined variable.
         :param pipeline_id: If None, will get pipeline id from CI pre-defined variable.
         :param job_status: status of job. One pipeline could have multiple jobs with same name after retry.
         :param job_status: status of job. One pipeline could have multiple jobs with same name after retry.
                            job_status is used to filter these jobs.
                            job_status is used to filter these jobs.
+        :param suffix: suffix of the job. e.g. 'limit' for build only needed apps.
         :return: a list of job IDs (parallel job will generate multiple jobs)
         :return: a list of job IDs (parallel job will generate multiple jobs)
         """
         """
         job_id_list = []
         job_id_list = []
@@ -144,6 +145,9 @@ class Gitlab(object):
             if match:
             if match:
                 if match.group(1) == job_name and job.status == job_status:
                 if match.group(1) == job_name and job.status == job_status:
                     job_id_list.append({"id": job.id, "parallel_num": match.group(3)})
                     job_id_list.append({"id": job.id, "parallel_num": match.group(3)})
+                elif suffix:
+                    if match.group(1) == "{}_{}".format(job_name, suffix) and job.status == job_status:
+                        job_id_list.append({"id": job.id, "parallel_num": match.group(3)})
         return job_id_list
         return job_id_list
 
 
     @retry_download
     @retry_download

+ 3 - 4
tools/ci/python_packages/tiny_test_fw/Utility/CIAssignTest.py

@@ -189,16 +189,15 @@ class AssignTest(object):
         job_list.sort(key=lambda x: x["name"])
         job_list.sort(key=lambda x: x["name"])
         return job_list
         return job_list
 
 
-    def _search_cases(self, test_case_path, case_filter=None, test_case_file_pattern=None):
+    def search_cases(self, case_filter=None):
         """
         """
-        :param test_case_path: path contains test case folder
         :param case_filter: filter for test cases. the filter to use is default filter updated with case_filter param.
         :param case_filter: filter for test cases. the filter to use is default filter updated with case_filter param.
         :return: filtered test case list
         :return: filtered test case list
         """
         """
         _case_filter = self.DEFAULT_FILTER.copy()
         _case_filter = self.DEFAULT_FILTER.copy()
         if case_filter:
         if case_filter:
             _case_filter.update(case_filter)
             _case_filter.update(case_filter)
-        test_methods = SearchCases.Search.search_test_cases(test_case_path, test_case_file_pattern)
+        test_methods = SearchCases.Search.search_test_cases(self.test_case_path, self.test_case_file_pattern)
         return CaseConfig.filter_test_cases(test_methods, _case_filter)
         return CaseConfig.filter_test_cases(test_methods, _case_filter)
 
 
     def _group_cases(self):
     def _group_cases(self):
@@ -287,7 +286,7 @@ class AssignTest(object):
         failed_to_assign = []
         failed_to_assign = []
         assigned_groups = []
         assigned_groups = []
         case_filter = self._apply_bot_filter()
         case_filter = self._apply_bot_filter()
-        self.test_cases = self._search_cases(self.test_case_path, case_filter, self.test_case_file_pattern)
+        self.test_cases = self.search_cases(case_filter)
         self._apply_bot_test_count()
         self._apply_bot_test_count()
         test_groups = self._group_cases()
         test_groups = self._group_cases()
 
 

+ 1 - 0
tools/ci/python_packages/tiny_test_fw/Utility/SearchCases.py

@@ -50,6 +50,7 @@ class Search(object):
 
 
         for i, test_function in enumerate(test_functions_out):
         for i, test_function in enumerate(test_functions_out):
             print("\t{}. ".format(i + 1) + test_function.case_info["name"])
             print("\t{}. ".format(i + 1) + test_function.case_info["name"])
+            test_function.case_info['app_dir'] = os.path.dirname(file_name)
         return test_functions_out
         return test_functions_out
 
 
     @classmethod
     @classmethod

+ 1 - 1
tools/ci/python_packages/ttfw_idf/CIAssignExampleTest.py

@@ -65,7 +65,7 @@ def create_artifact_index_file(project_id=None, pipeline_id=None, case_group=Exa
         return "{}/list_job_{}.json".format(case_group.BUILD_LOCAL_DIR, parallel or 1)
         return "{}/list_job_{}.json".format(case_group.BUILD_LOCAL_DIR, parallel or 1)
 
 
     for build_job_name in case_group.BUILD_JOB_NAMES:
     for build_job_name in case_group.BUILD_JOB_NAMES:
-        job_info_list = gitlab_inst.find_job_id(build_job_name, pipeline_id=pipeline_id)
+        job_info_list = gitlab_inst.find_job_id(build_job_name, pipeline_id=pipeline_id, suffix='limit')
         for job_info in job_info_list:
         for job_info in job_info_list:
             raw_data = gitlab_inst.download_artifact(job_info["id"], [format_build_log_path()])[0]
             raw_data = gitlab_inst.download_artifact(job_info["id"], [format_build_log_path()])[0]
             build_info_list = [json.loads(line) for line in raw_data.splitlines()]
             build_info_list = [json.loads(line) for line in raw_data.splitlines()]

+ 1 - 1
tools/ci/python_packages/ttfw_idf/CIAssignUnitTest.py

@@ -139,7 +139,7 @@ class UnitTestAssignTest(CIAssignTest.AssignTest):
     def __init__(self, test_case_path, ci_config_file):
     def __init__(self, test_case_path, ci_config_file):
         CIAssignTest.AssignTest.__init__(self, test_case_path, ci_config_file, case_group=Group)
         CIAssignTest.AssignTest.__init__(self, test_case_path, ci_config_file, case_group=Group)
 
 
-    def _search_cases(self, test_case_path, case_filter=None, test_case_file_pattern=None):
+    def search_cases(self, case_filter=None):
         """
         """
         For unit test case, we don't search for test functions.
         For unit test case, we don't search for test functions.
         The unit test cases is stored in a yaml file which is created in job build-idf-test.
         The unit test cases is stored in a yaml file which is created in job build-idf-test.

+ 134 - 0
tools/ci/python_packages/ttfw_idf/CIScanTests.py

@@ -0,0 +1,134 @@
+import argparse
+import json
+import os
+import re
+from collections import defaultdict
+
+from find_apps import find_apps
+from find_build_apps import CMakeBuildSystem
+from ttfw_idf.CIAssignExampleTest import CIExampleAssignTest, TestAppsGroup, ExampleGroup
+
+VALID_TARGETS = [
+    'esp32',
+    'esp32s2',
+]
+
+SPECIAL_REFS = [
+    'master',
+    re.compile(r'^release/v'),
+    re.compile(r'^v\d+\.\d+'),
+]
+
+
+def _judge_build_all():
+    ref = os.getenv('CI_COMMIT_REF_NAME')
+    pipeline_src = os.getenv('CI_PIPELINE_SOURCE')
+    if not ref or not pipeline_src:
+        return False
+
+    # scheduled pipeline will build all
+    if pipeline_src == 'schedule':
+        return True
+
+    # master, release/v..., v1.2.3..., and will build all
+    for special_ref in SPECIAL_REFS:
+        if isinstance(special_ref, re._pattern_type):
+            if special_ref.match(ref):
+                return True
+        else:
+            if ref == special_ref:
+                return True
+    return False
+
+
+def main():
+    parser = argparse.ArgumentParser(description='Scan the required build tests')
+    actions = parser.add_subparsers(dest='action')
+
+    common = argparse.ArgumentParser(add_help=False)
+    common.add_argument('paths', type=str, nargs='+',
+                        help="One or more app paths")
+    common.add_argument('-c', '--ci_config_file', type=str, required=True,
+                        help="gitlab ci config file")
+    common.add_argument('-o', '--output_path', type=str, required=True,
+                        help="output path of the scan result")
+    common.add_argument('-p', '--preserve-all', action="store_true",
+                        help='add this flag to preserve artifacts for all apps')
+
+    actions.add_parser('example_test', parents=[common])
+    actions.add_parser('test_apps', parents=[common])
+    # actions.add_parser('unit_test', parents=[common])
+
+    args = parser.parse_args()
+
+    test_cases = []
+    for path in args.paths:
+        if args.action == 'example_test':
+            assign = CIExampleAssignTest(path, args.ci_config_file, ExampleGroup)
+        elif args.action == 'test_apps':
+            CIExampleAssignTest.CI_TEST_JOB_PATTERN = re.compile(r'^test_app_test_.+')
+            assign = CIExampleAssignTest(path, args.ci_config_file, TestAppsGroup)
+        # elif args.action == 'unit_test':
+        #     assign = UnitTestAssignTest(args.test_case, args.ci_config_file)
+        else:
+            raise SystemExit(1)  # which is impossible
+
+        test_cases.extend(assign.search_cases())
+
+    try:
+        os.makedirs(args.output_path)
+    except Exception:
+        pass
+
+    '''
+    {
+        <target>: {
+            'test_case_apps': [<app_dir>],
+            'standalone_apps': [<app_dir>],
+        },
+        ...
+    }
+    '''
+    scan_info_dict = defaultdict(dict)
+    # store the test cases dir, exclude these folders when scan for standalone apps
+    exclude_apps = []
+
+    # TODO change this chip to target after feat/add_multi_target_for_example_test is merged
+    for target in VALID_TARGETS:
+        target_dict = scan_info_dict[target]
+        test_case_apps = target_dict['test_case_apps'] = set()
+        for case in test_cases:
+            app_dir = case.case_info['app_dir']
+            app_target = case.case_info['chip']
+            if app_target.lower() != target.lower():
+                continue
+            test_case_apps.update(find_apps(CMakeBuildSystem, app_dir, True, [], target.lower()))
+            exclude_apps.append(app_dir)
+
+    for target in VALID_TARGETS:
+        target_dict = scan_info_dict[target]
+        standalone_apps = target_dict['standalone_apps'] = set()
+        for path in args.paths:
+            standalone_apps.update(find_apps(CMakeBuildSystem, path, True, exclude_apps, target.lower()))
+
+    build_all = _judge_build_all()
+    for target in VALID_TARGETS:
+        apps = []
+        for app_dir in scan_info_dict[target]['test_case_apps']:
+            apps.append({
+                'app_dir': app_dir,
+                'build': True,
+                'preserve': True,
+            })
+        for app_dir in scan_info_dict[target]['standalone_apps']:
+            apps.append({
+                'app_dir': app_dir,
+                'build': build_all,
+                'preserve': args.preserve_all and build_all,  # you can't preserve the artifacts if you don't build them right?
+            })
+        with open(os.path.join(args.output_path, 'scan_{}.json'.format(target.lower())), 'w') as fw:
+            fw.writelines([json.dumps(app) + '\n' for app in apps])
+
+
+if __name__ == '__main__':
+    main()

+ 44 - 15
tools/find_apps.py

@@ -11,6 +11,8 @@ import re
 import glob
 import glob
 import logging
 import logging
 import typing
 import typing
+from typing import Optional
+
 from find_build_apps import (
 from find_build_apps import (
     BUILD_SYSTEMS,
     BUILD_SYSTEMS,
     BUILD_SYSTEM_CMAKE,
     BUILD_SYSTEM_CMAKE,
@@ -22,8 +24,8 @@ from find_build_apps import (
     DEFAULT_TARGET,
     DEFAULT_TARGET,
 )
 )
 
 
-# Helper functions
 
 
+# Helper functions
 
 
 def dict_from_sdkconfig(path):
 def dict_from_sdkconfig(path):
     """
     """
@@ -124,8 +126,8 @@ def find_builds_for_app(
     return build_items
     return build_items
 
 
 
 
-def find_apps(build_system_class, path, recursive, exclude_list,
-              target):  # type: (typing.Type[BuildSystem], str, bool, typing.List[str], str) -> typing.List[str]
+def find_apps(build_system_class, path, recursive, exclude_list, target,
+              build_apps_list_file=None):  # type: (typing.Type[BuildSystem], str, bool, typing.List[str], str, Optional[str]) -> typing.List[str]
     """
     """
     Find app directories in path (possibly recursively), which contain apps for the given build system, compatible
     Find app directories in path (possibly recursively), which contain apps for the given build system, compatible
     with the given target.
     with the given target.
@@ -134,6 +136,8 @@ def find_apps(build_system_class, path, recursive, exclude_list,
     :param recursive: whether to recursively descend into nested directories if no app is found
     :param recursive: whether to recursively descend into nested directories if no app is found
     :param exclude_list: list of paths to be excluded from the recursive search
     :param exclude_list: list of paths to be excluded from the recursive search
     :param target: desired value of IDF_TARGET; apps incompatible with the given target are skipped.
     :param target: desired value of IDF_TARGET; apps incompatible with the given target are skipped.
+    :param build_apps_list_file: List of apps need to be built, apps not on this list will be skipped.
+                                 None or empty file to build all apps.
     :return: list of paths of the apps found
     :return: list of paths of the apps found
     """
     """
     build_system_name = build_system_class.NAME
     build_system_name = build_system_class.NAME
@@ -147,6 +151,18 @@ def find_apps(build_system_class, path, recursive, exclude_list,
             return []
             return []
         return [path]
         return [path]
 
 
+    # if this argument is empty, treat it as build_all.
+    if not build_apps_list_file:
+        build_all = True
+    else:
+        # if this argument is not empty but the file doesn't exists, treat as no supported apps.
+        if not os.path.exists(build_apps_list_file):
+            return []
+        else:
+            build_all = False
+            with open(build_apps_list_file) as fr:
+                apps_need_be_built = set([line.strip() for line in fr.readlines() if line])
+
     # The remaining part is for recursive == True
     # The remaining part is for recursive == True
     apps_found = []  # type: typing.List[str]
     apps_found = []  # type: typing.List[str]
     for root, dirs, _ in os.walk(path, topdown=True):
     for root, dirs, _ in os.walk(path, topdown=True):
@@ -156,6 +172,11 @@ def find_apps(build_system_class, path, recursive, exclude_list,
             del dirs[:]
             del dirs[:]
             continue
             continue
 
 
+        if not build_all:
+            if os.path.abspath(root) not in apps_need_be_built:
+                logging.debug("Skipping, app not listed in {}".format(build_apps_list_file))
+                continue
+
         if build_system_class.is_app(root):
         if build_system_class.is_app(root):
             logging.debug("Found {} app in {}".format(build_system_name, root))
             logging.debug("Found {} app in {}".format(build_system_name, root))
             # Don't recurse into app subdirectories
             # Don't recurse into app subdirectories
@@ -193,22 +214,22 @@ def main():
     parser.add_argument(
     parser.add_argument(
         "--work-dir",
         "--work-dir",
         help="If set, the app is first copied into the specified directory, and then built." +
         help="If set, the app is first copied into the specified directory, and then built." +
-        "If not set, the work directory is the directory of the app.",
+             "If not set, the work directory is the directory of the app.",
     )
     )
     parser.add_argument(
     parser.add_argument(
         "--config",
         "--config",
         action="append",
         action="append",
         help="Adds configurations (sdkconfig file names) to build. This can either be " +
         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.",
+             "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(
     parser.add_argument(
         "--build-dir",
         "--build-dir",
         help="If set, specifies the build directory name. Can expand placeholders. Can be either a " +
         help="If set, specifies the build directory name. Can expand placeholders. Can be either a " +
-        "name relative to the work directory, or an absolute path.",
+             "name relative to the work directory, or an absolute path.",
     )
     )
     parser.add_argument(
     parser.add_argument(
         "--build-log",
         "--build-log",
@@ -232,6 +253,13 @@ def main():
         type=argparse.FileType("w"),
         type=argparse.FileType("w"),
         help="Output the list of builds to the specified file",
         help="Output the list of builds to the specified file",
     )
     )
+    parser.add_argument(
+        '-s',
+        '--scan-tests-json',
+        default=None,
+        help="Scan tests result. Restrict the build/architect behavior to apps need to be built.\n"
+             "If it's None or the file does not exist, will build all apps and upload all artifacts."
+    )
     parser.add_argument("paths", nargs="+", help="One or more app paths.")
     parser.add_argument("paths", nargs="+", help="One or more app paths.")
     args = parser.parse_args()
     args = parser.parse_args()
     setup_logging(args)
     setup_logging(args)
@@ -251,17 +279,17 @@ def main():
     # Prepare the list of app paths
     # Prepare the list of app paths
     app_paths = []  # type: typing.List[str]
     app_paths = []  # type: typing.List[str]
     for path in args.paths:
     for path in args.paths:
-        app_paths += find_apps(build_system_class, path, args.recursive, args.exclude or [], args.target)
+        app_paths += find_apps(build_system_class, path, args.recursive, args.exclude or [], args.target, args.build_apps_list_file)
 
 
+    build_items = []  # type: typing.List[BuildItem]
     if not app_paths:
     if not app_paths:
-        logging.critical("No {} apps found".format(build_system_class.NAME))
-        raise SystemExit(1)
-    logging.info("Found {} apps".format(len(app_paths)))
+        logging.warning("No {} apps found, skipping...".format(build_system_class.NAME))
+        sys.exit(0)
 
 
+    logging.info("Found {} apps".format(len(app_paths)))
     app_paths = sorted(app_paths)
     app_paths = sorted(app_paths)
 
 
     # Find compatible configurations of each app, collect them as BuildItems
     # Find compatible configurations of each app, collect them as BuildItems
-    build_items = []  # type: typing.List[BuildItem]
     config_rules = config_rules_from_str(args.config or [])
     config_rules = config_rules_from_str(args.config or [])
     for app_path in app_paths:
     for app_path in app_paths:
         build_items += find_builds_for_app(
         build_items += find_builds_for_app(
@@ -278,6 +306,7 @@ def main():
     # Write out the BuildItems. Only JSON supported now (will add YAML later).
     # Write out the BuildItems. Only JSON supported now (will add YAML later).
     if args.format != "json":
     if args.format != "json":
         raise NotImplementedError()
         raise NotImplementedError()
+
     out = args.output or sys.stdout
     out = args.output or sys.stdout
     out.writelines([item.to_json() + "\n" for item in build_items])
     out.writelines([item.to_json() + "\n" for item in build_items])
 
 

+ 2 - 0
tools/find_build_apps/__init__.py

@@ -6,6 +6,7 @@ from .common import (
     config_rules_from_str,
     config_rules_from_str,
     setup_logging,
     setup_logging,
     DEFAULT_TARGET,
     DEFAULT_TARGET,
+    safe_exit_if_file_is_empty,
 )
 )
 from .cmake import CMakeBuildSystem, BUILD_SYSTEM_CMAKE
 from .cmake import CMakeBuildSystem, BUILD_SYSTEM_CMAKE
 from .make import MakeBuildSystem, BUILD_SYSTEM_MAKE
 from .make import MakeBuildSystem, BUILD_SYSTEM_MAKE
@@ -28,4 +29,5 @@ __all__ = [
     "MakeBuildSystem",
     "MakeBuildSystem",
     "BUILD_SYSTEM_MAKE",
     "BUILD_SYSTEM_MAKE",
     "BUILD_SYSTEMS",
     "BUILD_SYSTEMS",
+    "safe_exit_if_file_is_empty",
 ]
 ]

+ 6 - 0
tools/find_build_apps/common.py

@@ -383,3 +383,9 @@ def setup_logging(args):
         stream=args.log_file or sys.stderr,
         stream=args.log_file or sys.stderr,
         level=log_level,
         level=log_level,
     )
     )
+
+
+def safe_exit_if_file_is_empty(file_name):
+    if os.stat(file_name).st_size == 0:
+        logging.warning('Skipping all...')
+        sys.exit(0)