From 35fd4ad7d973a88bd8ec68e181672171ce54f72b Mon Sep 17 00:00:00 2001 From: Przemek Wirkus Date: Mon, 18 Aug 2014 15:33:24 +0100 Subject: [PATCH] Database: added new factory and verbose database detection functions Added separate file to store test suite DB access interface Added new functionality to database interface: database name, connection status, hostname + uname functionality Added option --db to singletest.py so users can define database connection url for database access Added database configuration check with switch --config. Now users can combine switch --db and --config to check database conenctivity --- workspace_tools/singletest.py | 32 ++++----- workspace_tools/test_api.py | 132 ++++++++++------------------------ workspace_tools/test_db.py | 130 +++++++++++++++++++++++++++++++++ workspace_tools/test_mysql.py | 27 +++++-- 4 files changed, 206 insertions(+), 115 deletions(-) create mode 100644 workspace_tools/test_db.py diff --git a/workspace_tools/singletest.py b/workspace_tools/singletest.py index 3301f56901..244ab1f6d4 100644 --- a/workspace_tools/singletest.py +++ b/workspace_tools/singletest.py @@ -17,18 +17,10 @@ See the License for the specific language governing permissions and limitations under the License. Author: Przemyslaw Wirkus +""" -------------------------------------------------------------------------------- - -Call: - singletest.py --help - -to get help information. - -------------------------------------------------------------------------------- - -File format example: test_spec.json - +""" +File format example: test_spec.json: { "targets": { "KL46Z": ["ARM", "GCC_ARM"], @@ -38,8 +30,7 @@ File format example: test_spec.json } } -File format example: muts_all.json - +File format example: muts_all.json: { "1" : {"mcu": "LPC1768", "port":"COM4", @@ -53,9 +44,9 @@ File format example: muts_all.json "peripherals": ["digital_loop", "port_loop", "analog_loop"] } } - """ + # Check if 'prettytable' module is installed try: from prettytable import PrettyTable @@ -81,13 +72,16 @@ sys.path.insert(0, ROOT) from workspace_tools.build_api import mcu_toolchain_matrix # Imports from TEST API +from workspace_tools.test_api import BaseDBAccess from workspace_tools.test_api import SingleTestRunner +from workspace_tools.test_api import factory_db_logger from workspace_tools.test_api import singletest_in_cli_mode +from workspace_tools.test_api import detect_database_verbose from workspace_tools.test_api import get_json_data_from_file -from workspace_tools.test_api import print_muts_configuration_from_json -from workspace_tools.test_api import print_test_configuration_from_json from workspace_tools.test_api import get_avail_tests_summary_table from workspace_tools.test_api import get_default_test_options_parser +from workspace_tools.test_api import print_muts_configuration_from_json +from workspace_tools.test_api import print_test_configuration_from_json def get_version(): @@ -119,6 +113,10 @@ if __name__ == '__main__': print get_avail_tests_summary_table() exit(0) + if opts.db_url and opts.verbose_test_configuration_only: + detect_database_verbose(opts.db_url) + exit(0) + # Print summary / information about automation test status if opts.test_case_report: test_case_report_cols = ['id', 'automated', 'description', 'peripherals', 'host_test', 'duration', 'source_dir'] @@ -156,6 +154,7 @@ if __name__ == '__main__': print print_test_configuration_from_json(test_spec) exit(0) + # Verbose test specification and MUTs configuration if MUTs and opts.verbose: print print_muts_configuration_from_json(MUTs) @@ -169,6 +168,7 @@ if __name__ == '__main__': single_test = SingleTestRunner(_global_loops_count=opts.test_global_loops_value, _test_loops_list=opts.test_loops_list, _muts=MUTs, + _opts_db_url=opts.db_url, _opts_log_file_name=opts.log_file_name, _test_spec=test_spec, _opts_goanna_for_mbed_sdk=opts.goanna_for_mbed_sdk, diff --git a/workspace_tools/test_api.py b/workspace_tools/test_api.py index 6751ecb7e0..eaf5b49282 100644 --- a/workspace_tools/test_api.py +++ b/workspace_tools/test_api.py @@ -45,6 +45,8 @@ from workspace_tools.targets import TARGET_MAP from workspace_tools.build_api import build_project, build_mbed_libs, build_lib from workspace_tools.build_api import get_target_supported_toolchains from workspace_tools.libraries import LIBRARIES, LIBRARY_MAP +from workspace_tools.test_db import BaseDBAccess +from workspace_tools.test_mysql import MySQLDBAccess class ProcessObserver(Thread): @@ -130,6 +132,7 @@ class SingleTestRunner(object): _global_loops_count=1, _test_loops_list=None, _muts={}, + _opts_db_url=None, _opts_log_file_name=None, _test_spec={}, _opts_goanna_for_mbed_sdk=None, @@ -173,6 +176,7 @@ class SingleTestRunner(object): self.test_spec = _test_spec # Settings passed e.g. from command line + self.opts_db_url = _opts_db_url self.opts_log_file_name = _opts_log_file_name self.opts_goanna_for_mbed_sdk = _opts_goanna_for_mbed_sdk self.opts_goanna_for_tests = _opts_goanna_for_tests @@ -194,6 +198,7 @@ class SingleTestRunner(object): self.opts_extend_test_timeout = _opts_extend_test_timeout self.logger = CLITestLogger(file_name=self.opts_log_file_name) # Default test logger + self.db_logger = factory_db_logger(self.opts_db_url) def shuffle_random_func(self): return self.shuffle_random_seed @@ -1165,103 +1170,36 @@ class CLITestLogger(TestLogger): return log_line_str -class BaseDBAccess(): - """ Class used to connect with test database and store test results - """ - def __init__(self): - self.db_object = None - # Test Suite DB scheme (table names) - self.TABLE_BUILD_ID = 'mtest_build_id' - self.TABLE_BUILD_ID_STATUS = 'mtest_build_id_status' - self.TABLE_TARGET = 'mtest_target' - self.TABLE_TEST_ENTRY = 'mtest_test_entry' - self.TABLE_TEST_ID = 'mtest_test_id' - self.TABLE_TEST_RESULT = 'mtest_test_result' - self.TABLE_TEST_TYPE = 'mtest_test_type' - self.TABLE_TOOLCHAIN = 'mtest_toolchain' - # Build ID status PKs - self.BUILD_ID_STATUS_STARTED = 1 # Started - self.BUILD_ID_STATUS_IN_PROGRESS = 2 # In Progress - self.BUILD_ID_STATUS_COMPLETED = 3 #Completed - self.BUILD_ID_STATUS_FAILED = 4 # Failed +def factory_db_logger(db_url): + (db_type, username, password, host, db_name) = BaseDBAccess().parse_db_connection_string(db_url) + if db_type == 'mysql': + return MySQLDBAccess() + else: + return None - def get_hostname(self): - """ Useful when creating build_id in database - Function returns (hostname, uname) which can be used as (build_id_name, build_id_desc) - """ - # Get hostname from socket - import socket - hostname = socket.gethostbyaddr(socket.gethostname())[0] - # Get uname from platform resources - import platform - uname = json.dumps(platform.uname()) - return (hostname, uname) - def parse_db_connection_string(self, str): - """ Parsing SQL DB connection string. String should contain: - - DB Name, user name, password, URL (DB host), name - Function should return tuple with parsed (host, user, passwd, db) or None if error - E.g. connection string: 'mysql://username:password@127.0.0.1/db_name' - """ - PATTERN = '^([\w]+)://([\w]+):([\w]*)@(.*)/([\w]+)' - result = re.match(PATTERN, str) - if result is not None: - result = result.groups() # Tuple (db_name, host, user, passwd, db) - return result - - def is_connected(self): - """ Returns True if we are connected to database - """ - pass - - def connect(self, host, user, passwd, db): - """ Connects to DB and returns DB object - """ - pass - - def disconnect(self): - """ Close DB connection - """ - pass - - def escape_string(self, str): - """ Escapes string so it can be put in SQL query between quotes - """ - pass - - def select_all(self, query): - """ Execute SELECT query and get all results - """ - pass - - def insert(self, query, commit=True): - """ Execute INSERT query, define if you want to commit - """ - pass - - def get_next_build_id(self, name, desc=''): - """ Insert new build_id (DB unique build like ID number to send all test results) - """ - pass - - def get_table_entry_pk(self, table, column, value, update_db=True): - """ Checks for entries in tables with two columns (_pk, ) - If update_db is True updates table entry if value in specified column doesn't exist - """ - pass - - def update_table_entry(self, table, column, value): - """ Updates table entry if value in specified column doesn't exist - Locks table to perform atomic read + update - """ - pass - - def insert_test_entry(self, next_build_id, target, toolchain, test_type, test_id, test_result, test_time, test_timeout, test_loop, test_extra=''): - """ Inserts test result entry to database. All checks regarding existing - toolchain names in DB are performed. - If some data is missing DB will be updated - """ - pass +def detect_database_verbose(db_url): + result = BaseDBAccess().parse_db_connection_string(db_url) + if result is not None: + # Parsing passed + (db_type, username, password, host, db_name) = result + #print "DB type '%s', user name '%s', password '%s', host '%s', db name '%s'"% result + # Let's try to connect + db_ = factory_db_logger(db_url) + if db_ is not None: + print "Connecting to database '%s'..."% db_url, + db_.connect(host, username, password, db_name) + if db_.is_connected(): + print "ok" + print "Detecting database..." + print db_.detect_database(verbose=True) + print "Disconnecting...", + db_.disconnect() + print "done" + else: + print "Database type '%s' unknown"% db_type + else: + print "Parse error: '%s' - DB Url error"% (db_url) def get_default_test_options_parser(): @@ -1397,6 +1335,10 @@ def get_default_test_options_parser(): type="int", help='You can increase global timeout for each test by specifying additional test timeout in seconds') + parser.add_option('', '--db', + dest='db_url', + help='This specifies what database test suite uses to store its state. To pass DB connection info use database connection string. Example: \'mysql://username:password@127.0.0.1/db_name\'') + parser.add_option('-l', '--log', dest='log_file_name', help='Log events to external file (note not all console entries may be visible in log file)') diff --git a/workspace_tools/test_db.py b/workspace_tools/test_db.py new file mode 100644 index 0000000000..3ae4aac388 --- /dev/null +++ b/workspace_tools/test_db.py @@ -0,0 +1,130 @@ +""" +mbed SDK +Copyright (c) 2011-2014 ARM Limited + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Author: Przemyslaw Wirkus +""" + +import re + + +class BaseDBAccess(): + """ Class used to connect with test database and store test results + """ + def __init__(self): + self.db_object = None + self.db_type = None + # Test Suite DB scheme (table names) + self.TABLE_BUILD_ID = 'mtest_build_id' + self.TABLE_BUILD_ID_STATUS = 'mtest_build_id_status' + self.TABLE_TARGET = 'mtest_target' + self.TABLE_TEST_ENTRY = 'mtest_test_entry' + self.TABLE_TEST_ID = 'mtest_test_id' + self.TABLE_TEST_RESULT = 'mtest_test_result' + self.TABLE_TEST_TYPE = 'mtest_test_type' + self.TABLE_TOOLCHAIN = 'mtest_toolchain' + # Build ID status PKs + self.BUILD_ID_STATUS_STARTED = 1 # Started + self.BUILD_ID_STATUS_IN_PROGRESS = 2 # In Progress + self.BUILD_ID_STATUS_COMPLETED = 3 #Completed + self.BUILD_ID_STATUS_FAILED = 4 # Failed + + def get_hostname(self): + """ Useful when creating build_id in database + Function returns (hostname, uname) which can be used as (build_id_name, build_id_desc) + """ + # Get hostname from socket + import socket + hostname = socket.gethostbyaddr(socket.gethostname())[0] + # Get uname from platform resources + import platform + uname = json.dumps(platform.uname()) + return (hostname, uname) + + def get_db_type(self): + """ Returns database type. E.g. 'mysql', 'sqlLite' etc. + """ + return self.db_type + + def detect_database(self, verbose=False): + """ detect database and return VERION data structure or string (verbose=True) + """ + return None + + def parse_db_connection_string(self, str): + """ Parsing SQL DB connection string. String should contain: + - DB Name, user name, password, URL (DB host), name + Function should return tuple with parsed (db_type, username, password, host, db_name) or None if error + E.g. connection string: 'mysql://username:password@127.0.0.1/db_name' + """ + PATTERN = '^([\w]+)://([\w]+):([\w]*)@(.*)/([\w]+)' + result = re.match(PATTERN, str) + if result is not None: + result = result.groups() # Tuple (db_name, host, user, passwd, db) + return result # (db_type, username, password, host, db_name) + + def is_connected(self): + """ Returns True if we are connected to database + """ + pass + + def connect(self, host, user, passwd, db): + """ Connects to DB and returns DB object + """ + pass + + def disconnect(self): + """ Close DB connection + """ + pass + + def escape_string(self, str): + """ Escapes string so it can be put in SQL query between quotes + """ + pass + + def select_all(self, query): + """ Execute SELECT query and get all results + """ + pass + + def insert(self, query, commit=True): + """ Execute INSERT query, define if you want to commit + """ + pass + + def get_next_build_id(self, name, desc=''): + """ Insert new build_id (DB unique build like ID number to send all test results) + """ + pass + + def get_table_entry_pk(self, table, column, value, update_db=True): + """ Checks for entries in tables with two columns (_pk, ) + If update_db is True updates table entry if value in specified column doesn't exist + """ + pass + + def update_table_entry(self, table, column, value): + """ Updates table entry if value in specified column doesn't exist + Locks table to perform atomic read + update + """ + pass + + def insert_test_entry(self, next_build_id, target, toolchain, test_type, test_id, test_result, test_time, test_timeout, test_loop, test_extra=''): + """ Inserts test result entry to database. All checks regarding existing + toolchain names in DB are performed. + If some data is missing DB will be updated + """ + pass diff --git a/workspace_tools/test_mysql.py b/workspace_tools/test_mysql.py index 93cde7fa6b..4a4ebd25af 100644 --- a/workspace_tools/test_mysql.py +++ b/workspace_tools/test_mysql.py @@ -20,9 +20,8 @@ Author: Przemyslaw Wirkus import re import MySQLdb as mdb - # Imports from TEST API -from workspace_tools.test_api import BaseDBAccess +from workspace_tools.test_db import BaseDBAccess class MySQLDBAccess(BaseDBAccess): @@ -31,6 +30,20 @@ class MySQLDBAccess(BaseDBAccess): def __init__(self): BaseDBAccess.__init__(self) + def detect_database(self, verbose=False): + """ detect database and return VERION data structure or string (verbose=True) + """ + query = 'SHOW VARIABLES LIKE "%version%"' + rows = self.select_all(query) + if verbose: + result = [] + for row in rows: + result.append("\t%s: %s"% (row['Variable_name'], row['Value'])) + result = "\n".join(result) + else: + result = rows + return result + def parse_db_connection_string(self, str): """ Parsing SQL DB connection string. String should contain: - DB Name, user name, password, URL (DB host), name @@ -38,8 +51,10 @@ class MySQLDBAccess(BaseDBAccess): E.g. connection string: 'mysql://username:password@127.0.0.1/db_name' """ result = BaseDBAccess.parse_db_connection_string(str) - if result is not None and result[0] != 'mysql': - result = None + if result is not None: + (db_type, username, password, host, db_name) = result + if db_type != 'mysql': + result = None return result def is_connected(self): @@ -52,15 +67,19 @@ class MySQLDBAccess(BaseDBAccess): """ try: self.db_object = mdb.connect(host=host, user=user, passwd=passwd, db=db) + self.db_type = 'mysql' except mdb.Error, e: print "Error %d: %s"% (e.args[0], e.args[1]) self.db_object = None + self.db_type = None def disconnect(self): """ Close DB connection """ if self.db_object: self.db_object.close() + self.db_object = None + self.db_type = None def escape_string(self, str): """ Escapes string so it can be put in SQL query between quotes