From 5487cab0ab745285512cb87969dd174d86ec9568 Mon Sep 17 00:00:00 2001 From: Vincent Mahe Date: Mon, 25 May 2020 18:32:58 +0200 Subject: Add MTS driver It creates a new container xtesting-mts to avoid increase core container size. Signed-off-by: Vincent Mahe Change-Id: I59544023e1235747e140a442815778a133bf6acf (cherry picked from commit 32eb7687bf7b8440aa87805480789ef61ae65ec9) --- .gitignore | 2 + ansible/site.yml | 20 +++ build.sh | 34 ++++- docker/Dockerfile | 20 --- docker/core/Dockerfile | 20 +++ docker/core/testcases.yaml | 80 ++++++++++ docker/mts/Dockerfile | 24 +++ docker/mts/mts-installer.properties | 2 + docker/mts/testcases.yaml | 24 +++ docker/testcases.yaml | 80 ---------- requirements.txt | 1 + setup.cfg | 1 + tox.ini | 2 +- xtesting/ci/testcases.yaml | 20 +++ xtesting/core/mts.py | 287 ++++++++++++++++++++++++++++++++++++ xtesting/samples/mts/pause.xml | 7 + xtesting/samples/mts/test.xml | 9 ++ 17 files changed, 527 insertions(+), 106 deletions(-) delete mode 100644 docker/Dockerfile create mode 100644 docker/core/Dockerfile create mode 100644 docker/core/testcases.yaml create mode 100644 docker/mts/Dockerfile create mode 100644 docker/mts/mts-installer.properties create mode 100644 docker/mts/testcases.yaml delete mode 100644 docker/testcases.yaml create mode 100644 xtesting/core/mts.py create mode 100644 xtesting/samples/mts/pause.xml create mode 100644 xtesting/samples/mts/test.xml diff --git a/.gitignore b/.gitignore index 69ce36c7..c8e50570 100644 --- a/.gitignore +++ b/.gitignore @@ -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 23f316e4..8d52e171 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -5,6 +5,23 @@ docker_tags: - hunter: branch: stable/hunter + builds: + dependencies: + - repo: _ + dport: + container: alpine + tag: '3.9' + steps: + - name: opnfv/xtesting + containers: + - name: xtesting + ref_arg: BRANCH + path: docker/core + - name: opnfv/xtesting-mts + containers: + - name: xtesting-mts + ref_arg: BRANCH + path: docker/mts suites: - container: xtesting tests: @@ -14,3 +31,6 @@ - fourth - fifth - sixth + - container: xtesting-mts + tests: + - seventh diff --git a/build.sh b/build.sh index 4bad8199..a978d936 100644 --- a/build.sh +++ b/build.sh @@ -13,22 +13,46 @@ 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}} 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.9|arm64v8/alpine:3.9|g" {} + + find . -name Dockerfile -exec sed -i \ + -e "s|opnfv/xtesting:${tag}|${repo}/xtesting:arm64-${tag}|g" {} + elif [[ ${arch} == arm ]]; then find . -name Dockerfile -exec sed -i \ -e "s|alpine:3.9|arm32v6/alpine:3.9|g" {} + + find . -name Dockerfile -exec sed -i \ + -e "s|opnfv/xtesting:${tag}|${repo}/xtesting:arm-${tag}|g" {} + + else + find . -name Dockerfile -exec sed -i \ + -e "s|opnfv/xtesting:${tag}|${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/Dockerfile deleted file mode 100644 index c99dcc8e..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM alpine:3.9 - -ARG BRANCH=stable/hunter -ARG OPENSTACK_TAG=stable/rocky - -RUN apk --no-cache add --update python py-pip bash git mailcap && \ - apk --no-cache add --virtual .build-deps --update \ - python-dev build-base && \ - git init /src/functest-xtesting && \ - (cd /src/functest-xtesting && \ - git fetch --tags https://gerrit.opnfv.org/gerrit/functest-xtesting $BRANCH && \ - git checkout FETCH_HEAD) && \ - pip install --no-cache-dir --src /src \ - -chttps://opendev.org/openstack/requirements/raw/branch/$OPENSTACK_TAG/upper-constraints.txt \ - -chttps://git.opnfv.org/functest-xtesting/plain/upper-constraints.txt?h=$BRANCH \ - /src/functest-xtesting && \ - rm -r /src/functest-xtesting && \ - apk del .build-deps -COPY testcases.yaml /usr/lib/python2.7/site-packages/xtesting/ci/testcases.yaml -CMD ["run_tests", "-t", "all"] diff --git a/docker/core/Dockerfile b/docker/core/Dockerfile new file mode 100644 index 00000000..f08277db --- /dev/null +++ b/docker/core/Dockerfile @@ -0,0 +1,20 @@ +FROM alpine:3.9 + +ARG BRANCH=stable/hunter +ARG OPENSTACK_TAG=stable/rocky + +RUN apk --no-cache add --update python py-pip bash git mailcap libxml2 libxslt && \ + apk --no-cache add --virtual .build-deps --update \ + python-dev build-base && \ + git init /src/functest-xtesting && \ + (cd /src/functest-xtesting && \ + git fetch --tags https://gerrit.opnfv.org/gerrit/functest-xtesting $BRANCH && \ + git checkout FETCH_HEAD) && \ + pip install --no-cache-dir --src /src \ + -chttps://opendev.org/openstack/requirements/raw/branch/$OPENSTACK_TAG/upper-constraints.txt \ + -chttps://git.opnfv.org/functest-xtesting/plain/upper-constraints.txt?h=$BRANCH \ + /src/functest-xtesting && \ + rm -r /src/functest-xtesting && \ + apk del .build-deps +COPY testcases.yaml /usr/lib/python2.7/site-packages/xtesting/ci/testcases.yaml +CMD ["run_tests", "-t", "all"] diff --git a/docker/core/testcases.yaml b/docker/core/testcases.yaml new file mode 100644 index 00000000..87759417 --- /dev/null +++ b/docker/core/testcases.yaml @@ -0,0 +1,80 @@ +--- +tiers: + - + name: samples + description: '' + testcases: + - + case_name: first + project_name: xtesting + criteria: 100 + blocking: true + clean_flag: false + description: '' + run: + name: 'first' + + - + case_name: second + project_name: xtesting + criteria: 100 + blocking: true + clean_flag: false + description: '' + run: + name: 'second' + + - + case_name: third + project_name: xtesting + criteria: 100 + blocking: true + clean_flag: false + description: '' + run: + name: 'bashfeature' + args: + cmd: 'echo -n Hello World; exit 0' + + - + case_name: fourth + project_name: xtesting + criteria: 100 + blocking: true + clean_flag: false + description: '' + run: + name: 'unit' + args: + name: 'xtesting.samples.fourth' + + - + case_name: fifth + project_name: xtesting + criteria: 100 + blocking: true + clean_flag: false + description: '' + run: + name: 'robotframework' + args: + suites: + - /usr/lib/python2.7/site-packages/xtesting/samples/HelloWorld.robot + variable: + - 'var01:foo' + - 'var02:bar' + + - + case_name: sixth + project_name: xtesting + criteria: 100 + blocking: false + clean_flag: false + description: '' + run: + name: 'behaveframework' + args: + suites: + - /usr/lib/python3.6/site-packages/xtesting/samples/features/ + tags: + - foo 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..0694884e --- /dev/null +++ b/docker/mts/testcases.yaml @@ -0,0 +1,24 @@ +--- +tiers: + - + name: samples + description: '' + testcases: + - + case_name: seventh + project_name: xtesting + 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/docker/testcases.yaml b/docker/testcases.yaml deleted file mode 100644 index 87759417..00000000 --- a/docker/testcases.yaml +++ /dev/null @@ -1,80 +0,0 @@ ---- -tiers: - - - name: samples - description: '' - testcases: - - - case_name: first - project_name: xtesting - criteria: 100 - blocking: true - clean_flag: false - description: '' - run: - name: 'first' - - - - case_name: second - project_name: xtesting - criteria: 100 - blocking: true - clean_flag: false - description: '' - run: - name: 'second' - - - - case_name: third - project_name: xtesting - criteria: 100 - blocking: true - clean_flag: false - description: '' - run: - name: 'bashfeature' - args: - cmd: 'echo -n Hello World; exit 0' - - - - case_name: fourth - project_name: xtesting - criteria: 100 - blocking: true - clean_flag: false - description: '' - run: - name: 'unit' - args: - name: 'xtesting.samples.fourth' - - - - case_name: fifth - project_name: xtesting - criteria: 100 - blocking: true - clean_flag: false - description: '' - run: - name: 'robotframework' - args: - suites: - - /usr/lib/python2.7/site-packages/xtesting/samples/HelloWorld.robot - variable: - - 'var01:foo' - - 'var02:bar' - - - - case_name: sixth - project_name: xtesting - criteria: 100 - blocking: false - clean_flag: false - description: '' - run: - name: 'behaveframework' - args: - suites: - - /usr/lib/python3.6/site-packages/xtesting/samples/features/ - tags: - - foo diff --git a/requirements.txt b/requirements.txt index ea776baf..69bab64b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,3 +16,4 @@ python-subunit # Apache-2.0/BSD os-testr # Apache-2.0 junitxml boto3 # Apache-2.0 +lxml!=3.7.0 # BSD diff --git a/setup.cfg b/setup.cfg index 9b8dc92e..755d1079 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/tox.ini b/tox.ini index 205f2f87..018f4f58 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ basepython = python2.7 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 = python2.7 diff --git a/xtesting/ci/testcases.yaml b/xtesting/ci/testcases.yaml index 1d5ac3df..f987ee59 100644 --- a/xtesting/ci/testcases.yaml +++ b/xtesting/ci/testcases.yaml @@ -80,3 +80,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 , " + "Cedric Ollivier ") + + +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 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 elt between and 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 @@ + + + + + Duree de pause demandee : [pauseDuration] + + 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 @@ + + + + + + + + + -- cgit 1.2.3-korg