From 8986ed7ab7b9fadc87150b0349fdacaedf0ce38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Csord=C3=A1s?= Date: Fri, 13 Mar 2020 10:39:07 +0100 Subject: [PATCH] [client] Store support configuration file --- docs/web/user_guide.md | 17 +++- web/client/codechecker_client/cmd/store.py | 53 ++++++++--- web/tests/functional/cli_config/__init__.py | 25 ++++- .../cli_config/test_store_config.py | 92 +++++++++++++++++++ 4 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 web/tests/functional/cli_config/test_store_config.py diff --git a/docs/web/user_guide.md b/docs/web/user_guide.md index eca657366e..e421519aee 100644 --- a/docs/web/user_guide.md +++ b/docs/web/user_guide.md @@ -162,8 +162,9 @@ a database. to the database. ``` -usage: CodeChecker store [-h] [-t {plist}] [-n NAME] [--tag TAG] [-f] - [--url PRODUCT_URL] +usage: CodeChecker store [-h] [-t {plist}] [-n NAME] [--tag TAG] + [--trim-path-prefix [TRIM_PATH_PREFIX [TRIM_PATH_PREFIX ...]]] + [--config CONFIG_FILE] [-f] [--url PRODUCT_URL] [--verbose {info,debug,debug_analyzer}] [file/folder [file/folder ...]] @@ -192,6 +193,18 @@ optional arguments: removing "/a/b/" prefix will store files like c/x.cpp and c/y.cpp. If multiple prefix is given, the longest match will be removed. + --config CONFIG_FILE Allow the configuration from an explicit JSON based + configuration file. The values configured in the + config file will overwrite the values set in the + command line. The format of configuration file is: + { + "enabled": true, + "store": [ + "--name=run_name", + "--tag=my_tag" + "--url=http://codechecker.my/MyProduct" + ] + }. (default: None) -f, --force Delete analysis results stored in the database for the current analysis run's name and store only the results reported in the 'input' files. (By default, diff --git a/web/client/codechecker_client/cmd/store.py b/web/client/codechecker_client/cmd/store.py index 8c098488d0..6c0adf89f3 100644 --- a/web/client/codechecker_client/cmd/store.py +++ b/web/client/codechecker_client/cmd/store.py @@ -25,9 +25,7 @@ from codechecker_client import client as libclient -from codechecker_common import logger -from codechecker_common import util -from codechecker_common import plist_parser +from codechecker_common import arg, logger, plist_parser, util from codechecker_common.output_formatters import twodim_to_str from codechecker_common.source_code_comment_handler import \ SourceCodeCommentHandler @@ -72,16 +70,18 @@ def get_argparser_ctor_args(): return { 'prog': 'CodeChecker store', - 'formatter_class': argparse.ArgumentDefaultsHelpFormatter, + 'formatter_class': arg.RawDescriptionDefaultHelpFormatter, # Description is shown when the command's help is queried directly - 'description': "Store the results from one or more 'codechecker-" - "analyze' result files in a database.", + 'description': """ +Store the results from one or more 'codechecker-analyze' result files in a +database.""", # Epilogue is shown after the arguments when the help is queried # directly. - 'epilog': "The results can be viewed by connecting to such a server " - "in a Web browser or via 'CodeChecker cmd'.", + 'epilog': """ +The results can be viewed by connecting to such a server in a Web browser or +via 'CodeChecker cmd'.""", # Help is shown when the "parent" CodeChecker command lists the # individual subcommands. @@ -143,6 +143,23 @@ def add_arguments_to_parser(parser): "If multiple prefix is given, the longest match " "will be removed.") + parser.add_argument('--config', + dest='config_file', + required=False, + help="R|Allow the configuration from an explicit JSON " + "based configuration file. The values configured " + "in the config file will overwrite the values " + "set in the command line. The format of " + "configuration file is:\n" + "{\n" + " \"enabled\": true,\n" + " \"store\": [\n" + " \"--name=run_name\",\n" + " \"--tag=my_tag\"\n" + " \"--url=http://codechecker.my/MyProduct\"\n" + " ]\n" + "}.") + parser.add_argument('-f', '--force', dest="force", default=argparse.SUPPRESS, @@ -158,9 +175,10 @@ def add_arguments_to_parser(parser): server_args = parser.add_argument_group( "server arguments", - "Specifies a 'CodeChecker server' instance which will be used to " - "store the results. This server must be running and listening, and " - "the given product must exist prior to the 'store' command being ran.") + """ +Specifies a 'CodeChecker server' instance which will be used to store the +results. This server must be running and listening, and the given product +must exist prior to the 'store' command being ran.""") server_args.add_argument('--url', type=str, @@ -173,7 +191,18 @@ def add_arguments_to_parser(parser): "'[http[s]://]host:port/Endpoint'.") logger.add_verbose_arguments(parser) - parser.set_defaults(func=main) + parser.set_defaults(func=main, + func_process_config_file=process_config_file) + + +def process_config_file(args): + """ + Handler to get config file options. + """ + if args.config_file and os.path.exists(args.config_file): + cfg = util.load_json_or_empty(args.config_file, default={}) + if cfg.get("enabled"): + return cfg.get('store', []) def __get_run_name(input_list): diff --git a/web/tests/functional/cli_config/__init__.py b/web/tests/functional/cli_config/__init__.py index 5246e2ca80..d0bc9e71f9 100644 --- a/web/tests/functional/cli_config/__init__.py +++ b/web/tests/functional/cli_config/__init__.py @@ -10,8 +10,11 @@ import os import shutil +import sys +from libtest import codechecker from libtest import env +from libtest import project # Test workspace should be initialized in this module. @@ -33,11 +36,23 @@ def setup_package(): codechecker_cfg = { 'workspace': TEST_WORKSPACE, 'check_env': env.test_env(TEST_WORKSPACE), - 'viewer_host': 'localhost', - 'viewer_product': 'db_cleanup' + 'reportdir': os.path.join(TEST_WORKSPACE, 'reports') } - env.export_test_cfg(TEST_WORKSPACE, {'codechecker_cfg': codechecker_cfg}) + # Start or connect to the running CodeChecker server and get connection + # details. + print("This test uses a CodeChecker server... connecting...") + server_access = codechecker.start_or_get_server() + server_access['viewer_product'] = 'config' + codechecker.add_test_package_product(server_access, TEST_WORKSPACE) + + # Extend the checker configuration with the server access. + codechecker_cfg.update(server_access) + + test_config = { + 'codechecker_cfg': codechecker_cfg} + + env.export_test_cfg(TEST_WORKSPACE, test_config) def teardown_package(): @@ -47,5 +62,9 @@ def teardown_package(): # and print out the path. global TEST_WORKSPACE + check_env = env.import_test_cfg(TEST_WORKSPACE)[ + 'codechecker_cfg']['check_env'] + codechecker.remove_test_package_product(TEST_WORKSPACE, check_env) + print("Removing: " + TEST_WORKSPACE) shutil.rmtree(TEST_WORKSPACE) diff --git a/web/tests/functional/cli_config/test_store_config.py b/web/tests/functional/cli_config/test_store_config.py new file mode 100644 index 0000000000..27ca0e9da7 --- /dev/null +++ b/web/tests/functional/cli_config/test_store_config.py @@ -0,0 +1,92 @@ +# +# ----------------------------------------------------------------------------- +# The CodeChecker Infrastructure +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. +# ----------------------------------------------------------------------------- + +""" +Test store configuration file. +""" + + +import json +import os +import subprocess +import unittest + +from libtest import env + + +class TestStoreConfig(unittest.TestCase): + _ccClient = None + + def setUp(self): + + # TEST_WORKSPACE is automatically set by test package __init__.py . + self.test_workspace = os.environ['TEST_WORKSPACE'] + self.codechecker_cfg = env.import_codechecker_cfg(self.test_workspace) + + test_class = self.__class__.__name__ + print('Running ' + test_class + ' tests in ' + self.test_workspace) + + # Get the CodeChecker cmd if needed for the tests. + self._codechecker_cmd = env.codechecker_cmd() + + self.config_file = os.path.join(self.test_workspace, + "codechecker.json") + + # Create an empty report directory which will be used to check store + # command. + if not os.path.exists(self.codechecker_cfg['reportdir']): + os.mkdir(self.codechecker_cfg['reportdir']) + + def test_valid_config(self): + """ Store with a valid configuration file. """ + with open(self.config_file, 'w+', + encoding="utf-8", errors="ignore") as config_f: + json.dump({ + 'enabled': True, + 'store': [ + '--name=' + 'store_config', + '--url=' + env.parts_to_url(self.codechecker_cfg), + self.codechecker_cfg['reportdir']]}, config_f) + + store_cmd = [env.codechecker_cmd(), 'store', '--config', + self.config_file] + + subprocess.check_output( + store_cmd, encoding="utf-8", errors="ignore") + + def test_invalid_config(self): + """ Store with an invalid configuration file. """ + with open(self.config_file, 'w+', + encoding="utf-8", errors="ignore") as config_f: + json.dump({ + 'enabled': True, + 'store': ['--dummy-option']}, config_f) + + store_cmd = [env.codechecker_cmd(), 'store', + '--name', 'store_config', + '--config', self.config_file, + '--url', env.parts_to_url(self.codechecker_cfg), + self.codechecker_cfg['reportdir']] + + with self.assertRaises(subprocess.CalledProcessError): + subprocess.check_output( + store_cmd, encoding="utf-8", errors="ignore") + + def test_empty_config(self): + """ Store with an empty configuration file. """ + with open(self.config_file, 'w+', + encoding="utf-8", errors="ignore") as config_f: + config_f.write("") + + store_cmd = [env.codechecker_cmd(), 'store', + '--name', 'store_config', + '--config', self.config_file, + '--url', env.parts_to_url(self.codechecker_cfg), + self.codechecker_cfg['reportdir']] + + subprocess.check_output( + store_cmd, encoding="utf-8", errors="ignore")