Merge "Adding a structure for the reporting of test cases for functest"
authorMorgan Richomme <morgan.richomme@orange.com>
Thu, 11 Feb 2016 16:08:11 +0000 (16:08 +0000)
committerGerrit Code Review <gerrit@172.30.200.206>
Thu, 11 Feb 2016 16:08:11 +0000 (16:08 +0000)
12 files changed:
docker/run_tests.sh
docs/userguide/runfunctest.rst
testcases/Controllers/ODL/CI/odlreport2db.py
testcases/Controllers/ONOS/Teston/CI/onosfunctest.py
testcases/VIM/OpenStack/CI/libraries/run_rally-cert.py
testcases/VIM/OpenStack/CI/libraries/run_rally.py
testcases/VIM/OpenStack/CI/libraries/run_tempest.py
testcases/features/doctor.py
testcases/functest_utils.py
testcases/vIMS/CI/vIMS.py
testcases/vPing/CI/libraries/vPing_ssh.py
testcases/vPing/CI/libraries/vPing_userdata.py

index 7dc7f05..0f9e8a3 100755 (executable)
@@ -22,6 +22,7 @@ where:
     -h|--help         show this help text
     -r|--report       push results to database (false by default)
     -n|--no-clean     do not clean OpenStack resources after test run
+    -s|--serial       run tests in one thread
     -t|--test         run specific set of tests
       <test_name>     one or more of the following separated by comma:
                             vping_ssh,vping_userdata,odl,rally,tempest,vims,onos,promise,ovno
@@ -38,6 +39,8 @@ examples:
 offline=false
 report=""
 clean=true
+serial=false
+
 # Get the list of runnable tests
 # Check if we are in CI mode
 
@@ -82,16 +85,25 @@ function run_test(){
     echo "  Running test case: $i"
     echo "----------------------------------------------"
     echo ""
+    clean_flag=""
+    if [ $clean == "false" ]; then
+        clean_flag="-n"
+    fi
+    serial_flag=""
+    if [ $serial == "true" ]; then
+        serial_flag="-s"
+    fi
+
     case $test_name in
         "vping_ssh")
             info "Running vPing-SSH test..."
             python ${FUNCTEST_REPO_DIR}/testcases/vPing/CI/libraries/vPing_ssh.py \
-                --debug ${report}
+                --debug $clean_flag ${report}
         ;;
         "vping_userdata")
             info "Running vPing-userdata test... "
             python ${FUNCTEST_REPO_DIR}/testcases/vPing/CI/libraries/vPing_userdata.py \
-                --debug ${report}
+                --debug $clean_flag ${report}
         ;;
         "odl")
             info "Running ODL test..."
@@ -110,7 +122,7 @@ function run_test(){
         "tempest")
             info "Running Tempest tests..."
             python ${FUNCTEST_REPO_DIR}/testcases/VIM/OpenStack/CI/libraries/run_tempest.py \
-                --debug -m custom ${report}
+                --debug $serial_flag $clean_flag -m custom ${report}
             # save tempest.conf for further troubleshooting
             tempest_conf="${RALLY_VENV_DIR}/tempest/for-deployment-*/tempest.conf"
             if [ -f ${tempest_conf} ]; then
@@ -121,13 +133,13 @@ function run_test(){
         "vims")
             info "Running vIMS test..."
             python ${FUNCTEST_REPO_DIR}/testcases/vIMS/CI/vIMS.py \
-                --debug ${report}
+                --debug $clean_flag ${report}
             clean_openstack
         ;;
         "rally")
             info "Running Rally benchmark suite..."
             python ${FUNCTEST_REPO_DIR}/testcases/VIM/OpenStack/CI/libraries/run_rally-cert.py \
-                --debug all ${report}
+                --debug $clean_flag all ${report}
             clean_openstack
 
         ;;
@@ -208,6 +220,9 @@ while [[ $# > 0 ]]
         -n|--no-clean)
             clean=false
         ;;
+        -s|--serial)
+            serial=true
+        ;;
         -t|--test|--tests)
             TEST="$2"
             shift
index 6151141..3643337 100644 (file)
@@ -26,6 +26,7 @@ several options::
         -h|--help         show this help text
         -r|--report       push results to database (false by default)
         -n|--no-clean     do not clean up OpenStack resources after test run
+        -s|--serial       run tests in one thread
         -t|--test         run specific set of tests
           <test_name>     one or more of the following separated by comma:
                              vping_ssh,vping_userdata,odl,rally,tempest,vims,onos,promise,ovno
@@ -58,6 +59,10 @@ is called once by *prepare_env.sh* when setting up the Functest environment
 to snapshot all the OpenStack resources (images, networks, volumes, security groups,
 tenants, users) so that an eventual cleanup does not remove any of this defaults.
 
+The *-s* option forces execution of test cases in a single thread. Currently this
+option affects Tempest test cases only and can be used e.g. for troubleshooting
+concurrency problems.
+
 The script
 *$repos_dir/functest/testcases/VIM/OpenStack/CI/libraries/clean_openstack.py*
 is normally called after a test execution if the *-n* is not specified. It
index 1538f79..4706796 100644 (file)
@@ -130,6 +130,7 @@ def main(argv):
         #                        -p opnfv-jump-2
         #                        -s os-odl_l2-ha
         functest_utils.push_results_to_db(database,
+                                          "functest",
                                           data['case_name'],
                                           None,
                                           data['pod_name'],
index bf031cb..dc45088 100644 (file)
@@ -182,6 +182,7 @@ def main():
         pod_name = functest_utils.get_pod_name(logger)
         result = GetResult()
         functest_utils.push_results_to_db(TEST_DB,
+                                          "functest",
                                           "ONOS",
                                           logger, pod_name, scenario,
                                           payload=result)
index e187004..0d19926 100755 (executable)
@@ -54,6 +54,9 @@ parser.add_argument("-s", "--smoke",
 parser.add_argument("-v", "--verbose",
                     help="Print verbose info about the progress",
                     action="store_true")
+parser.add_argument("-n", "--noclean",
+                    help="Don't clean the created resources for this test.",
+                    action="store_true")
 
 args = parser.parse_args()
 
@@ -428,8 +431,10 @@ def main():
         report += ""\
         "| " + name + " | " + duration + " | " + nb_tests + " | " + success + "|\n"\
         "+-------------------+------------+---------------+-----------+\n"
-        payload.append({'module': name, 'duration': duration,
-                         'nb tests': nb_tests, 'success': success})
+        payload.append({'module': name,
+                        'details': {'duration': s['overall_duration'],
+                                    'nb tests': s['nb_tests'],
+                                    'success': s['success']}})
 
     total_duration_str = time.strftime("%H:%M:%S", time.gmtime(total_duration))
     total_duration_str2 = "{0:<10}".format(total_duration_str)
@@ -442,9 +447,9 @@ def main():
     report += "+===================+============+===============+===========+\n"
 
     logger.info("\n"+report)
-    payload.append({'summary': {'duration': total_duration_str2,
-                               'nb tests': total_nb_tests_str,
-                               'nb success': total_success_str}})
+    payload.append({'summary': {'duration': total_duration,
+                               'nb tests': total_nb_tests,
+                               'nb success': total_success}})
 
     # Generate json results for DB
     #json_results = {"timestart": time_start, "duration": total_duration,
@@ -455,6 +460,9 @@ def main():
         logger.debug("Pushing Rally summary into DB...")
         push_results_to_db("Rally", payload)
 
+    if args.noclean:
+        exit(0)
+
     logger.debug("Deleting image '%s' with ID '%s'..." \
                          % (GLANCE_IMAGE_NAME, image_id))
     if not functest_utils.delete_glance_image(nova_client, image_id):
index 18f60ac..6b1aae2 100755 (executable)
@@ -47,6 +47,9 @@ parser.add_argument("-r", "--report",
 parser.add_argument("-v", "--verbose",
                     help="Print verbose info about the progress",
                     action="store_true")
+parser.add_argument("-n", "--noclean",
+                    help="Don't clean the created resources for this test.",
+                    action="store_true")
 
 args = parser.parse_args()
 
@@ -271,6 +274,9 @@ def main():
         print(args.test_name)
         run_task(args.test_name)
 
+    if args.noclean:
+        exit(0)
+
     logger.debug("Deleting image '%s' with ID '%s'..." \
                          % (GLANCE_IMAGE_NAME, image_id))
     if not functest_utils.delete_glance_image(nova_client, image_id):
index b8ed271..2946691 100644 (file)
@@ -33,12 +33,21 @@ modes = ['full', 'smoke', 'baremetal', 'compute', 'data_processing',
 
 """ tests configuration """
 parser = argparse.ArgumentParser()
-parser.add_argument("-d", "--debug", help="Debug mode",  action="store_true")
-parser.add_argument("-m", "--mode", help="Tempest test mode [smoke, all]",
+parser.add_argument("-d", "--debug",
+                    help="Debug mode",
+                    action="store_true")
+parser.add_argument("-s", "--serial",
+                    help="Run tests in one thread",
+                    action="store_true")
+parser.add_argument("-m", "--mode",
+                    help="Tempest test mode [smoke, all]",
                     default="smoke")
 parser.add_argument("-r", "--report",
                     help="Create json result file",
                     action="store_true")
+parser.add_argument("-n", "--noclean",
+                    help="Don't clean the created resources for this test.",
+                    action="store_true")
 
 args = parser.parse_args()
 
@@ -289,12 +298,19 @@ def main():
     else:
         MODE = "--set "+args.mode
 
+    if args.serial:
+        MODE = "--concur 1 "+MODE
+
     if not os.path.exists(TEMPEST_RESULTS_DIR):
         os.makedirs(TEMPEST_RESULTS_DIR)
 
     create_tempest_resources()
     configure_tempest()
     run_tempest(MODE)
+
+    if args.noclean:
+        exit(0)
+
     free_tempest_resources()
 
 
index 8eb85a8..5669a99 100644 (file)
@@ -71,7 +71,7 @@ def main():
                     'd': details,
                 })
     functest_utils.push_results_to_db(TEST_DB_URL,
-                                      'doctor-notification',
+                                      'doctor','doctor-notification',
                                       logger, pod_name, scenario,
                                       details)
 
index 57ec186..94a4fa8 100644 (file)
@@ -18,7 +18,6 @@ import socket
 import subprocess
 import sys
 import urllib2
-import yaml
 from git import Repo
 
 
@@ -39,6 +38,7 @@ def check_credentials():
     env_vars = ['OS_AUTH_URL', 'OS_USERNAME', 'OS_PASSWORD', 'OS_TENANT_NAME']
     return all(map(lambda v: v in os.environ and os.environ[v], env_vars))
 
+
 def get_credentials(service):
     """Returns a creds dictionary filled with the following keys:
     * username
@@ -70,7 +70,6 @@ def get_credentials(service):
     return creds
 
 
-
 #*********************************************
 #   NOVA
 #*********************************************
@@ -134,10 +133,10 @@ def get_floating_ips(nova_client):
 
 def create_flavor(nova_client, flavor_name, ram, disk, vcpus):
     try:
-        flavor = nova_client.flavors.create(flavor_name,ram,vcpus,disk)
+        flavor = nova_client.flavors.create(flavor_name, ram, vcpus, disk)
     except Exception, e:
         print "Error [create_flavor(nova_client, '%s', '%s', '%s', "\
-            "'%s')]:" %(flavor_name,ram, disk, vcpus), e
+            "'%s')]:" % (flavor_name, ram, disk, vcpus), e
         return None
     return flavor.id
 
@@ -156,7 +155,7 @@ def create_floating_ip(neutron_client):
 
 def add_floating_ip(nova_client, server_id, floatingip_id):
     try:
-        nova_client.servers.add_floating_ip(server_id,floatingip_id)
+        nova_client.servers.add_floating_ip(server_id, floatingip_id)
         return True
     except Exception, e:
         print "Error [add_floating_ip(nova_client, '%s', '%s')]:" % \
@@ -182,8 +181,6 @@ def delete_floating_ip(nova_client, floatingip_id):
         return False
 
 
-
-
 #*********************************************
 #   NEUTRON
 #*********************************************
@@ -274,7 +271,7 @@ def create_neutron_subnet(neutron_client, name, cidr, net_id):
         return subnet['subnets'][0]['id']
     except Exception, e:
         print "Error [create_neutron_subnet(neutron_client, '%s', '%s', "\
-            "'%s')]:" %(name,cidr, net_id), e
+            "'%s')]:" % (name, cidr, net_id), e
         return False
 
 
@@ -300,7 +297,7 @@ def create_neutron_port(neutron_client, name, network_id, ip):
         return port['port']['id']
     except Exception, e:
         print "Error [create_neutron_port(neutron_client, '%s', '%s', "\
-            "'%s')]:" %(name,network_id, ip), e
+            "'%s')]:" % (name, network_id, ip), e
         return False
 
 
@@ -311,7 +308,7 @@ def update_neutron_net(neutron_client, network_id, shared=False):
         return True
     except Exception, e:
         print "Error [update_neutron_net(neutron_client, '%s', '%s')]:" % \
-            (network_id,str(shared)), e
+            (network_id, str(shared)), e
         return False
 
 
@@ -325,7 +322,7 @@ def update_neutron_port(neutron_client, port_id, device_owner):
         return port['port']['id']
     except Exception, e:
         print "Error [update_neutron_port(neutron_client, '%s', '%s')]:" % \
-            (port_id,device_owner), e
+            (port_id, device_owner), e
         return False
 
 
@@ -336,14 +333,15 @@ def add_interface_router(neutron_client, router_id, subnet_id):
         return True
     except Exception, e:
         print "Error [add_interface_router(neutron_client, '%s', '%s')]:" % \
-            (router_id,subnet_id), e
+            (router_id, subnet_id), e
         return False
 
+
 def add_gateway_router(neutron_client, router_id):
     ext_net_id = get_external_net_id(neutron_client)
     router_dict = {'network_id': ext_net_id}
     try:
-        neutron_client.add_gateway_router(router_id,router_dict)
+        neutron_client.add_gateway_router(router_id, router_dict)
         return True
     except Exception, e:
         print "Error [add_gateway_router(neutron_client, '%s')]:" % router_id, e
@@ -396,7 +394,7 @@ def remove_interface_router(neutron_client, router_id, subnet_id):
         return True
     except Exception, e:
         print "Error [remove_interface_router(neutron_client, '%s', '%s')]:" % \
-            (router_id,subnet_id), e
+            (router_id, subnet_id), e
         return False
 
 
@@ -409,7 +407,6 @@ def remove_gateway_router(neutron_client, router_id):
         return False
 
 
-
 #*********************************************
 #   SEC GROUPS
 #*********************************************
@@ -424,44 +421,43 @@ def get_security_groups(neutron_client):
 
 
 def create_security_group(neutron_client, sg_name, sg_description):
-    json_body= {'security_group' : { 'name' : sg_name, \
-                                    'description' : sg_description }}
+    json_body = {'security_group': {'name': sg_name,
+                                    'description': sg_description}}
     try:
         secgroup = neutron_client.create_security_group(json_body)
         return secgroup['security_group']
     except Exception, e:
         print "Error [create_security_group(neutron_client, '%s', '%s')]:" % \
-            (sg_name,sg_description), e
+            (sg_name, sg_description), e
         return False
 
 
 def create_secgroup_rule(neutron_client, sg_id, direction, protocol,
-                         port_range_min = None, port_range_max = None):
-    if port_range_min == None and port_range_max == None:
-        json_body = { 'security_group_rule' : \
-                         { 'direction' : direction, \
-                          'security_group_id' : sg_id, \
-                          'protocol' : protocol } }
-    elif port_range_min != None and port_range_max != None:
-        json_body = { 'security_group_rule' : \
-                             { 'direction' : direction, \
-                              'security_group_id' : sg_id, \
-                              'port_range_min': port_range_min, \
-                              'port_range_max' : port_range_max, \
-                              'protocol' : protocol } }
+                         port_range_min=None, port_range_max=None):
+    if port_range_min is None and port_range_max is None:
+        json_body = {'security_group_rule': {'direction': direction,
+                                             'security_group_id': sg_id,
+                                             'protocol': protocol}}
+    elif port_range_min is not None and port_range_max is not None:
+        json_body = {'security_group_rule': {'direction': direction,
+                                             'security_group_id': sg_id,
+                                             'port_range_min': port_range_min,
+                                             'port_range_max': port_range_max,
+                                             'protocol': protocol}}
     else:
         print "Error [create_secgroup_rule(neutron_client, '%s', '%s', "\
-            "'%s', '%s', '%s', '%s')]:" %(neutron_client, sg_id, direction, \
-                                port_range_min, port_range_max, protocol),\
-                                " Invalid values for port_range_min, port_range_max"
+              "'%s', '%s', '%s', '%s')]:" % (neutron_client, sg_id, direction, \
+                                             port_range_min, port_range_max, protocol),\
+              " Invalid values for port_range_min, port_range_max"
         return False
     try:
         neutron_client.create_security_group_rule(json_body)
         return True
     except Exception, e:
         print "Error [create_secgroup_rule(neutron_client, '%s', '%s', "\
-            "'%s', '%s', '%s', '%s')]:" %(neutron_client, sg_id, direction, \
-                                port_range_min, port_range_max, protocol), e
+            "'%s', '%s', '%s', '%s')]:" % (neutron_client, sg_id, direction,
+                                           port_range_min, port_range_max,
+                                           protocol), e
         return False
 
 
@@ -487,7 +483,7 @@ def update_sg_quota(neutron_client, tenant_id, sg_quota, sg_rule_quota):
         return True
     except Exception, e:
         print "Error [update_sg_quota(neutron_client, '%s', '%s', "\
-            "'%s')]:" %(tenant_id, sg_quota, sg_rule_quota), e
+            "'%s')]:" % (tenant_id, sg_quota, sg_rule_quota), e
         return False
 
 
@@ -500,8 +496,6 @@ def delete_security_group(neutron_client, secgroup_id):
         return False
 
 
-
-
 #*********************************************
 #   GLANCE
 #*********************************************
@@ -538,7 +532,7 @@ def create_glance_image(glance_client, image_name, file_path, public=True):
         return image.id
     except Exception, e:
         print "Error [create_glance_image(glance_client, '%s', '%s', "\
-            "'%s')]:" %(image_name, file_path, str(public)), e
+            "'%s')]:" % (image_name, file_path, str(public)), e
         return False
 
 
@@ -551,7 +545,6 @@ def delete_glance_image(nova_client, image_id):
         return False
 
 
-
 #*********************************************
 #   CINDER
 #*********************************************
@@ -594,11 +587,11 @@ def update_cinder_quota(cinder_client, tenant_id, vols_quota,
 
     try:
         quotas_default = cinder_client.quotas.update(tenant_id,
-                                                      **quotas_values)
+                                                     **quotas_values)
         return True
     except Exception, e:
         print "Error [update_cinder_quota(cinder_client, '%s', '%s', '%s'" \
-            "'%s')]:" %(tenant_id, vols_quota, snapshots_quota, gigabytes_quota), e
+            "'%s')]:" % (tenant_id, vols_quota, snapshots_quota, gigabytes_quota), e
         return False
 
 
@@ -628,7 +621,6 @@ def delete_volume_type(cinder_client, volume_type):
         return False
 
 
-
 #*********************************************
 #   KEYSTONE
 #*********************************************
@@ -701,7 +693,7 @@ def create_user(keystone_client, user_name, user_password,
         return user.id
     except Exception, e:
         print "Error [create_user(keystone_client, '%s', '%s', '%s'" \
-            "'%s')]:" %(user_name, user_password, user_email, tenant_id), e
+            "'%s')]:" % (user_name, user_password, user_email, tenant_id), e
         return False
 
 
@@ -711,7 +703,7 @@ def add_role_user(keystone_client, user_id, role_id, tenant_id):
         return True
     except Exception, e:
         print "Error [add_role_user(keystone_client, '%s', '%s'" \
-            "'%s')]:" %(user_id, role_id, tenant_id), e
+            "'%s')]:" % (user_id, role_id, tenant_id), e
         return False
 
 
@@ -827,14 +819,14 @@ def get_pod_name(logger=None):
         return "unknown-pod"
 
 
-def push_results_to_db(db_url, case_name, logger, pod_name,
+def push_results_to_db(db_url, project, case_name, logger, pod_name,
                        version, payload):
     """
     POST results to the Result target DB
     """
     url = db_url + "/results"
     installer = get_installer_type(logger)
-    params = {"project_name": "functest", "case_name": case_name,
+    params = {"project_name": project, "case_name": case_name,
               "pod_name": pod_name, "installer": installer,
               "version": version, "details": payload}
 
@@ -845,8 +837,8 @@ def push_results_to_db(db_url, case_name, logger, pod_name,
             logger.debug(r)
         return True
     except Exception, e:
-        print "Error [push_results_to_db('%s', '%s', '%s', '%s', '%s')]:" \
-            % (db_url, case_name, pod_name, version, payload), e
+        print "Error [push_results_to_db('%s', '%s', '%s', '%s', '%s', '%s')]:" \
+            % (db_url, project, case_name, pod_name, version, payload), e
         return False
 
 
@@ -861,7 +853,7 @@ def get_resolvconf_ns():
         ip = re.search(r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b", line)
         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         if ip:
-            result = sock.connect_ex((ip.group(),53))
+            result = sock.connect_ex((ip.group(), 53))
             if result == 0:
                 nameservers.append(ip.group())
         line = rconf.readline()
index a8ac97f..1746d38 100644 (file)
@@ -40,6 +40,9 @@ parser.add_argument("-d", "--debug", help="Debug mode",  action="store_true")
 parser.add_argument("-r", "--report",
                     help="Create json result file",
                     action="store_true")
+parser.add_argument("-n", "--noclean",
+                    help="Don't clean the created resources for this test.",
+                    action="store_true")
 args = parser.parse_args()
 
 """ logging configuration """
@@ -134,7 +137,9 @@ def push_results():
         scenario = functest_utils.get_scenario(logger)
         pod_name = functest_utils.get_pod_name(logger)
 
-        functest_utils.push_results_to_db(db_url=DB_URL, case_name="vIMS",
+        functest_utils.push_results_to_db(db_url=DB_URL, 
+                                          project="functest",
+                                          case_name="vIMS",
                                           logger=logger, pod_name=pod_name,
                                           version=scenario,
                                           payload=RESULTS)
@@ -461,6 +466,8 @@ def main():
     cfy.undeploy_manager()
 
     ############### GENERAL CLEANUP ################
+    if args.noclean:
+        exit(0)
 
     ks_creds = functest_utils.get_credentials("keystone")
 
index 9c83c80..3050aad 100644 (file)
@@ -37,6 +37,9 @@ parser.add_argument("-d", "--debug", help="Debug mode", action="store_true")
 parser.add_argument("-r", "--report",
                     help="Create json result file",
                     action="store_true")
+parser.add_argument("-n", "--noclean",
+                    help="Don't clean the created resources for this test.",
+                    action="store_true")
 
 args = parser.parse_args()
 
@@ -198,6 +201,9 @@ def create_private_neutron_net(neutron):
 
 
 def cleanup(nova, neutron, image_id, network_dic, port_id1, port_id2, secgroup_id):
+    if args.noclean:
+        logger.debug("The OpenStack resources are not deleted.")
+        return True
 
     # delete both VMs
     logger.info("Cleaning up...")
@@ -288,6 +294,7 @@ def push_results(start_time_ts, duration, test_status):
         scenario = functest_utils.get_scenario(logger)
         pod_name = functest_utils.get_pod_name(logger)
         functest_utils.push_results_to_db(TEST_DB,
+                                          "functest",
                                           "vPing",
                                           logger, pod_name, scenario,
                                           payload={'timestart': start_time_ts,
index be1ed3f..9056296 100644 (file)
@@ -35,6 +35,9 @@ parser.add_argument("-d", "--debug", help="Debug mode", action="store_true")
 parser.add_argument("-r", "--report",
                     help="Create json result file",
                     action="store_true")
+parser.add_argument("-n", "--noclean",
+                    help="Don't clean the created resources for this test.",
+                    action="store_true")
 
 args = parser.parse_args()
 
@@ -192,6 +195,9 @@ def create_private_neutron_net(neutron):
 
 
 def cleanup(nova, neutron, image_id, network_dic, port_id1, port_id2):
+    if args.noclean:
+        logger.debug("The OpenStack resources are not deleted.")
+        return True
 
     # delete both VMs
     logger.info("Cleaning up...")
@@ -276,6 +282,7 @@ def push_results(start_time_ts, duration, test_status):
         scenario = functest_utils.get_scenario(logger)
         pod_name = functest_utils.get_pod_name(logger)
         functest_utils.push_results_to_db(TEST_DB,
+                                          "functest",
                                           "vPing_userdata",
                                           logger, pod_name, scenario,
                                           payload={'timestart': start_time_ts,