diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | ansible/site.yml | 10 | ||||
-rw-r--r-- | build.sh | 36 | ||||
-rw-r--r-- | docker/core/Dockerfile (renamed from docker/Dockerfile) | 4 | ||||
-rw-r--r-- | docker/core/testcases.yaml (renamed from docker/testcases.yaml) | 0 | ||||
-rw-r--r-- | docker/mts/Dockerfile | 24 | ||||
-rw-r--r-- | docker/mts/mts-installer.properties | 2 | ||||
-rw-r--r-- | docker/mts/testcases.yaml | 26 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | setup.cfg | 1 | ||||
-rw-r--r-- | tox.ini | 2 | ||||
-rw-r--r-- | xtesting/ci/testcases.yaml | 20 | ||||
-rw-r--r-- | xtesting/core/mts.py | 287 | ||||
-rw-r--r-- | xtesting/samples/mts/pause.xml | 7 | ||||
-rw-r--r-- | xtesting/samples/mts/test.xml | 9 |
15 files changed, 421 insertions, 10 deletions
@@ -21,3 +21,5 @@ build dist AUTHORS ChangeLog +.eggs/ +.vscode/
\ No newline at end of file diff --git a/ansible/site.yml b/ansible/site.yml index ccc42b3f..3f49628e 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -13,7 +13,12 @@ containers: - name: xtesting ref_arg: BRANCH - path: docker + path: docker/core + - name: opnfv/xtesting-mts + containers: + - name: xtesting-mts + ref_arg: BRANCH + path: docker/mts suites: - container: xtesting tests: @@ -23,3 +28,6 @@ - fourth - fifth - sixth + - container: xtesting-mts + tests: + - seventh @@ -8,27 +8,51 @@ set -xe repo=${REPO:-opnfv} -tag=${BRANCH:-latest} arch=${arch-"\ amd64 \ arm64 \ arm"} +amd64_dirs=${amd64_dirs-"\ +docker/core \ +docker/mts"} +arm_dirs=${arm_dirs-${amd64_dirs}} +arm64_dirs=${arm64_dirs-${amd64_dirs}} +tag=${BRANCH:-latest} image="xtesting" build_opts=(--pull=true --no-cache --force-rm=true) -for arch in ${arch};do +for arch in ${arch}; do if [[ ${arch} == arm64 ]]; then find . -name Dockerfile -exec sed -i \ -e "s|alpine:3.12|arm64v8/alpine:3.12|g" {} + + find . -name Dockerfile -exec sed -i \ + -e "s|opnfv/xtesting|${repo}/xtesting:arm64-${tag}|g" {} + elif [[ ${arch} == arm ]]; then find . -name Dockerfile -exec sed -i \ -e "s|alpine:3.12|arm32v6/alpine:3.12|g" {} + + find . -name Dockerfile -exec sed -i \ + -e "s|opnfv/xtesting|${repo}/xtesting:arm-${tag}|g" {} + + else + find . -name Dockerfile -exec sed -i \ + -e "s|opnfv/xtesting|${repo}/xtesting:amd64-${tag}|g" {} + fi - (cd docker && docker build "${build_opts[@]}" \ - -t "${repo}/${image}:${arch}-${tag}" .) - docker push "${repo}/${image}:${arch}-${tag}" + dirs=${arch}_dirs + for dir in ${!dirs}; do + if [[ ${dir} == docker/core ]]; then + image=xtesting + else + image=xtesting-${dir##**/} + fi + (cd "${dir}" && + docker build "${build_opts[@]}" \ + -t "${repo}/${image}:${arch}-${tag}" . && + docker push "${repo}/${image}:${arch}-${tag}") + [ "${dir}" != "docker/core" ] && + (docker rmi \ + "${repo}/${image}:${arch}-${tag}" || true) + done [ "$?" == "0" ] && - (sudo docker rmi "${repo}/${image}:${arch}-${tag}"|| true) + (sudo docker rmi "${repo}/xtesting:${arch}-${tag}"|| true) find . -name Dockerfile -exec git checkout \{\} +; done exit $? diff --git a/docker/Dockerfile b/docker/core/Dockerfile index a5e3801e..58389734 100644 --- a/docker/Dockerfile +++ b/docker/core/Dockerfile @@ -3,9 +3,9 @@ FROM alpine:3.12 ARG BRANCH=master ARG OPENSTACK_TAG=master -RUN apk --no-cache add --update python3 py3-pip bash git mailcap && \ +RUN apk --no-cache add --update python3 py3-pip bash git mailcap libxml2 libxslt && \ apk --no-cache add --virtual .build-deps --update \ - python3-dev build-base && \ + python3-dev build-base libxml2-dev libxslt-dev && \ git init /src/functest-xtesting && \ (cd /src/functest-xtesting && \ git fetch --tags https://gerrit.opnfv.org/gerrit/functest-xtesting $BRANCH && \ diff --git a/docker/testcases.yaml b/docker/core/testcases.yaml index 7708a86c..7708a86c 100644 --- a/docker/testcases.yaml +++ b/docker/core/testcases.yaml diff --git a/docker/mts/Dockerfile b/docker/mts/Dockerfile new file mode 100644 index 00000000..597e4f4b --- /dev/null +++ b/docker/mts/Dockerfile @@ -0,0 +1,24 @@ +FROM opnfv/xtesting + +ARG MTS_TAG=6.6.3 +ARG APP_FOLDER=/opt/mts +ARG MAVEN_OPTS= +ENV JAVA_HOME=/usr/lib/jvm/java-1.8-openjdk +ENV NGN_JAVA_HOME=${JAVA_HOME}/bin +ENV MAVEN_OPTS=$MAVEN_OPTS + +COPY mts-installer.properties /src/mts-installer.properties +RUN apk --no-cache add --update openjdk8-jre lksctp-tools libpcap && \ + apk --no-cache add --virtual .build-deps --update \ + libpcap-dev openjdk8 maven git && \ + git init /src/git-mts && \ + (cd /src/git-mts && \ + git fetch --tags https://github.com/ericsson-mts/mts $MTS_TAG && \ + git checkout FETCH_HEAD && \ + echo ${NGN_JAVA_HOME} > src/main/bin/java_home.release && \ + mvn versions:set -DnewVersion=${MTS_TAG} && mvn package && mvn install && \ + java -jar target/mts-${MTS_TAG}-installer.jar -options /src/mts-installer.properties) && \ + rm -rf /root/.m2/ ${APP_FOLDER}/tutorial /src/mts-installer.properties /src/git-mts && \ + apk del .build-deps +COPY testcases.yaml /usr/lib/python3.8/site-packages/xtesting/ci/testcases.yaml +CMD ["run_tests", "-t", "all"] diff --git a/docker/mts/mts-installer.properties b/docker/mts/mts-installer.properties new file mode 100644 index 00000000..fe4e030f --- /dev/null +++ b/docker/mts/mts-installer.properties @@ -0,0 +1,2 @@ +INSTALL_PATH=/opt/mts +java_memory=1024 diff --git a/docker/mts/testcases.yaml b/docker/mts/testcases.yaml new file mode 100644 index 00000000..40bb46d8 --- /dev/null +++ b/docker/mts/testcases.yaml @@ -0,0 +1,26 @@ +--- +tiers: + - + name: samples + order: 1 + description: '' + testcases: + - + case_name: seventh + project_name: xtesting + enabled: true + criteria: 100 + blocking: true + clean_flag: false + description: 'Some MTS tests' + run: + name: 'mts' + args: + test_file: /usr/lib/python3.8/site-packages/xtesting/samples/mts/test.xml + testcases: + - Pause_5_sec + max_duration: 2 # in seconds + log_level: INFO + store_method: FILE + java_memory: 2048 + console: true diff --git a/requirements.txt b/requirements.txt index d3019492..b3e0a620 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ os-testr<2.0.0;python_version=='2.7' # Apache-2.0 os-testr;python_version>='3.6' # Apache-2.0 junitxml boto3 # Apache-2.0 +lxml!=3.7.0 # BSD @@ -31,6 +31,7 @@ xtesting.testcase = unit = xtesting.core.unit:Suite first = xtesting.samples.first:Test second = xtesting.samples.second:Test + mts = xtesting.core.mts:MTSLauncher [build_sphinx] all_files = 1 @@ -33,7 +33,7 @@ basepython = python3.8 whitelist_externals = bash commands = pylint --min-similarity-lines=10 \ - --disable=locally-disabled --ignore-imports=y --reports=n xtesting + --disable=locally-disabled --ignore-imports=y --reports=n --extension-pkg-whitelist=lxml xtesting [testenv:yamllint] basepython = python3.8 diff --git a/xtesting/ci/testcases.yaml b/xtesting/ci/testcases.yaml index 9de9c4bb..802708d0 100644 --- a/xtesting/ci/testcases.yaml +++ b/xtesting/ci/testcases.yaml @@ -81,3 +81,23 @@ tiers: - /usr/lib/python3.6/site-packages/xtesting/samples/features/ tags: - foo + + - + case_name: seventh + project_name: xtesting + enabled: true + criteria: 100 + blocking: true + clean_flag: false + description: '' + run: + name: 'mts' + args: + test_file: /opt/mts/bin/test/test.xml + testcases: + - Pause_5_sec + max_duration: 2 # in seconds + log_level: INFO + store_method: FILE + java_memory: 2048 + console: true diff --git a/xtesting/core/mts.py b/xtesting/core/mts.py new file mode 100644 index 00000000..a5cc6a1d --- /dev/null +++ b/xtesting/core/mts.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python + +# Copyright (c) 2020 Orange and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 + +# pylint: disable=too-many-instance-attributes + +"""Define the parent classes of all Xtesting Features. + +Feature is considered as TestCase offered by Third-party. It offers +helpers to run any python method or any bash command. +""" + +import csv +import logging +import os +import subprocess +import sys +import time + +from lxml import etree +import prettytable + +from xtesting.core import testcase + + +__author__ = ("Vincent Mahe <v.mahe@orange.com>, " + "Cedric Ollivier <cedric.ollivier@orange.com>") + + +class MTSLauncher(testcase.TestCase): + """Class designed to run MTS tests.""" + + __logger = logging.getLogger(__name__) + mts_install_dir = "/opt/mts" + + def __init__(self, **kwargs): + super(MTSLauncher, self).__init__(**kwargs) + self.result_file = "{}/{}.log".format(self.res_dir, self.case_name) + # Location of the HTML report generated by MTS + self.mts_stats_dir = os.path.join(self.res_dir, 'mts_stats_report') + # Location of the log files generated by MTS for each test. + # Need to end path with a separator because of a bug in MTS. + self.mts_logs_dir = os.path.join(self.res_dir, + 'mts_logs' + os.path.sep) + # The location of file named testPlan.csv + # that it always in $MTS_HOME/logs + self.mts_result_csv_file = self.mts_install_dir + os.path.sep + self.mts_result_csv_file += ("logs" + os.path.sep + "testPlan.csv") + self.total_tests = 0 + self.pass_tests = 0 + self.fail_tests = 0 + self.skip_tests = 0 + self.response = None + self.testcases = [] + + def parse_results(self): + """Parse testPlan.csv containing the status of each testcase of the test file. + See sample file in `xtesting/samples/mts/output/testPlan.csv` + """ + with open(self.mts_result_csv_file) as stream_: + self.__logger.info("Parsing file : %s", self.mts_result_csv_file) + reader = csv.reader(stream_, delimiter=';') + rownum = 0 + _tests_data = [] + msg = prettytable.PrettyTable( + header_style='upper', padding_width=5, + field_names=['MTS test', 'MTS test case', + 'status']) + for row in reader: + _test_dict = {} + nb_values = len(row) + if rownum > 0: + # If there's only one delimiter, + # it is the name of the <test> elt + if nb_values == 2: + test_name = row[0] + _test_dict['parent'] = test_name + elif nb_values == 3: + testcase_name = row[0].lstrip() + testcase_status = row[2] + self.total_tests += 1 + if testcase_status == 'OK': + self.pass_tests += 1 + elif testcase_status == 'Failed': + self.fail_tests += 1 + elif testcase_status == '?': + self.skip_tests += 1 + _test_dict['status'] = testcase_status + _test_dict['name'] = testcase_name + msg.add_row( + [test_name, + _test_dict['name'], + _test_dict['status']]) + rownum += 1 + _tests_data.append(_test_dict) + try: + self.result = 100 * ( + self.pass_tests / self.total_tests) + except ZeroDivisionError: + self.__logger.error("No test has been run") + self.__logger.info("MTS Test result:\n\n%s\n", msg.get_string()) + self.details = {} + self.details['description'] = "Execution of some MTS tests" + self.details['total_tests'] = self.total_tests + self.details['pass_tests'] = self.pass_tests + self.details['fail_tests'] = self.fail_tests + self.details['skip_tests'] = self.skip_tests + self.details['tests'] = _tests_data + + def parse_xml_test_file(self, xml_test_file): + """Parse the XML file containing the test definition for MTS. + See sample file in `xtesting/samples/mts/test.xml` + """ + nb_testcases = -1 + self.__logger.info( + "Parsing XML test file %s containing the MTS tests definitions.", + xml_test_file) + try: + parser = etree.XMLParser(load_dtd=True, resolve_entities=True) + self.__logger.info("XML test file %s successfully parsed.", + xml_test_file) + root = etree.parse(xml_test_file, parser=parser) + # Need to look at all child nodes because there may be + # some <for> elt between <test> and <testcase> elt + self.testcases = root.xpath('//test//testcase/@name') + nb_testcases = len(self.testcases) + if nb_testcases == 0: + self.__logger.warning("Found no MTS testcase !") + elif nb_testcases == 1: + self.__logger.info("Found only one MTS testcase: %s", + self.testcases[0]) + else: + self.__logger.info("Found %d MTS testcases :", nb_testcases) + for mts_testcase in self.testcases: + self.__logger.info(" - %s", mts_testcase) + except etree.XMLSyntaxError as xml_err: + self.__logger.error("Error while parsing XML test file: %s", + str(xml_err)) + return nb_testcases + + def check_enabled_mts_test_cases(self, enabled_testcases): + """Make sure that all required MTS test cases exist + in the XML test file. + """ + if len(enabled_testcases) > 0: + # Verify if the MTS test case exists in the whole list of test + # cases declared in the test XML file + for enabled_testcase in enabled_testcases: + if enabled_testcase not in self.testcases: + self.__logger.error( + "The required MTS testcase named `%s` does not exist" + " !", enabled_testcase) + return False + return True + + def execute(self, **kwargs): # pylint: disable=too-many-locals + """Execute the cmd passed as arg + + Args: + kwargs: Arbitrary keyword arguments. + + Returns: + 0 if cmd returns 0, + -1 otherwise. + """ + try: + console = kwargs["console"] if "console" in kwargs else False + # Read specific parameters for MTS + test_file = kwargs["test_file"] + log_level = kwargs[ + "log_level"] if "log_level" in kwargs else "INFO" + + # For some MTS tests, we need to force stop after N sec + max_duration = kwargs[ + "max_duration"] if "max_duration" in kwargs else None + store_method = kwargs[ + "store_method"] if "store_method" in kwargs else "FILE" + # Must use the $HOME_MTS/bin as current working dir + cwd = self.mts_install_dir + os.path.sep + "bin" + + # Get the list of enabled MTS testcases, if any + enabled_testcases = kwargs[ + "testcases"] if "testcases" in kwargs else [] + enabled_testcases_str = '' + if len(enabled_testcases) > 0: + enabled_testcases_str = ' '.join(enabled_testcases) + check_ok = self.check_enabled_mts_test_cases(enabled_testcases) + if not check_ok: + return -2 + + # Build command line to launch for MTS + cmd = ("cd {} && ./startCmd.sh {} {} -sequential -levelLog:{}" + " -storageLog:{}" + " -config:stats.REPORT_DIRECTORY+{}" + " -config:logs.STORAGE_DIRECTORY+{}" + " -genReport:true" + " -showRep:false").format(cwd, + test_file, + enabled_testcases_str, + log_level, + store_method, + self.mts_stats_dir, + self.mts_logs_dir) + + # Make sure to create the necessary output sub-folders for MTS + if not os.path.isdir(self.mts_stats_dir): + os.makedirs(self.mts_stats_dir) + if not os.path.isdir(self.mts_logs_dir): + os.makedirs(self.mts_logs_dir) + self.__logger.info( + "MTS statistics output dir: %s ", self.mts_stats_dir) + self.__logger.info("MTS logs output dir: %s ", self.mts_logs_dir) + + # Launch MTS as a sub-process + # and save its standard output to a file + with open(self.result_file, 'w') as f_stdout: + self.__logger.info("Calling %s", cmd) + process = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + for line in iter(process.stdout.readline, b''): + if console: + sys.stdout.write(line.decode("utf-8")) + f_stdout.write(line.decode("utf-8")) + try: + process.wait(timeout=max_duration) + except subprocess.TimeoutExpired: + process.kill() + self.__logger.info( + "Killing MTS process after %d second(s).", + max_duration) + return 3 + with open(self.result_file, 'r') as f_stdin: + self.__logger.debug("$ %s\n%s", cmd, f_stdin.read().rstrip()) + return process.returncode + except KeyError: + self.__logger.error("Missing mandatory arg for MTS. kwargs: %s", + kwargs) + return -1 + + def run(self, **kwargs): + """Run the feature. + + It allows executing any Python method by calling execute(). + + It sets the following attributes required to push the results + to DB: + + * result, + * start_time, + * stop_time. + + It doesn't fulfill details when pushing the results to the DB. + + Args: + kwargs: Arbitrary keyword arguments. + + Returns: + TestCase.EX_OK if execute() returns 0, + TestCase.EX_RUN_ERROR otherwise. + """ + self.start_time = time.time() + exit_code = testcase.TestCase.EX_RUN_ERROR + self.result = 0 + try: + nb_testcases = self.parse_xml_test_file(kwargs["test_file"]) + # Do something only if there are some MTS test cases in the test + # file + if nb_testcases > 0: + if self.execute(**kwargs) == 0: + exit_code = testcase.TestCase.EX_OK + try: + self.parse_results() + except Exception: # pylint: disable=broad-except + self.__logger.exception( + "Cannot parse result file " + "$MTS_HOME/logs/testPlan.csv") + exit_code = testcase.TestCase.EX_RUN_ERROR + except Exception: # pylint: disable=broad-except + self.__logger.exception("%s FAILED", self.project_name) + self.stop_time = time.time() + return exit_code diff --git a/xtesting/samples/mts/pause.xml b/xtesting/samples/mts/pause.xml new file mode 100644 index 00000000..34f7b52b --- /dev/null +++ b/xtesting/samples/mts/pause.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Related XMLSchema file: conf/schemas/scenario.xsd --> + +<scenario> + <log>Duree de pause demandee : [pauseDuration]</log> + <pause name="Pause" seconds="[pauseDuration]"/> +</scenario> diff --git a/xtesting/samples/mts/test.xml b/xtesting/samples/mts/test.xml new file mode 100644 index 00000000..5901f5b2 --- /dev/null +++ b/xtesting/samples/mts/test.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<test name="Commissioning MTS"> + <testcase name="Pause_5_sec" state="true"> + <parameter name="[pauseDuration]" operation="set" value="5"/> + + <scenario name="[testcasename]" file="pause.xml" /> + </testcase> +</test> |