mirror of https://github.com/apache/cassandra
Standalone Jenkinsfile
– ensure test file paths, and their suite names are unique (the latter was broken for cqlshlib and python dtests) – removal of properties and system-out in test xml happens in CassandraXMLJUnitResultFormatter – new jenkins agent labels and introduce agents sizes - ci_summary.html generation script, ref work submitted in apache/cassandra-builds#99 – fix for stress-test and fqltool-test running on small - ant generate-test-report is limited to only running on individual test types (ci_parser.py provides ci_summary.html for the overview now) - each cell has a single retry, and the retry will happen on a different agent - on ci-cassandra the summary stage happens on the builtin, bc copyArtifacts on 15k+ files takes many hours otherwise - test-burn only needs two splits - dependency-check is disabled from the lint target until CASSANDRA-19213 - add $DEBUG env var to in-tree scripts, turns on bash debug - fix FBUtilities' handling of gcp cos_containerd (kernel version comes with a trailing '+' character) patch by Aleks Volochnev, Mick Semb Wever; reviewed by Aleksandr Volochnev, Josh McKenzie, Maxim Muzafarov, Stefan Miklosovic for CASSANDRA-18594 Co-authored-by: Aleksandr Volochnev <a.volochnev@gmail.com> Co-authored-by: Mick Semb Wever <mck@apache.org> Co-authored-by: Josh McKenzie <jmckenzie@apache.org> Co-authored-by: Artem Chekunov <artem.v.chekunov@gmail.com>
This commit is contained in:
parent
b0150e86fb
commit
f7c11bdcd4
122
.build/README.md
122
.build/README.md
|
|
@ -29,66 +29,6 @@ The following applies to all build scripts.
|
|||
|
||||
build_dir=/tmp/cass_Mtu462n .build/docker/check-code.sh
|
||||
|
||||
Running Sonar analysis (experimental)
|
||||
-------------------------------------
|
||||
|
||||
Run:
|
||||
|
||||
ant sonar
|
||||
|
||||
Sonar analysis requires the SonarQube server to be available. If there
|
||||
is already some SonarQube server, it can be used by setting the
|
||||
following env variables:
|
||||
|
||||
SONAR_HOST_URL=http://sonar.example.com
|
||||
SONAR_CASSANDRA_TOKEN=cassandra-project-analysis-token
|
||||
SONAR_PROJECT_KEY=<key of the Cassandra project in SonarQube>
|
||||
|
||||
If SonarQube server is not available, one can be started locally in
|
||||
a Docker container. The following command will create a SonarQube
|
||||
container and start the server:
|
||||
|
||||
ant sonar-create-server
|
||||
|
||||
The server will be available at http://localhost:9000 with admin
|
||||
credentials admin/password. The Docker container named `sonarqube`
|
||||
is created and left running. When using this local SonarQube server,
|
||||
no env variables to configure url, token, or project key are needed,
|
||||
and the analysis can be run right away with `ant sonar`.
|
||||
|
||||
After the analysis, the server remains running so that one can
|
||||
inspect the results.
|
||||
|
||||
To stop the local SonarQube server:
|
||||
|
||||
ant sonar-stop-server
|
||||
|
||||
However, this command just stops the Docker container without removing
|
||||
it. It allows to start the container later with:
|
||||
|
||||
docker container start sonarqube
|
||||
|
||||
and access previous analysis results. To drop the container, run:
|
||||
|
||||
docker container rm sonarqube
|
||||
|
||||
When `SONAR_HOST_URL` is not provided, the script assumes a dedicated
|
||||
local instance of the SonarQube server and sets it up automatically,
|
||||
which includes creating a project, setting up the quality profile, and
|
||||
quality gate from the configuration stored in
|
||||
[sonar-quality-profile.xml](sonar%2Fsonar-quality-profile.xml) and
|
||||
[sonar-quality-gate.json](sonar%2Fsonar-quality-gate.json)
|
||||
respectively. To run the analysis with a custom quality profile, start
|
||||
the server using `ant sonar-create-server`, create a project manually,
|
||||
and set up a desired quality profile for it. Then, create the analysis
|
||||
token for the project and export the following env variables:
|
||||
|
||||
SONAR_HOST_URL="http://127.0.0.1:9000"
|
||||
SONAR_CASSANDRA_TOKEN="<token>"
|
||||
SONAR_PROJECT_KEY="<key of the Cassandra project in SonarQube>"
|
||||
|
||||
The analysis can be run with `ant sonar`.
|
||||
|
||||
|
||||
Building Artifacts (tarball and maven)
|
||||
-------------------------------------
|
||||
|
|
@ -177,7 +117,7 @@ Running other types of tests with docker:
|
|||
.build/docker/run-tests.sh jvm-dtest-upgrade
|
||||
.build/docker/run-tests.sh dtest
|
||||
.build/docker/run-tests.sh dtest-novnode
|
||||
.build/docker/run-tests.sh dtest-offheap
|
||||
.build/docker/run-tests.sh dtest-latest
|
||||
.build/docker/run-tests.sh dtest-large
|
||||
.build/docker/run-tests.sh dtest-large-novnode
|
||||
.build/docker/run-tests.sh dtest-upgrade
|
||||
|
|
@ -198,3 +138,63 @@ Other python dtest types without docker:
|
|||
|
||||
.build/run-python-dtests.sh dtest-upgrade-large
|
||||
|
||||
|
||||
Running Sonar analysis (experimental)
|
||||
-------------------------------------
|
||||
|
||||
Run:
|
||||
|
||||
ant sonar
|
||||
|
||||
Sonar analysis requires the SonarQube server to be available. If there
|
||||
is already some SonarQube server, it can be used by setting the
|
||||
following env variables:
|
||||
|
||||
SONAR_HOST_URL=http://sonar.example.com
|
||||
SONAR_CASSANDRA_TOKEN=cassandra-project-analysis-token
|
||||
SONAR_PROJECT_KEY=<key of the Cassandra project in SonarQube>
|
||||
|
||||
If SonarQube server is not available, one can be started locally in
|
||||
a Docker container. The following command will create a SonarQube
|
||||
container and start the server:
|
||||
|
||||
ant sonar-create-server
|
||||
|
||||
The server will be available at http://localhost:9000 with admin
|
||||
credentials admin/password. The Docker container named `sonarqube`
|
||||
is created and left running. When using this local SonarQube server,
|
||||
no env variables to configure url, token, or project key are needed,
|
||||
and the analysis can be run right away with `ant sonar`.
|
||||
|
||||
After the analysis, the server remains running so that one can
|
||||
inspect the results.
|
||||
|
||||
To stop the local SonarQube server:
|
||||
|
||||
ant sonar-stop-server
|
||||
|
||||
However, this command just stops the Docker container without removing
|
||||
it. It allows to start the container later with:
|
||||
|
||||
docker container start sonarqube
|
||||
|
||||
and access previous analysis results. To drop the container, run:
|
||||
|
||||
docker container rm sonarqube
|
||||
|
||||
When `SONAR_HOST_URL` is not provided, the script assumes a dedicated
|
||||
local instance of the SonarQube server and sets it up automatically,
|
||||
which includes creating a project, setting up the quality profile, and
|
||||
quality gate from the configuration stored in
|
||||
[sonar-quality-profile.xml](sonar%2Fsonar-quality-profile.xml) and
|
||||
[sonar-quality-gate.json](sonar%2Fsonar-quality-gate.json)
|
||||
respectively. To run the analysis with a custom quality profile, start
|
||||
the server using `ant sonar-create-server`, create a project manually,
|
||||
and set up a desired quality profile for it. Then, create the analysis
|
||||
token for the project and export the following env variables:
|
||||
|
||||
SONAR_HOST_URL="http://127.0.0.1:9000"
|
||||
SONAR_CASSANDRA_TOKEN="<token>"
|
||||
SONAR_PROJECT_KEY="<key of the Cassandra project in SonarQube>"
|
||||
|
||||
The analysis can be run with `ant sonar`.
|
||||
|
|
|
|||
|
|
@ -24,5 +24,5 @@ command -v ant >/dev/null 2>&1 || { echo >&2 "ant needs to be installed"; exit 1
|
|||
[ -f "${CASSANDRA_DIR}/build.xml" ] || { echo >&2 "${CASSANDRA_DIR}/build.xml must exist"; exit 1; }
|
||||
|
||||
# execute
|
||||
ant -f "${CASSANDRA_DIR}/build.xml" check dependency-check
|
||||
ant -f "${CASSANDRA_DIR}/build.xml" check # dependency-check # FIXME dependency-check now requires NVD key downloaded first
|
||||
exit $?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,307 @@
|
|||
#!/usr/bin/env python3
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
|
||||
|
||||
"""
|
||||
Script to take an arbitrary root directory of subdirectories of junit output format and create a summary .html file
|
||||
with their results.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import cProfile
|
||||
import pstats
|
||||
import os
|
||||
import shutil
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Callable, Dict, Tuple, Type
|
||||
from pathlib import Path
|
||||
|
||||
from junit_helpers import JUnitResultBuilder, JUnitTestCase, JUnitTestSuite, JUnitTestStatus, LOG_FILE_NAME
|
||||
from logging_helper import build_logger, mute_logging, CustomLogger
|
||||
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
except ImportError:
|
||||
print('bs4 not installed; make sure you have bs4 in your active python env.')
|
||||
exit(1)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description="""
|
||||
Parses ci results provided ci output in input path and generates html
|
||||
results in specified output file. Expects an existing .html file to insert
|
||||
results into; this file will be backed up into a <file_name>.bak file in its
|
||||
local directory.
|
||||
""")
|
||||
parser.add_argument('--input', type=str, help='path to input files (recursive directory search for *.xml)')
|
||||
# TODO: Change this paradigm to a full "input dir translates into output file", where output file includes some uuid
|
||||
# We'll need full support for all job types, not just junit, which will also necessitate refactoring into some kind of
|
||||
# TestResultParser, of which JUnit would be one type. But there's a clear pattern here we can extract. Thinking checkstyle.
|
||||
parser.add_argument('--output', type=str, help='existing .html output file to append to')
|
||||
parser.add_argument('--mute', action='store_true', help='mutes stdout and only logs to log file')
|
||||
parser.add_argument('--profile', '-p', action='store_true', help='Enable perf profiling on operations')
|
||||
parser.add_argument('-v', '-d', '--verbose', '--debug', dest='debug', action='store_true', help='verbose log output')
|
||||
args = parser.parse_args()
|
||||
if args.input is None or args.output is None:
|
||||
parser.print_help()
|
||||
exit(1)
|
||||
|
||||
logger = build_logger(LOG_FILE_NAME, args.debug) # type: CustomLogger
|
||||
if args.mute:
|
||||
mute_logging(logger)
|
||||
|
||||
|
||||
def main():
|
||||
check_file_condition(lambda: os.path.exists(args.input), f'Cannot find {args.input}. Aborting.')
|
||||
|
||||
xml_files = [str(file) for file in Path(args.input).rglob('*.xml')]
|
||||
check_file_condition(lambda: len(xml_files) != 0, f'Found 0 .xml files in path: {args.input}. Cannot proceed with .xml extraction.')
|
||||
logger.info(f'Found {len(xml_files)} xml files under: {args.input}')
|
||||
|
||||
test_suites = process_xml_files(xml_files)
|
||||
|
||||
for suite in test_suites.values():
|
||||
if suite.is_empty() and suite.file_count() == 0:
|
||||
logger.warning(f'Have an empty test_suite: {suite.name()} that had no .xml files associated with it. Did the jobs run correctly and produce junit files? Check {suite.get_archive()} for test run command result details.')
|
||||
elif suite.is_empty():
|
||||
logger.warning(f'Got an unexpected empty test_suite: {suite.name()} with no .xml file parsing associated with it. Check {LOG_FILE_NAME}.log when run with -v for details.')
|
||||
|
||||
create_summary_file(test_suites, xml_files, args.output)
|
||||
|
||||
|
||||
def process_xml_files(xml_files: str) -> Dict[str, JUnitTestSuite]:
|
||||
"""
|
||||
For a given input input_dir, will find all .xml files in that tree, extract files from them preserving input_dir structure
|
||||
and parse out all found junit test results into the global test result containers.
|
||||
:param xml_files: all .xml files under args.input_dir
|
||||
"""
|
||||
|
||||
test_suites = dict() # type: Dict[str, JUnitTestSuite]
|
||||
test_count = 0
|
||||
|
||||
for file in xml_files:
|
||||
files, tests = process_xml_file(file, test_suites)
|
||||
test_count += tests
|
||||
|
||||
logger.progress(f'Total junit file count: {len(xml_files)}')
|
||||
logger.progress(f'Total suite count: {len(test_suites.keys())}')
|
||||
logger.progress(f'Total test count: {test_count}')
|
||||
passed = 0
|
||||
failed = 0
|
||||
skipped = 0
|
||||
|
||||
for suite in test_suites.values():
|
||||
passed += suite.passed()
|
||||
failed += suite.failed()
|
||||
if suite.failed() != 0:
|
||||
print_errors(suite)
|
||||
skipped += suite.skipped()
|
||||
|
||||
logger.progress(f'-- Passed: {passed}')
|
||||
logger.progress(f'-- Failed: {failed}')
|
||||
logger.progress(f'-- Skipped: {skipped}')
|
||||
return test_suites
|
||||
|
||||
|
||||
def process_xml_file(xml_file, test_suites: Dict[str, JUnitTestSuite]) -> Tuple[int, int]:
|
||||
"""
|
||||
Pretty straightforward here - walk through and look for tests,
|
||||
parsing them out into our global JUnitTestCase Dicts as we find them
|
||||
|
||||
No thread safety on target Dict -> relying on the "one .xml per suite" rule to keep things clean
|
||||
|
||||
Can be called in context of executor thread.
|
||||
:return: Tuple[file count, test count]
|
||||
"""
|
||||
|
||||
# TODO: In extreme cases (python upgrade dtests), this could theoretically be a HUGE file we're materializing in memory. Consider .iterparse or tag sanitization using sed first.
|
||||
with open(xml_file, "rb") as xml_input:
|
||||
try:
|
||||
suite_name = "?"
|
||||
root = ET.parse(xml_input).getroot() # type: ignore
|
||||
suite_name = str(root.get('name'))
|
||||
logger.progress(f'Processing archive: {xml_file} for test suite: {suite_name}')
|
||||
|
||||
# And make sure we're not racing
|
||||
if suite_name in test_suites:
|
||||
logger.error(f'Got a duplicate suite_name - this will lead to race conditions. Suite: {suite_name}. xml file: {xml_file}. Skipping this file.')
|
||||
return 0, 0
|
||||
else:
|
||||
test_suites[suite_name] = JUnitTestSuite(suite_name)
|
||||
|
||||
active_suite = test_suites[suite_name]
|
||||
# Store this for later logging if we have a failed job; help the user know where to look next.
|
||||
active_suite.set_archive(xml_file)
|
||||
test_file_count = 0
|
||||
test_count = 0
|
||||
fc = process_test_cases(active_suite, xml_file, root)
|
||||
if fc != 0:
|
||||
test_file_count += 1
|
||||
test_count += fc
|
||||
except (EOFError, ET.ParseError) as e:
|
||||
logger.error(f'Error on {xml_file}: {e}. Skipping; will be missing results for {suite_name}')
|
||||
return 0, 0
|
||||
except Exception as e:
|
||||
logger.critical(f'Got unexpected error while parsing {xml_file}: {e}. Aborting.')
|
||||
raise e
|
||||
return test_file_count, test_count
|
||||
|
||||
|
||||
def print_errors(suite: JUnitTestSuite) -> None:
|
||||
logger.warning(f'\n[Printing {suite.failed()} tests from suite: {suite.name()}]')
|
||||
for testcase in suite.get_tests(JUnitTestStatus.FAILURE):
|
||||
logger.warning(f'{testcase}')
|
||||
|
||||
|
||||
def process_test_cases(suite: JUnitTestSuite, file_name: str, root) -> int:
|
||||
"""
|
||||
For a given input .xml, will extract all JUnitTestCase matching objects and store them in the global registry keyed off
|
||||
suite name.
|
||||
|
||||
Can be called in context of executor thread.
|
||||
:param suite: The JUnitTestSuite object we're currently working with
|
||||
:param file_name: .xml file_name to check for tests. junit format.
|
||||
:param root: etree root for file_name
|
||||
:return : count of tests extracted from this file_name
|
||||
"""
|
||||
xml_exclusions = ['logback', 'checkstyle']
|
||||
if any(x in file_name for x in xml_exclusions):
|
||||
return 0
|
||||
|
||||
# Search inside entire hierarchy since sometimes it's at the root and sometimes one level down.
|
||||
test_count = len(root.findall('.//testcase'))
|
||||
if test_count == 0:
|
||||
logger.warning(f'Appear to be processing an .xml file without any junit tests in it: {file_name}. Update .xml exclusions to exclude this.')
|
||||
if args.debug:
|
||||
logger.info(ET.tostring(root))
|
||||
return 0
|
||||
|
||||
suite.add_file(file_name)
|
||||
found = 0
|
||||
for testcase in root.iter('testcase'):
|
||||
processed = JUnitTestCase(testcase)
|
||||
suite.add_testcase(processed)
|
||||
found = 1
|
||||
if found == 0:
|
||||
logger.error(f'file: {file_name} has test_count: {test_count} but root.iter iterated across nothing!')
|
||||
logger.error(ET.tostring(root))
|
||||
return test_count
|
||||
|
||||
|
||||
# TODO: Update this to instead be "create_summary_file" and build the entire summary page, not just append failures to existing
|
||||
# This should be trivial to do using JUnitTestSuite.failed, passed, etc methods
|
||||
def create_summary_file(test_suites: Dict[str, JUnitTestSuite], xml_files, output: str) -> None:
|
||||
"""
|
||||
Will create a table with all failed tests in it organized by sorted suite name.
|
||||
:param test_suites: Collection of JUnitTestSuite's parsed out pass/fail data
|
||||
:param output: Path to the .html we want to append to the <body> of
|
||||
"""
|
||||
|
||||
# if needed create a blank ci_summary.html
|
||||
if not os.path.exists(args.output):
|
||||
with open(args.output, "w") as ci_summary_html:
|
||||
ci_summary_html.write('<html><head><body><h1>CI Summary</h1></body></head></html>')
|
||||
|
||||
with open(output, 'r') as file:
|
||||
soup = BeautifulSoup(file, 'html.parser')
|
||||
|
||||
failures_tag = soup.new_tag("div")
|
||||
failures_tag.string = '<br/><br/>[Test Failure Details]<br/><br/>'
|
||||
suites_tag = soup.new_tag("div")
|
||||
suites_tag.string = '<br/><br/><hr/>[Test Suite Details]<br/><br/>'
|
||||
suites_builder = JUnitResultBuilder('Suites')
|
||||
suites_builder.label_columns(['Suite', 'Passed', 'Failed', 'Skipped'], ["width: 70%; text-align: left;", "width: 10%; text-align: right", "width: 10%; text-align: right", "width: 10%; text-align: right"])
|
||||
|
||||
JUnitResultBuilder.add_style_tags(soup)
|
||||
|
||||
# We cut off at 200 failures; if you have > than that chances are you have a bad run and there's no point in
|
||||
# just continuing to pollute the summary file with it and blow past file size. Since the inlined failures are
|
||||
# a tool to be used in the attaching / review process and not primarily workflow and fixing.
|
||||
total_passed_count = 0
|
||||
total_skipped_count = 0
|
||||
total_failure_count = 0
|
||||
for suite_name in sorted(test_suites.keys()):
|
||||
suite = test_suites[suite_name]
|
||||
passed_count = suite.passed()
|
||||
skipped_count = suite.skipped()
|
||||
failure_count = suite.failed()
|
||||
|
||||
suites_builder.add_row([suite_name, str(passed_count), str(failure_count), str(skipped_count)])
|
||||
|
||||
if failure_count == 0:
|
||||
# Don't append anything to results in the happy path case.
|
||||
logger.debug(f'No failed tests in suite: {suite_name}')
|
||||
elif total_failure_count < 200:
|
||||
# Else independent table per suite.
|
||||
failures_builder = JUnitResultBuilder(suite_name)
|
||||
failures_builder.label_columns(['Class', 'Method', 'Output', 'Duration'], ["width: 15%; text-align: left;", "width: 15%; text-align: left;", "width: 60%; text-align: left;", "width: 10%; text-align: right;"])
|
||||
for test in suite.get_tests(JUnitTestStatus.FAILURE):
|
||||
failures_builder.add_row(test.row_data())
|
||||
failures_tag.append(BeautifulSoup(failures_builder.build_table(), 'html.parser'))
|
||||
total_failure_count += failure_count
|
||||
if total_failure_count > 200:
|
||||
logger.critical(f'Saw {total_failure_count} failures; greater than 200 threshold. Not appending further failure details to {output}.')
|
||||
total_passed_count += passed_count
|
||||
total_skipped_count += skipped_count
|
||||
|
||||
# totals, manual html
|
||||
totals_tag = soup.new_tag("div")
|
||||
totals_tag.string = f"""[Totals]<br/><br/><table style="width:100px">
|
||||
<tr><td >Passed</td><td></td><td align="right"> {total_passed_count}</td></tr>
|
||||
<tr><td >Failed</td><td></td><td align="right"> {total_failure_count}</td></tr>
|
||||
<tr><td >Skipped</td><td></td><td align="right"> {total_skipped_count}</td></tr>
|
||||
<tr><td >Total</td><td> </td><td align="right"> {total_passed_count + total_failure_count + total_skipped_count}</td></tr>
|
||||
<tr><td >Files</td><td></td><td align="right"> {len(xml_files)}</td></tr>
|
||||
<tr><td >Suites</td><td></td><td align="right"> {len(test_suites.keys())}</td></tr></table><hr/>
|
||||
"""
|
||||
|
||||
soup.body.append(totals_tag)
|
||||
soup.body.append(failures_tag)
|
||||
suites_tag.append(BeautifulSoup(suites_builder.build_table(), 'html.parser'))
|
||||
soup.body.append(suites_tag)
|
||||
|
||||
# Only backup the output file if we've gotten this far
|
||||
shutil.copyfile(output, output + '.bak')
|
||||
|
||||
# We write w/formatter set to None as invalid char above our insertion in the input file we're modifying (from other
|
||||
# tests, test output, etc) can cause the parser to get very confused and do Bad Things.
|
||||
with open(output, 'w') as file:
|
||||
file.write(soup.prettify(formatter=None))
|
||||
logger.progress(f'Test failure details appended to file: {output}')
|
||||
|
||||
|
||||
def check_file_condition(function: Callable[[], bool], msg: str) -> None:
|
||||
"""
|
||||
Specifically raises a FileNotFoundError if something's wrong with the Callable
|
||||
"""
|
||||
if not function():
|
||||
log_and_raise(msg, FileNotFoundError)
|
||||
|
||||
|
||||
def log_and_raise(msg: str, error_type: Type[BaseException]) -> None:
|
||||
logger.critical(msg)
|
||||
raise error_type(msg)
|
||||
|
||||
|
||||
if __name__ == "__main__" and args.profile:
|
||||
profiler = cProfile.Profile()
|
||||
profiler.enable()
|
||||
main()
|
||||
profiler.disable()
|
||||
stats = pstats.Stats(profiler).sort_stats('cumulative')
|
||||
stats.print_stats()
|
||||
else:
|
||||
main()
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
#!/bin/sh -e
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
|
||||
#
|
||||
# Creates ci_summary.html
|
||||
# This expects a folder hierarchy that separates test types/targets.
|
||||
# For example:
|
||||
# build/test/output/test
|
||||
# build/test/output/jvm-dtest
|
||||
# build/test/output/dtest
|
||||
#
|
||||
# The ci_summary.html file, along with the results_details.tar.xz,
|
||||
# are the sharable artefacts used to satisfy pre-commit CI from a private CI.
|
||||
#
|
||||
|
||||
# variables, with defaults
|
||||
[ "x${CASSANDRA_DIR}" != "x" ] || CASSANDRA_DIR="$(readlink -f $(dirname "$0")/../..)"
|
||||
[ "x${DIST_DIR}" != "x" ] || DIST_DIR="${CASSANDRA_DIR}/build"
|
||||
|
||||
# pre-conditions
|
||||
command -v ant >/dev/null 2>&1 || { echo >&2 "ant needs to be installed"; exit 1; }
|
||||
[ -d "${CASSANDRA_DIR}" ] || { echo >&2 "Directory ${CASSANDRA_DIR} must exist"; exit 1; }
|
||||
[ -f "${CASSANDRA_DIR}/build.xml" ] || { echo >&2 "${CASSANDRA_DIR}/build.xml must exist"; exit 1; }
|
||||
[ -d "${DIST_DIR}" ] || { mkdir -p "${DIST_DIR}" ; }
|
||||
|
||||
# generate CI summary file
|
||||
cd ${DIST_DIR}/
|
||||
|
||||
cat >${DIST_DIR}/ci_summary.html <<EOL
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<h1>CI Summary</h1>
|
||||
<h2>sha: $(git ls-files -s ${CASSANDRA_DIR} | git hash-object --stdin)</h2>
|
||||
<h2>branch: $(git -C ${CASSANDRA_DIR} branch --remote --verbose --no-abbrev --contains | sed -rne 's/^[^\/]*\/([^\ ]+).*$/\1/p')</h2>
|
||||
<h2>repo: $(git -C ${CASSANDRA_DIR} remote get-url origin)</h2>
|
||||
<h2>Date: $(date)</h2>
|
||||
</body>
|
||||
</html>
|
||||
...
|
||||
EOL
|
||||
|
||||
${CASSANDRA_DIR}/.build/ci/ci_parser.py --mute --input ${DIST_DIR}/test/output/ --output ${DIST_DIR}/ci_summary.html
|
||||
|
||||
exit $?
|
||||
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
#!/bin/sh -e
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
|
||||
#
|
||||
# Aggregates all test xml files into one and generates the junit html report.
|
||||
# see the 'generate-test-report' target in build.xml for more.
|
||||
# It is intended to be used to aggregate all splits on each test type/target,
|
||||
# before calling generate-ci-summary.sh to create the overview summary of
|
||||
# all test types and failures in a pipeline run.
|
||||
#
|
||||
|
||||
# variables, with defaults
|
||||
[ "x${CASSANDRA_DIR}" != "x" ] || CASSANDRA_DIR="$(readlink -f $(dirname "$0")/../..)"
|
||||
[ "x${DIST_DIR}" != "x" ] || DIST_DIR="${CASSANDRA_DIR}/build"
|
||||
|
||||
# pre-conditions
|
||||
command -v ant >/dev/null 2>&1 || { echo >&2 "ant needs to be installed"; exit 1; }
|
||||
[ -d "${CASSANDRA_DIR}" ] || { echo >&2 "Directory ${CASSANDRA_DIR} must exist"; exit 1; }
|
||||
[ -f "${CASSANDRA_DIR}/build.xml" ] || { echo >&2 "${CASSANDRA_DIR}/build.xml must exist"; exit 1; }
|
||||
[ -d "${DIST_DIR}" ] || { mkdir -p "${DIST_DIR}" ; }
|
||||
|
||||
# generate test xml summary file and html report directories
|
||||
ant -f "${CASSANDRA_DIR}/build.xml" generate-test-report
|
||||
exit $?
|
||||
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
#!/usr/bin/env python3
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
|
||||
|
||||
"""
|
||||
In-memory representations of JUnit test results and some helper methods to construct .html output
|
||||
based on those results
|
||||
"""
|
||||
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from enum import Enum
|
||||
from jinja2 import Template
|
||||
from typing import Any, Dict, Iterable, List, Set, Tuple
|
||||
|
||||
LOG_FILE_NAME = 'junit_parsing'
|
||||
logger = logging.getLogger(LOG_FILE_NAME)
|
||||
|
||||
|
||||
class JUnitTestStatus(Enum):
|
||||
label: str
|
||||
|
||||
"""
|
||||
Map to the string tag expected in the child element in junit output
|
||||
"""
|
||||
UNKNOWN = (0, 'unknown')
|
||||
PASSED = (1, 'passed')
|
||||
FAILURE = (2, 'failure')
|
||||
SKIPPED = (3, 'skipped')
|
||||
# Error and FAILURE are unfortunately used interchangeably by some of our suites, so we combine them on parsing
|
||||
ERROR = (4, 'error')
|
||||
|
||||
def __new__(cls, value, label) -> Any:
|
||||
obj = object.__new__(cls)
|
||||
obj._value_ = value
|
||||
obj.label = label
|
||||
return obj
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
@staticmethod
|
||||
def html_cell_order() -> Tuple:
|
||||
"""
|
||||
We won't have UNKNOWN, and ERROR is merged into FAILURE. This is our preferred order to represent things in html.
|
||||
"""
|
||||
return JUnitTestStatus.FAILURE, JUnitTestStatus.PASSED, JUnitTestStatus.SKIPPED
|
||||
|
||||
|
||||
class JUnitResultBuilder:
|
||||
"""
|
||||
Wraps up jinja templating for our junit based results. Manually doing this stuff was proving to be a total headache.
|
||||
That said, this didn't turn out to be a picnic on its own either. The particularity of .html parsing and this
|
||||
templating combined with BeautifulSoup means things are... very very particular. Bad parsing on things from the .sh
|
||||
or other sources can make bs4 replace things in weird ways.
|
||||
"""
|
||||
def __init__(self, name: str) -> None:
|
||||
self._name = name
|
||||
self._labels = [] # type: List[str]
|
||||
self._column_styles = [] # type: List[str]
|
||||
self._rows = [] # type: List[List[str]]
|
||||
self._header = ['unknown', 'unknown', 'unknown', 'unknown']
|
||||
|
||||
# Have to have the 4 <th> members since the stylesheet formats them based on position and it'll get all stupid
|
||||
# otherwise.
|
||||
self._template = Template('''
|
||||
<table class="table-fixed">
|
||||
<tr style = "height: 18px;">
|
||||
<th colspan="4"> {{header}} </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="{{ column_styles[0] }}">{{ labels[0] }}</td>
|
||||
<th style="{{ column_styles[1] }}">{{ labels[1] }}</td>
|
||||
<th style="{{ column_styles[2] }}">{{ labels[2] }}</td>
|
||||
<th style="{{ column_styles[3] }}">{{ labels[3] }}</td>
|
||||
</tr>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td style="{{ column_styles[0] }}">{{ row[0] }}</td>
|
||||
<td style="{{ column_styles[1] }}">{{ row[1] }}</td>
|
||||
<td style="{{ column_styles[2] }}">{{ row[2] }}</td>
|
||||
<td style="{{ column_styles[3] }}">{{ row[3] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
''')
|
||||
|
||||
@staticmethod
|
||||
def add_style_tags(soup: BeautifulSoup) -> None:
|
||||
"""
|
||||
We want to be opinionated about the width of our tables for our test suites; the messages dominate the output
|
||||
so we want to dedicate the largest amount of space to them and limit word-wrapping
|
||||
"""
|
||||
style_tag = soup.new_tag("style")
|
||||
style_tag.string = """
|
||||
table, tr {
|
||||
border: 1px solid black; border-collapse: collapse;
|
||||
}
|
||||
.table-fixed {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
"""
|
||||
soup.head.append(style_tag)
|
||||
|
||||
def label_columns(self, cols: List[str], column_styles: List[str]) -> None:
|
||||
if len(cols) != 4:
|
||||
raise AssertionError(f'Got invalid number of columns on label_columns: {len(cols)}. Expected: 4.')
|
||||
self._labels = cols
|
||||
self._column_styles = column_styles
|
||||
|
||||
def add_row(self, row: List[str]) -> None:
|
||||
if len(row) != 4:
|
||||
raise AssertionError(f'Got invalid number of columns on add_row: {len(row)}. Expected: 4.')
|
||||
self._rows.append(row)
|
||||
|
||||
def build_table(self) -> str:
|
||||
return self._template.render(header=f'{self._name}', labels=self._labels, column_styles=self._column_styles, rows=self._rows)
|
||||
|
||||
|
||||
class JUnitTestCase:
|
||||
"""
|
||||
Pretty straightforward in-memory representation of the state of a jUnit test. Not the _most_ tolerant of bad input,
|
||||
so don't test your luck.
|
||||
"""
|
||||
def __init__(self, testcase: ET.Element) -> None:
|
||||
"""
|
||||
From a given xml element, constructs a junit testcase. Doesn't do any sanity checking to make sure you gave
|
||||
it something correct, so... don't screw up.
|
||||
|
||||
Here's our general junit formatting:
|
||||
|
||||
<testcase classname = "org.apache.cassandra.index.sai.cql.VectorUpdateDeleteTest" name = "updateTest-_jdk11" time = "0.314">
|
||||
<failure message = "Result set does not contain a row with pk = 0" type = "junit.framework.AssertionFailedError">
|
||||
# The following is stored in failure.text:
|
||||
DETAILED ERROR MESSAGE / STACK TRACE
|
||||
DETAILED ERROR MESSAGE / STACK TRACE
|
||||
...
|
||||
</failure>
|
||||
</testcase>
|
||||
|
||||
And our skipped format:
|
||||
<testcase classname="org.apache.cassandra.distributed.test.PreviewRepairCoordinatorFastTest" name="snapshotFailure[PARALLEL/true]-_jdk11" time="0.218">
|
||||
<skipped message="Parallel repair does not perform snapshots" />
|
||||
</testcase>
|
||||
|
||||
Same for errors
|
||||
|
||||
We conflate the <error and <failure sub-elements in our junit failures and will need to combine those on processing.
|
||||
"""
|
||||
if testcase is None:
|
||||
raise AssertionError('Got an empty testcase; cannot create JUnitTestCase from nothing.')
|
||||
# No point in including the whole long o.a.c. thing. At least for now. This could bite us later if we end up with
|
||||
# other pathed test cases but /shrug
|
||||
self._class_name = testcase.get('classname', '').replace('org.apache.cassandra.', '') # type: str
|
||||
self._test_name = testcase.get('name', '') # type: str
|
||||
self._message = 'Passed' # type: str
|
||||
self._status = JUnitTestStatus.PASSED # type: JUnitTestStatus
|
||||
self._time = testcase.get('time', '') # type: str
|
||||
|
||||
# For the current set of tests, we don't have > 1 child tag indicating something went wrong. So we check to ensure
|
||||
# that remains true and will assert out if we hit something unexpected.
|
||||
saw_error = 0
|
||||
|
||||
def _check_for_child_element(failure_type: JUnitTestStatus) -> None:
|
||||
"""
|
||||
The presence of any failure, error, or skipped child elements indicated this test wasn't a normal 'pass'.
|
||||
We want to extract the message from the child if it has one as well as update the status of this object,
|
||||
including glomming together ERROR and FAILURE cases here. We combine those two as some legit test failures
|
||||
are reported as <error /> in the pytest cases.
|
||||
"""
|
||||
nonlocal testcase
|
||||
child = testcase.find(failure_type.label)
|
||||
if child is None:
|
||||
return
|
||||
|
||||
nonlocal saw_error
|
||||
if saw_error != 0:
|
||||
raise AssertionError(f'Got a test with > 1 "bad" state (error, failed, skipped). classname: {self._class_name}. test: {self._test_name}.')
|
||||
saw_error = 1
|
||||
|
||||
# We don't know if we're going to have message attribute data, text attribute, or text inside our tag. So
|
||||
# we just connect all three
|
||||
final_msg = '-'.join(filter(None, (child.get('message'), child.get('text'), child.text)))
|
||||
|
||||
self._message = final_msg
|
||||
if failure_type == JUnitTestStatus.ERROR or failure_type == JUnitTestStatus.FAILURE:
|
||||
self._status = JUnitTestStatus.FAILURE
|
||||
else:
|
||||
self._status = failure_type
|
||||
|
||||
_check_for_child_element(JUnitTestStatus.FAILURE)
|
||||
_check_for_child_element(JUnitTestStatus.ERROR)
|
||||
_check_for_child_element(JUnitTestStatus.SKIPPED)
|
||||
|
||||
def row_data(self) -> List[str]:
|
||||
return [self._class_name, self._test_name, f"<pre>{self._message}</pre>", str(self._time)]
|
||||
|
||||
def status(self) -> JUnitTestStatus:
|
||||
return self._status
|
||||
|
||||
def message(self) -> str:
|
||||
return self._message
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""
|
||||
We want to allow overwriting of existing combinations of class + test names, since our tarballs of results will
|
||||
have us doing potentially duplicate sequential processing of files and we just want to keep the most recent one.
|
||||
Of note, sorting the tarball contents and trying to navigate to and find the oldest and only process that was
|
||||
_significantly_ slower than just brute-force overwriting this way. Like... I gave up after 10 minutes vs. < 1 second.
|
||||
"""
|
||||
return hash((self._class_name, self._test_name))
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if isinstance(other, JUnitTestCase):
|
||||
return (self._class_name, self._test_name) == (other._class_name, other._test_name)
|
||||
return NotImplemented
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
We slice the message here; don't rely on this for anything where you need full reporting
|
||||
:return:
|
||||
"""
|
||||
clean_msg = self._message.replace('\n', ' ')
|
||||
return (f"JUnitTestCase(class_name='{self._class_name}', "
|
||||
f"name='{self._test_name}', msg='{clean_msg[:50]}', "
|
||||
f"time={self._time}, status={self._status.name})")
|
||||
|
||||
|
||||
class JUnitTestSuite:
|
||||
"""
|
||||
Straightforward container for a set of tests.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self._name = name # type: str
|
||||
self._suites = dict() # type: Dict[JUnitTestStatus, Set[JUnitTestCase]]
|
||||
self._files = set() # type: Set[str]
|
||||
# We only allow one archive to be associated with each JUnitTestSuite
|
||||
self._archive = 'unknown' # type: str
|
||||
for status in JUnitTestStatus:
|
||||
self._suites[status] = set()
|
||||
|
||||
def add_testcase(self, newcase: JUnitTestCase) -> None:
|
||||
"""
|
||||
Replaces if existing is found.
|
||||
"""
|
||||
if newcase.status() == JUnitTestStatus.UNKNOWN:
|
||||
raise AssertionError(f'Attempted to add a testcase with an unknown status: {newcase}. Aborting.')
|
||||
self._suites[newcase.status()].discard(newcase)
|
||||
self._suites[newcase.status()].add(newcase)
|
||||
|
||||
def get_tests(self, status: JUnitTestStatus) -> Iterable[JUnitTestCase]:
|
||||
"""
|
||||
Returns sorted list of tests, class name first then test name
|
||||
"""
|
||||
return sorted(self._suites[status], key=lambda x: (x._class_name, x._test_name))
|
||||
|
||||
def passed(self) -> int:
|
||||
return self.count(JUnitTestStatus.PASSED)
|
||||
|
||||
def failed(self) -> int:
|
||||
return self.count(JUnitTestStatus.FAILURE)
|
||||
|
||||
def skipped(self) -> int:
|
||||
return self.count(JUnitTestStatus.SKIPPED)
|
||||
|
||||
def count(self, status: JUnitTestStatus) -> int:
|
||||
return len(self._suites[status])
|
||||
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
return self.passed() == 0 and self.failed() == 0 and self.skipped() == 0
|
||||
|
||||
def set_archive(self, name: str) -> None:
|
||||
if self._archive != "unknown":
|
||||
msg = f'Attempted to set archive for suite: {self._name} when archive already set: {self._archive}. This is a bug.'
|
||||
logger.critical(msg)
|
||||
raise AssertionError(msg)
|
||||
self._archive = name
|
||||
|
||||
def get_archive(self) -> str:
|
||||
return self._archive
|
||||
|
||||
def add_file(self, name: str) -> None:
|
||||
# Just silently noop if we already have one since dupes in tarball indicate the same thing effectively. That we have it.
|
||||
self._files.add(name)
|
||||
|
||||
def file_count(self) -> int:
|
||||
"""
|
||||
Returns count of _unique_ files associated with this suite, not necessarily the _absolute_ count of files, since
|
||||
we don't bother keeping count of multiple instances of a .xml file in the tarball.
|
||||
:return:
|
||||
"""
|
||||
return len(self._files)
|
||||
|
||||
@staticmethod
|
||||
def headers() -> List[str]:
|
||||
result = ['Suite']
|
||||
for status in JUnitTestStatus.html_cell_order():
|
||||
result.append(status.name)
|
||||
return result
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
|
||||
export TEXT_RED="0;31"
|
||||
export TEXT_GREEN="0;32"
|
||||
export TEXT_LIGHTGREEN="1;32"
|
||||
export TEXT_BROWN="0;33"
|
||||
export TEXT_YELLOW="1;33"
|
||||
export TEXT_BLUE="0;34"
|
||||
export TEXT_LIGHTBLUE="1;34"
|
||||
export TEXT_PURPLE="0;35"
|
||||
export TEXT_LIGHTPURPLE="1;35"
|
||||
export TEXT_CYAN="0;36"
|
||||
export TEXT_LIGHTCYAN="1;36"
|
||||
export TEXT_LIGHTGRAY="0;37"
|
||||
export TEXT_WHITE="1;37"
|
||||
export TEXT_DARKGRAY="1;30"
|
||||
export TEXT_LIGHTRED="1;31"
|
||||
|
||||
export SILENCE_LOGGING=false
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
|
||||
disable_logging() {
|
||||
export SILENCE_LOGGING=true
|
||||
}
|
||||
|
||||
enable_logging() {
|
||||
export SILENCE_LOGGING=false
|
||||
}
|
||||
|
||||
echo_color() {
|
||||
if [[ $LOG_TO_FILE == "true" ]]; then
|
||||
echo "$1"
|
||||
elif [[ $SILENCE_LOGGING != "true" ]]; then
|
||||
echo -e "\033[1;${2}m${1}\033[0m"
|
||||
fi
|
||||
}
|
||||
|
||||
log_header() {
|
||||
if [[ $SILENCE_LOGGING != "true" ]]; then
|
||||
log_separator
|
||||
echo_color "$1" $TEXT_GREEN
|
||||
log_separator
|
||||
fi
|
||||
}
|
||||
|
||||
log_progress() {
|
||||
if [[ $SILENCE_LOGGING != "true" ]]; then
|
||||
echo_color "$1" $TEXT_LIGHTCYAN
|
||||
fi
|
||||
}
|
||||
|
||||
log_info() {
|
||||
if [[ $SILENCE_LOGGING != "true" ]]; then
|
||||
echo_color "$1" $TEXT_LIGHTGRAY
|
||||
fi
|
||||
}
|
||||
|
||||
log_quiet() {
|
||||
if [[ $SILENCE_LOGGING != "true" ]]; then
|
||||
echo_color "$1" $TEXT_DARKGRAY
|
||||
fi
|
||||
}
|
||||
|
||||
# For transient always-on debugging
|
||||
log_transient() {
|
||||
if [[ $SILENCE_LOGGING != "true" ]]; then
|
||||
echo_color "[TRANSIENT]: $1" $TEXT_BROWN
|
||||
fi
|
||||
}
|
||||
|
||||
# For durable user-selectable debugging
|
||||
log_debug() {
|
||||
if [[ "$SILENCE_LOGGING" = "true" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${DEBUG:-false}" == true || "${DEBUG_LOGGING:-false}" == true ]]; then
|
||||
echo_color "[DEBUG] $1" $TEXT_PURPLE
|
||||
fi
|
||||
}
|
||||
|
||||
log_quiet() {
|
||||
if [[ $SILENCE_LOGGING != "true" ]]; then
|
||||
echo_color "$1" $TEXT_LIGHTGRAY
|
||||
fi
|
||||
}
|
||||
|
||||
log_todo() {
|
||||
if [[ $SILENCE_LOGGING != "true" ]]; then
|
||||
echo_color "TODO: $1" $TEXT_LIGHTPURPLE
|
||||
fi
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
if [[ $SILENCE_LOGGING != "true" ]]; then
|
||||
echo_color "WARNING: $1" $TEXT_YELLOW
|
||||
fi
|
||||
}
|
||||
|
||||
log_error() {
|
||||
if [[ $SILENCE_LOGGING != "true" ]]; then
|
||||
echo_color "ERROR: $1" $TEXT_RED
|
||||
fi
|
||||
}
|
||||
|
||||
log_separator() {
|
||||
if [[ $SILENCE_LOGGING != "true" ]]; then
|
||||
echo_color "--------------------------------------------" $TEXT_GREEN
|
||||
fi
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env python3
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
|
||||
|
||||
"""
|
||||
We want to add a little functionality on top of built-in logging; colorization, some new log levels, and logging
|
||||
to a file built-in as well as some other conveniences.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from enum import Enum
|
||||
from logging import Logger
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
|
||||
PROGRESS_LEVEL_NUM = 25
|
||||
SECTION_LEVEL_NUM = 26
|
||||
logging.addLevelName(PROGRESS_LEVEL_NUM, 'PROGRESS')
|
||||
logging.addLevelName(SECTION_LEVEL_NUM, 'SECTION')
|
||||
|
||||
|
||||
class LogLevel(Enum):
|
||||
"""
|
||||
Matches logging.<VALUE> int levels; wrapped in enum here for convenience
|
||||
"""
|
||||
CRITICAL = 50
|
||||
FATAL = CRITICAL
|
||||
ERROR = 40
|
||||
WARNING = 30
|
||||
WARN = WARNING
|
||||
INFO = 20
|
||||
DEBUG = 10
|
||||
NOTSET = 0
|
||||
|
||||
|
||||
class CustomLogger(Logger):
|
||||
# Some decorations to match the paradigm used in some other .sh files
|
||||
def progress(self, message: str, *args, **kws) -> None:
|
||||
if self.isEnabledFor(PROGRESS_LEVEL_NUM):
|
||||
self._log(PROGRESS_LEVEL_NUM, message, args, **kws)
|
||||
|
||||
def separator(self, *args, **kws) -> None:
|
||||
if self.isEnabledFor(logging.DEBUG) and self.isEnabledFor(SECTION_LEVEL_NUM):
|
||||
self._log(SECTION_LEVEL_NUM, '-----------------------------------------------------------------------------', args, **kws)
|
||||
|
||||
def header(self, message: str, *args, **kws) -> None:
|
||||
if self.isEnabledFor(logging.DEBUG) and self.isEnabledFor(SECTION_LEVEL_NUM):
|
||||
self._log(SECTION_LEVEL_NUM, f'----[{message}]----', args, **kws)
|
||||
|
||||
|
||||
logging.setLoggerClass(CustomLogger)
|
||||
LOG_FORMAT_STRING = '%(asctime)s - [tid:%(threadid)s] - [%(levelname)s]::%(message)s'
|
||||
|
||||
|
||||
def build_logger(name: str, verbose: bool) -> CustomLogger:
|
||||
logger = CustomLogger(name)
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.addFilter(ThreadContextFilter())
|
||||
|
||||
stdout_handler = logging.StreamHandler()
|
||||
file_handler = RotatingFileHandler(f'{name}.log')
|
||||
|
||||
formatter = CustomFormatter()
|
||||
stdout_handler.setFormatter(formatter)
|
||||
# Don't want color escape characters in our file logging, so we just use the string rather than the whole formatter
|
||||
file_handler.setFormatter(logging.Formatter(LOG_FORMAT_STRING))
|
||||
|
||||
logger.addHandler(stdout_handler)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
if verbose:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Prevent root logger propagation from duplicating results
|
||||
logger.propagate = False
|
||||
return logger
|
||||
|
||||
|
||||
def set_loglevel(logger: logging.Logger, level: LogLevel) -> None:
|
||||
if logger.handlers:
|
||||
for handler in logger.handlers:
|
||||
handler.setLevel(level.value)
|
||||
|
||||
|
||||
def mute_logging(logger: logging.Logger) -> None:
|
||||
if logger.handlers:
|
||||
for handler in logger.handlers:
|
||||
handler.setLevel(logging.CRITICAL + 1)
|
||||
|
||||
|
||||
# Since we'll thread, let's point out threadid in our format
|
||||
class ThreadContextFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
record.threadid = threading.get_ident()
|
||||
return True
|
||||
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
grey = "\x1b[38;21m"
|
||||
blue = "\x1b[34;21m"
|
||||
green = "\x1b[32;21m"
|
||||
yellow = "\x1b[33;21m"
|
||||
red = "\x1b[31;21m"
|
||||
bold_red = "\x1b[31;1m"
|
||||
reset = "\x1b[0m"
|
||||
purple = "\x1b[35m"
|
||||
|
||||
FORMATS = {
|
||||
PROGRESS_LEVEL_NUM: blue + LOG_FORMAT_STRING + reset,
|
||||
SECTION_LEVEL_NUM: green + LOG_FORMAT_STRING + reset,
|
||||
logging.DEBUG: purple + LOG_FORMAT_STRING + reset,
|
||||
logging.INFO: grey + LOG_FORMAT_STRING + reset,
|
||||
logging.WARNING: yellow + LOG_FORMAT_STRING + reset,
|
||||
logging.ERROR: red + LOG_FORMAT_STRING + reset,
|
||||
logging.CRITICAL: bold_red + LOG_FORMAT_STRING + reset
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno, self.format)
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/bash
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
|
||||
|
||||
source "logging.sh"
|
||||
|
||||
skip_mypy=(
|
||||
"./logging_helper.py"
|
||||
)
|
||||
|
||||
failed=0
|
||||
log_progress "Linting ci_parser..."
|
||||
for i in `find . -maxdepth 1 -name "*.py"`; do
|
||||
log_progress "Checking $i..."
|
||||
flake8 "$i"
|
||||
if [[ $? != 0 ]]; then
|
||||
failed=1
|
||||
fi
|
||||
|
||||
if [[ ! " ${skip_mypy[*]} " =~ ${i} ]]; then
|
||||
mypy --ignore-missing-imports "$i"
|
||||
if [[ $? != 0 ]]; then
|
||||
failed=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
if [[ $failed -eq 1 ]]; then
|
||||
log_error "Failed linting. See above errors; don't merge until clean."
|
||||
exit 1
|
||||
else
|
||||
log_progress "All scripts passed checks"
|
||||
exit 0
|
||||
fi
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
|
||||
#
|
||||
# Changes to this file must also be put into
|
||||
|
||||
beautifulsoup4==4.12.3
|
||||
jinja2==3.1.3
|
||||
|
|
@ -26,10 +26,14 @@
|
|||
#
|
||||
################################
|
||||
|
||||
[ $DEBUG ] && set -x
|
||||
|
||||
# variables, with defaults
|
||||
[ "x${cassandra_dir}" != "x" ] || cassandra_dir="$(readlink -f $(dirname "$0")/../..)"
|
||||
[ "x${build_dir}" != "x" ] || build_dir="${cassandra_dir}/build"
|
||||
[ "x${m2_dir}" != "x" ] || m2_dir="${HOME}/.m2/repository"
|
||||
[ -d "${build_dir}" ] || { mkdir -p "${build_dir}" ; }
|
||||
[ -d "${m2_dir}" ] || { mkdir -p "${m2_dir}" ; }
|
||||
|
||||
java_version_default=`grep 'property\s*name="java.default"' ${cassandra_dir}/build.xml |sed -ne 's/.*value="\([^"]*\)".*/\1/p'`
|
||||
java_version_supported=`grep 'property\s*name="java.supported"' ${cassandra_dir}/build.xml |sed -ne 's/.*value="\([^"]*\)".*/\1/p'`
|
||||
|
|
@ -118,11 +122,11 @@ docker_command="export ANT_OPTS=\"-Dbuild.dir=\${DIST_DIR} ${CASSANDRA_DOCKER_AN
|
|||
# run without the default seccomp profile
|
||||
# re-use the host's maven repository
|
||||
container_id=$(docker run --name ${container_name} -d --security-opt seccomp=unconfined --rm \
|
||||
-v "${cassandra_dir}":/home/build/cassandra -v ~/.m2/repository/:/home/build/.m2/repository/ -v "${build_dir}":/dist \
|
||||
-v "${cassandra_dir}":/home/build/cassandra -v ${m2_dir}:/home/build/.m2/repository/ -v "${build_dir}":/dist \
|
||||
${docker_volume_opt} \
|
||||
${image_name} sleep 1h)
|
||||
|
||||
echo "Running container ${container_name} ${container_id}"
|
||||
echo "Running container ${container_name} ${container_id} using image ${image_name}"
|
||||
|
||||
docker exec --user root ${container_name} bash -c "\${CASSANDRA_DIR}/.build/docker/_create_user.sh build $(id -u) $(id -g)"
|
||||
docker exec --user build ${container_name} bash -c "${docker_command}"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/bash
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
|
||||
#
|
||||
# Build the jars
|
||||
|
||||
$(dirname "$0")/_docker_run.sh bullseye-build.docker build-jars.sh $1
|
||||
exit $?
|
||||
|
|
@ -51,3 +51,6 @@ RUN update-java-alternatives --set java-1.11.0-openjdk-$(dpkg --print-architectu
|
|||
|
||||
# python3 is needed for the gen-doc target
|
||||
RUN pip install --upgrade pip
|
||||
|
||||
# dependencies for .build/ci/ci_parser.py
|
||||
RUN pip install beautifulsoup4==4.12.3 jinja2==3.1.3
|
||||
|
|
|
|||
|
|
@ -15,14 +15,10 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#
|
||||
#
|
||||
# A wrapper script to run-tests.sh (or dtest-python.sh) in docker.
|
||||
# Can split (or grep) the test list into multiple docker runs, collecting results.
|
||||
#
|
||||
# Each split chunk may be further parallelised over docker containers based on the host's available cpu and memory (and the test type).
|
||||
# Define env variable DISABLE_INNER_SPLITS to disable inner splitting.
|
||||
#
|
||||
|
||||
[ $DEBUG ] && set -x
|
||||
|
||||
# help
|
||||
if [ "$#" -lt 1 ] || [ "$#" -gt 3 ] || [ "$1" == "-h" ]; then
|
||||
|
|
@ -38,7 +34,9 @@ fi
|
|||
[ "x${cassandra_dir}" != "x" ] || cassandra_dir="$(readlink -f $(dirname "$0")/../..)"
|
||||
[ "x${cassandra_dtest_dir}" != "x" ] || cassandra_dtest_dir="${cassandra_dir}/../cassandra-dtest"
|
||||
[ "x${build_dir}" != "x" ] || build_dir="${cassandra_dir}/build"
|
||||
[ "x${m2_dir}" != "x" ] || m2_dir="${HOME}/.m2/repository"
|
||||
[ -d "${build_dir}" ] || { mkdir -p "${build_dir}" ; }
|
||||
[ -d "${m2_dir}" ] || { mkdir -p "${m2_dir}" ; }
|
||||
|
||||
# pre-conditions
|
||||
command -v docker >/dev/null 2>&1 || { echo >&2 "docker needs to be installed"; exit 1; }
|
||||
|
|
@ -84,21 +82,30 @@ pushd ${cassandra_dir}/.build >/dev/null
|
|||
dockerfile="ubuntu2004_test.docker"
|
||||
image_tag="$(md5sum docker/${dockerfile} | cut -d' ' -f1)"
|
||||
image_name="apache/cassandra-${dockerfile/.docker/}:${image_tag}"
|
||||
docker_mounts="-v ${cassandra_dir}:/home/cassandra/cassandra -v "${build_dir}":/home/cassandra/cassandra/build -v ${HOME}/.m2/repository:/home/cassandra/.m2/repository"
|
||||
docker_mounts="-v ${cassandra_dir}:/home/cassandra/cassandra -v "${build_dir}":/home/cassandra/cassandra/build -v ${m2_dir}:/home/cassandra/.m2/repository"
|
||||
# HACK hardlinks in overlay are buggy, the following mount prevents hardlinks from being used. ref $TMP_DIR in .build/run-tests.sh
|
||||
docker_mounts="${docker_mounts} -v "${build_dir}/tmp":/home/cassandra/cassandra/build/tmp"
|
||||
|
||||
# Look for existing docker image, otherwise build
|
||||
if ! ( [[ "$(docker images -q ${image_name} 2>/dev/null)" != "" ]] ) ; then
|
||||
echo "Build image not found locally"
|
||||
# try docker login to increase dockerhub rate limits
|
||||
echo "Attempting 'docker login' to increase dockerhub rate limits"
|
||||
timeout -k 5 5 docker login >/dev/null 2>/dev/null
|
||||
echo "Pulling build image..."
|
||||
if ! ( docker pull -q ${image_name} >/dev/null 2>/dev/null ) ; then
|
||||
# Create build images containing the build tool-chain, Java and an Apache Cassandra git working directory, with retry
|
||||
echo "Building docker image ${image_name}..."
|
||||
until docker build -t ${image_name} -f docker/${dockerfile} . ; do
|
||||
echo "docker build failed… trying again in 10s… "
|
||||
sleep 10
|
||||
done
|
||||
echo "Docker image ${image_name} has been built"
|
||||
else
|
||||
echo "Successfully pulled build image."
|
||||
fi
|
||||
else
|
||||
echo "Found build image locally."
|
||||
fi
|
||||
|
||||
pushd ${cassandra_dir} >/dev/null
|
||||
|
|
@ -112,30 +119,41 @@ if [[ ! -z ${JENKINS_URL+x} ]] && [[ ! -z ${NODE_NAME+x} ]] ; then
|
|||
fi
|
||||
|
||||
# find host's available cores and mem
|
||||
cores=1
|
||||
command -v nproc >/dev/null 2>&1 && cores=$(nproc --all)
|
||||
mem=1
|
||||
# linux
|
||||
command -v free >/dev/null 2>&1 && mem=$(free -b | grep Mem: | awk '{print $2}')
|
||||
# macos
|
||||
sysctl -n hw.memsize >/dev/null 2>&1 && mem=$(sysctl -n hw.memsize)
|
||||
cores=$(docker run --rm alpine nproc --all) || { echo >&2 "Unable to check available CPU cores"; exit 1; }
|
||||
|
||||
case $(uname) in
|
||||
"Linux")
|
||||
mem=$(docker run --rm alpine free -b | grep Mem: | awk '{print $2}') || { echo >&2 "Unable to check available memory"; exit 1; }
|
||||
;;
|
||||
"Darwin")
|
||||
mem=$(sysctl -n hw.memsize) || { echo >&2 "Unable to check available memory"; exit 1; }
|
||||
;;
|
||||
*)
|
||||
echo >&2 "Unsupported operating system, expected Linux or Darwin"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
# figure out resource limits, scripts, and mounts for the test type
|
||||
case ${target} in
|
||||
# test-burn doesn't have enough tests in it to split beyond 8, and burn and long we want a bit more resources anyway
|
||||
"stress-test" | "fqltool-test" | "microbench" | "test-burn" | "long-test" | "cqlsh-test" )
|
||||
[[ ${mem} -gt $((5 * 1024 * 1024 * 1024 * ${jenkins_executors})) ]] || { echo >&2 "tests require minimum docker memory 6g (per jenkins executor (${jenkins_executors})), found ${mem}"; exit 1; }
|
||||
"build_dtest_jars")
|
||||
;;
|
||||
"dtest" | "dtest-novnode" | "dtest-offheap" | "dtest-large" | "dtest-large-novnode" | "dtest-upgrade" | "dtest-upgrade-novnode"| "dtest-upgrade-large" | "dtest-upgrade-novnode-large" )
|
||||
"stress-test" | "fqltool-test" )
|
||||
[[ ${mem} -gt $((1 * 1024 * 1024 * 1024 * ${jenkins_executors})) ]] || { echo >&2 "${target} require minimum docker memory 1g (per jenkins executor (${jenkins_executors})), found ${mem}"; exit 1; }
|
||||
;;
|
||||
# test-burn doesn't have enough tests in it to split beyond 8, and burn and long we want a bit more resources anyway
|
||||
"microbench" | "test-burn" | "long-test" | "cqlsh-test" )
|
||||
[[ ${mem} -gt $((5 * 1024 * 1024 * 1024 * ${jenkins_executors})) ]] || { echo >&2 "${target} require minimum docker memory 6g (per jenkins executor (${jenkins_executors})), found ${mem}"; exit 1; }
|
||||
;;
|
||||
"dtest" | "dtest-novnode" | "dtest-latest" | "dtest-large" | "dtest-large-novnode" | "dtest-upgrade" | "dtest-upgrade-novnode"| "dtest-upgrade-large" | "dtest-upgrade-novnode-large" )
|
||||
[ -f "${cassandra_dtest_dir}/dtest.py" ] || { echo >&2 "${cassandra_dtest_dir}/dtest.py must exist"; exit 1; }
|
||||
[[ ${mem} -gt $((15 * 1024 * 1024 * 1024 * ${jenkins_executors})) ]] || { echo >&2 "dtests require minimum docker memory 16g (per jenkins executor (${jenkins_executors})), found ${mem}"; exit 1; }
|
||||
[[ ${mem} -gt $((15 * 1024 * 1024 * 1024 * ${jenkins_executors})) ]] || { echo >&2 "${target} require minimum docker memory 16g (per jenkins executor (${jenkins_executors})), found ${mem}"; exit 1; }
|
||||
test_script="run-python-dtests.sh"
|
||||
docker_mounts="${docker_mounts} -v ${cassandra_dtest_dir}:/home/cassandra/cassandra-dtest"
|
||||
# check that ${cassandra_dtest_dir} is valid
|
||||
[ -f "${cassandra_dtest_dir}/dtest.py" ] || { echo >&2 "${cassandra_dtest_dir}/dtest.py not found. please specify 'cassandra_dtest_dir' to point to the local cassandra-dtest source"; exit 1; }
|
||||
;;
|
||||
"test"| "test-cdc" | "test-compression" | "test-oa" | "test-system-keyspace-directory" | "test-latest" | "jvm-dtest" | "jvm-dtest-upgrade" | "jvm-dtest-novnode" | "jvm-dtest-upgrade-novnode" | "simulator-dtest")
|
||||
[[ ${mem} -gt $((5 * 1024 * 1024 * 1024 * ${jenkins_executors})) ]] || { echo >&2 "tests require minimum docker memory 6g (per jenkins executor (${jenkins_executors})), found ${mem}"; exit 1; }
|
||||
[[ ${mem} -gt $((5 * 1024 * 1024 * 1024 * ${jenkins_executors})) ]] || { echo >&2 "${target} require minimum docker memory 6g (per jenkins executor (${jenkins_executors})), found ${mem}"; exit 1; }
|
||||
max_docker_runs_by_cores=$( echo "sqrt( ${cores} / ${jenkins_executors} )" | bc )
|
||||
max_docker_runs_by_mem=$(( ${mem} / ( 5 * 1024 * 1024 * 1024 * ${jenkins_executors} ) ))
|
||||
;;
|
||||
|
|
@ -164,14 +182,15 @@ fi
|
|||
docker_flags="${docker_flags} -d --rm"
|
||||
|
||||
# make sure build_dir is good
|
||||
mkdir -p ${build_dir}/tmp || true
|
||||
mkdir -p ${build_dir}/test/logs || true
|
||||
mkdir -p ${build_dir}/test/output || true
|
||||
chmod -R ag+rwx ${build_dir}
|
||||
mkdir -p "${build_dir}/tmp" || true
|
||||
mkdir -p "${build_dir}/test/logs" || true
|
||||
mkdir -p "${build_dir}/test/output" || true
|
||||
mkdir -p "${build_dir}/test/reports" || true
|
||||
chmod -R ag+rwx "${build_dir}"
|
||||
|
||||
# define testtag.extra so tests can be aggregated together. (jdk is already appended in build.xml)
|
||||
case ${target} in
|
||||
"cqlsh-test" | "dtest" | "dtest-novnode" | "dtest-offheap" | "dtest-large" | "dtest-large-novnode" | "dtest-upgrade" | "dtest-upgrade-large" | "dtest-upgrade-novnode" | "dtest-upgrade-novnode-large" )
|
||||
case "${target}" in
|
||||
"cqlsh-test" | "dtest" | "dtest-novnode" | "dtest-latest" | "dtest-large" | "dtest-large-novnode" | "dtest-upgrade" | "dtest-upgrade-large" | "dtest-upgrade-novnode" | "dtest-upgrade-novnode-large" )
|
||||
ANT_OPTS="-Dtesttag.extra=_$(arch)_python${python_version/./-}"
|
||||
;;
|
||||
"jvm-dtest-novnode" | "jvm-dtest-upgrade-novnode" )
|
||||
|
|
@ -224,9 +243,11 @@ echo "Running container ${container_name} ${docker_id}"
|
|||
docker exec --user root ${container_name} bash -c "\${CASSANDRA_DIR}/.build/docker/_create_user.sh cassandra $(id -u) $(id -g)" | tee -a ${logfile}
|
||||
docker exec --user root ${container_name} update-alternatives --set python /usr/bin/python${python_version} | tee -a ${logfile}
|
||||
|
||||
# capture logs and pid for container
|
||||
# capture logs and status
|
||||
set -o pipefail
|
||||
docker exec --user cassandra ${container_name} bash -c "${docker_command}" | tee -a ${logfile}
|
||||
status=$?
|
||||
set +o pipefail
|
||||
|
||||
if [ "$status" -ne 0 ] ; then
|
||||
echo "${docker_id} failed (${status}), debug…"
|
||||
|
|
|
|||
|
|
@ -117,8 +117,8 @@ if [ "${DTEST_TARGET}" = "dtest" ]; then
|
|||
DTEST_ARGS="--use-vnodes --num-tokens=${NUM_TOKENS} --skip-resource-intensive-tests"
|
||||
elif [ "${DTEST_TARGET}" = "dtest-novnode" ]; then
|
||||
DTEST_ARGS="--skip-resource-intensive-tests --keep-failed-test-dir"
|
||||
elif [ "${DTEST_TARGET}" = "dtest-offheap" ]; then
|
||||
DTEST_ARGS="--use-vnodes --num-tokens=${NUM_TOKENS} --use-off-heap-memtables --skip-resource-intensive-tests"
|
||||
elif [ "${DTEST_TARGET}" = "dtest-latest" ]; then
|
||||
DTEST_ARGS="--use-vnodes --num-tokens=${NUM_TOKENS} --configuration-yaml=cassandra_latest.yaml --skip-resource-intensive-tests"
|
||||
elif [ "${DTEST_TARGET}" = "dtest-large" ]; then
|
||||
DTEST_ARGS="--use-vnodes --num-tokens=${NUM_TOKENS} --only-resource-intensive-tests --force-resource-intensive-tests"
|
||||
elif [ "${DTEST_TARGET}" = "dtest-large-novnode" ]; then
|
||||
|
|
@ -145,6 +145,7 @@ if [[ "${DTEST_SPLIT_CHUNK}" =~ ^[0-9]+/[0-9]+$ ]]; then
|
|||
( split --help 2>&1 ) | grep -q "r/K/N" || split_cmd=gsplit
|
||||
command -v ${split_cmd} >/dev/null 2>&1 || { echo >&2 "${split_cmd} needs to be installed"; exit 1; }
|
||||
SPLIT_TESTS=$(${split_cmd} -n r/${DTEST_SPLIT_CHUNK} ${DIST_DIR}/test_list.txt)
|
||||
SPLIT_STRING="_${DTEST_SPLIT_CHUNK//\//_}"
|
||||
elif [[ "x" != "x${DTEST_SPLIT_CHUNK}" ]] ; then
|
||||
SPLIT_TESTS=$(grep -e "${DTEST_SPLIT_CHUNK}" ${DIST_DIR}/test_list.txt)
|
||||
[[ "x" != "x${SPLIT_TESTS}" ]] || { echo "no tests match regexp \"${DTEST_SPLIT_CHUNK}\""; exit 1; }
|
||||
|
|
@ -152,9 +153,10 @@ else
|
|||
SPLIT_TESTS=$(cat ${DIST_DIR}/test_list.txt)
|
||||
fi
|
||||
|
||||
pytest_results_file="${DIST_DIR}/test/output/nosetests.xml"
|
||||
pytest_opts="-vv --log-cli-level=DEBUG --junit-xml=${pytest_results_file} --junit-prefix=${DTEST_TARGET} -s"
|
||||
|
||||
PYTEST_OPTS="-vv --log-cli-level=DEBUG --junit-xml=${DIST_DIR}/test/output/nosetests.xml --junit-prefix=${DTEST_TARGET} -s"
|
||||
pytest ${PYTEST_OPTS} --cassandra-dir=${CASSANDRA_DIR} --keep-failed-test-dir ${DTEST_ARGS} ${SPLIT_TESTS} 2>&1 | tee -a ${DIST_DIR}/test_stdout.txt
|
||||
pytest ${pytest_opts} --cassandra-dir=${CASSANDRA_DIR} --keep-failed-test-dir ${DTEST_ARGS} ${SPLIT_TESTS} 2>&1 | tee -a ${DIST_DIR}/test_stdout.txt
|
||||
|
||||
# tar up any ccm logs for easy retrieval
|
||||
if ls ${TMPDIR}/*/test/*/logs/* &>/dev/null ; then
|
||||
|
|
@ -164,10 +166,13 @@ fi
|
|||
|
||||
# merge all unit xml files into one, and print summary test numbers
|
||||
pushd ${CASSANDRA_DIR}/ >/dev/null
|
||||
# remove <testsuites> wrapping elements. `ant generate-unified-test-report` doesn't like it`
|
||||
sed -r "s/<[\/]?testsuites>//g" ${DIST_DIR}/test/output/nosetests.xml > ${TMPDIR}/nosetests.xml
|
||||
cat ${TMPDIR}/nosetests.xml > ${DIST_DIR}/test/output/nosetests.xml
|
||||
ant -quiet -silent generate-unified-test-report
|
||||
# remove <testsuites> wrapping elements. ant generate-test-report` doesn't like it, and update testsuite name
|
||||
sed -r "s/<[\/]?testsuites>//g" ${pytest_results_file} > ${TMPDIR}/nosetests.xml
|
||||
cat ${TMPDIR}/nosetests.xml > ${pytest_results_file}
|
||||
sed "s/testsuite name=\"Cassandra dtests\"/testsuite name=\"${DTEST_TARGET}_jdk${java_version}_python${python_version}_cython${cython}_$(uname -m)${SPLIT_STRING}\"/g" ${pytest_results_file} > ${TMPDIR}/nosetests.xml
|
||||
cat ${TMPDIR}/nosetests.xml > ${pytest_results_file}
|
||||
|
||||
ant -quiet -silent generate-test-report
|
||||
popd >/dev/null
|
||||
|
||||
################################
|
||||
|
|
|
|||
|
|
@ -180,13 +180,14 @@ _main() {
|
|||
# ant test setup
|
||||
export TMP_DIR="${DIST_DIR}/tmp"
|
||||
[ -d ${TMP_DIR} ] || mkdir -p "${TMP_DIR}"
|
||||
export ANT_TEST_OPTS="-Dno-build-test=true -Dtmp.dir=${TMP_DIR}"
|
||||
export ANT_TEST_OPTS="-Dno-build-test=true -Dtmp.dir=${TMP_DIR} -Dbuild.test.output.dir=${DIST_DIR}/test/output/${target}"
|
||||
|
||||
# fresh virtualenv and test logs results everytime
|
||||
[[ "/" == "${DIST_DIR}" ]] || rm -rf "${DIST_DIR}/test/{html,output,logs}"
|
||||
[[ "/" == "${DIST_DIR}" ]] || rm -rf "${DIST_DIR}/test/{html,output,logs,reports}"
|
||||
|
||||
# cheap trick to ensure dependency libraries are in place. allows us to stash only project specific build artifacts.
|
||||
ant -quiet -silent resolver-dist-lib
|
||||
# also recreate some of the non-build files we need
|
||||
ant -quiet -silent resolver-dist-lib _createVersionPropFile
|
||||
|
||||
case ${target} in
|
||||
"stress-test")
|
||||
|
|
@ -240,6 +241,9 @@ _main() {
|
|||
fi
|
||||
ant testclasslist -Dtest.classlistprefix=distributed -Dtest.timeout=$(_timeout_for "test.distributed.timeout") -Dtest.classlistfile=<(echo "${testlist}") ${ANT_TEST_OPTS} || echo "failed ${target} ${split_chunk}"
|
||||
;;
|
||||
"build_dtest_jars")
|
||||
_build_all_dtest_jars
|
||||
;;
|
||||
"jvm-dtest-upgrade" | "jvm-dtest-upgrade-novnode")
|
||||
_build_all_dtest_jars
|
||||
[ "jvm-dtest-upgrade-novnode" == "${target}" ] || ANT_TEST_OPTS="${ANT_TEST_OPTS} -Dcassandra.dtest.num_tokens=16"
|
||||
|
|
@ -262,7 +266,7 @@ _main() {
|
|||
esac
|
||||
|
||||
# merge all unit xml files into one, and print summary test numbers
|
||||
ant -quiet -silent generate-unified-test-report
|
||||
ant -quiet -silent generate-test-report
|
||||
|
||||
popd >/dev/null
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,4 +1,5 @@
|
|||
5.0-beta2
|
||||
* Fix FBUtilities' parsing of gcp cos_containerd kernel versions (CASSANDRA-18594)
|
||||
* Clean up KeyRangeIterator classes (CASSANDRA-19428)
|
||||
* Warn clients about possible consistency violations for filtering queries against multiple mutable columns (CASSANDRA-19489)
|
||||
* Align buffer with commitlog segment size (CASSANDRA-19471)
|
||||
|
|
|
|||
73
build.xml
73
build.xml
|
|
@ -59,6 +59,8 @@
|
|||
<property name="build.dir" value="${basedir}/build"/>
|
||||
<property name="build.dir.lib" value="${build.dir}/lib"/>
|
||||
<property name="build.test.dir" value="${build.dir}/test"/>
|
||||
<property name="build.test.output.dir" value="${build.test.dir}/output"/>
|
||||
<property name="build.test.report.dir" value="${build.test.dir}/reports" />
|
||||
<property name="build.classes" value="${build.dir}/classes"/>
|
||||
<property name="build.classes.main" value="${build.classes}/main" />
|
||||
<property name="javadoc.dir" value="${build.dir}/javadoc"/>
|
||||
|
|
@ -603,8 +605,8 @@
|
|||
-->
|
||||
<target name="stress-test-some" depends="maybe-build-test" description="Runs stress tests">
|
||||
<testmacro inputdir="${stress.test.src}" timeout="${test.timeout}">
|
||||
<test unless:blank="${test.methods}" name="${test.name}" methods="${test.methods}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if:blank="${test.methods}" name="${test.name}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}"/>
|
||||
<test unless:blank="${test.methods}" name="${test.name}" methods="${test.methods}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if:blank="${test.methods}" name="${test.name}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}"/>
|
||||
</testmacro>
|
||||
</target>
|
||||
|
||||
|
|
@ -1142,8 +1144,8 @@
|
|||
</classpath>
|
||||
</taskdef>
|
||||
<mkdir dir="${build.test.dir}/cassandra"/>
|
||||
<mkdir dir="${build.test.dir}/output"/>
|
||||
<mkdir dir="${build.test.dir}/output/@{testtag}"/>
|
||||
<mkdir dir="${build.test.output.dir}"/>
|
||||
<mkdir dir="${build.test.output.dir}/@{testtag}"/>
|
||||
<mkdir dir="${tmp.dir}"/>
|
||||
<junit-timeout fork="on" forkmode="@{forkmode}" failureproperty="testfailed" maxmemory="1024m" timeout="@{timeout}" showoutput="@{showoutput}">
|
||||
<formatter classname="org.apache.cassandra.CassandraXMLJUnitResultFormatter" extension=".xml" usefile="true"/>
|
||||
|
|
@ -1206,7 +1208,7 @@
|
|||
<exclude name="**/ant-*.jar"/>
|
||||
</fileset>
|
||||
</classpath>
|
||||
<batchtest todir="${build.test.dir}/output/@{testtag}">
|
||||
<batchtest todir="${build.test.output.dir}/@{testtag}">
|
||||
<fileset dir="@{inputdir}" includes="@{filter}" excludes="@{exclude}"/>
|
||||
<filelist dir="@{inputdir}" files="@{filelist}"/>
|
||||
</batchtest>
|
||||
|
|
@ -1375,8 +1377,8 @@
|
|||
-->
|
||||
<target name="testsome" depends="maybe-build-test" description="Execute specific unit tests" >
|
||||
<testmacro inputdir="${test.unit.src}" timeout="${test.timeout}">
|
||||
<test if="withMethods" name="${test.name}" methods="${test.methods}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if="withoutMethods" name="${test.name}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}"/>
|
||||
<test if="withMethods" name="${test.name}" methods="${test.methods}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if="withoutMethods" name="${test.name}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}"/>
|
||||
<jvmarg value="-Dlegacy-sstable-root=${test.data}/legacy-sstables"/>
|
||||
<jvmarg value="-Dinvalid-legacy-sstable-root=${test.data}/invalid-legacy-sstables"/>
|
||||
<jvmarg value="-Dcassandra.ring_delay_ms=1000"/>
|
||||
|
|
@ -1391,8 +1393,8 @@
|
|||
-->
|
||||
<target name="long-testsome" depends="maybe-build-test" description="Execute specific long unit tests" >
|
||||
<testmacro inputdir="${test.long.src}" timeout="${test.long.timeout}">
|
||||
<test if="withMethods" name="${test.name}" methods="${test.methods}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if="withoutMethods" name="${test.name}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}"/>
|
||||
<test if="withMethods" name="${test.name}" methods="${test.methods}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if="withoutMethods" name="${test.name}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}"/>
|
||||
<jvmarg value="-Dcassandra.ring_delay_ms=1000"/>
|
||||
<jvmarg value="-Dcassandra.tolerate_sstable_size=true"/>
|
||||
</testmacro>
|
||||
|
|
@ -1404,8 +1406,8 @@
|
|||
-->
|
||||
<target name="burn-testsome" depends="maybe-build-test" description="Execute specific burn unit tests" >
|
||||
<testmacro inputdir="${test.burn.src}" timeout="${test.burn.timeout}">
|
||||
<test if="withMethods" name="${test.name}" methods="${test.methods}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if="withoutMethods" name="${test.name}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}"/>
|
||||
<test if="withMethods" name="${test.name}" methods="${test.methods}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if="withoutMethods" name="${test.name}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}"/>
|
||||
<jvmarg value="-Dlogback.configurationFile=test/conf/logback-burntest.xml"/>
|
||||
</testmacro>
|
||||
</target>
|
||||
|
|
@ -1502,7 +1504,7 @@
|
|||
<sequential>
|
||||
<echo message="running CQL tests"/>
|
||||
<mkdir dir="${build.test.dir}/cassandra"/>
|
||||
<mkdir dir="${build.test.dir}/output"/>
|
||||
<mkdir dir="${build.test.output.dir}"/>
|
||||
<junit fork="on" forkmode="once" failureproperty="testfailed" maxmemory="1024m" timeout="${test.timeout}">
|
||||
<formatter type="brief" usefile="false"/>
|
||||
<jvmarg value="-Dstorage-config=${test.conf}"/>
|
||||
|
|
@ -1521,7 +1523,7 @@
|
|||
<include name="**/*.jar" />
|
||||
</fileset>
|
||||
</classpath>
|
||||
<batchtest todir="${build.test.dir}/output">
|
||||
<batchtest todir="${build.test.output.dir}">
|
||||
<fileset dir="${test.unit.src}" includes="**/cql3/*Test.java">
|
||||
<contains text="CQLTester" casesensitive="yes"/>
|
||||
</fileset>
|
||||
|
|
@ -1548,7 +1550,7 @@
|
|||
<sequential>
|
||||
<echo message="running ${test.methods} tests from ${test.name}"/>
|
||||
<mkdir dir="${build.test.dir}/cassandra"/>
|
||||
<mkdir dir="${build.test.dir}/output"/>
|
||||
<mkdir dir="${build.test.output.dir}"/>
|
||||
<junit fork="on" forkmode="once" failureproperty="testfailed" maxmemory="1024m" timeout="${test.timeout}">
|
||||
<formatter type="brief" usefile="false"/>
|
||||
<jvmarg value="-Dstorage-config=${test.conf}"/>
|
||||
|
|
@ -1567,8 +1569,8 @@
|
|||
<include name="**/*.jar" />
|
||||
</fileset>
|
||||
</classpath>
|
||||
<test unless:blank="${test.methods}" name="org.apache.cassandra.cql3.${test.name}" methods="${test.methods}" todir="${build.test.dir}/output"/>
|
||||
<test if:blank="${test.methods}" name="org.apache.cassandra.cql3.${test.name}" todir="${build.test.dir}/output"/>
|
||||
<test unless:blank="${test.methods}" name="org.apache.cassandra.cql3.${test.name}" methods="${test.methods}" todir="${build.test.output.dir}"/>
|
||||
<test if:blank="${test.methods}" name="org.apache.cassandra.cql3.${test.name}" todir="${build.test.output.dir}"/>
|
||||
</junit>
|
||||
</sequential>
|
||||
</target>
|
||||
|
|
@ -1664,15 +1666,6 @@
|
|||
<testhelper testdelegate="testlist"/>
|
||||
</target>
|
||||
|
||||
<target name="generate-test-report" description="Generates JUnit's HTML report from results already in build/output">
|
||||
<junitreport todir="${build.test.dir}">
|
||||
<fileset dir="${build.test.dir}/output">
|
||||
<include name="**/TEST-*.xml"/>
|
||||
</fileset>
|
||||
<report format="frames" todir="${build.test.dir}/junitreport"/>
|
||||
</junitreport>
|
||||
</target>
|
||||
|
||||
<!-- run a list of tests as provided in -Dtest.classlistfile (or default of 'testnames.txt')
|
||||
The class list file should be one test class per line, with the path starting after test/unit
|
||||
e.g. org/apache/cassandra/hints/HintMessageTest.java -->
|
||||
|
|
@ -1825,8 +1818,8 @@
|
|||
-->
|
||||
<target name="test-jvm-dtest-some" depends="maybe-build-test" description="Execute some in-jvm dtests">
|
||||
<testmacro inputdir="${test.distributed.src}" timeout="${test.distributed.timeout}" forkmode="once" showoutput="true">
|
||||
<test unless:blank="${test.methods}" name="${test.name}" methods="${test.methods}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if:blank="${test.methods}" name="${test.name}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}"/>
|
||||
<test unless:blank="${test.methods}" name="${test.name}" methods="${test.methods}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if:blank="${test.methods}" name="${test.name}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}"/>
|
||||
<jvmarg value="-Dlogback.configurationFile=test/conf/logback-dtest.xml"/>
|
||||
<jvmarg value="-Dcassandra.ring_delay_ms=10000"/>
|
||||
<jvmarg value="-Dcassandra.tolerate_sstable_size=true"/>
|
||||
|
|
@ -1837,8 +1830,8 @@
|
|||
|
||||
<target name="test-jvm-dtest-latest-some" depends="maybe-build-test" description="Execute some in-jvm dtests with latest configuration">
|
||||
<testmacro inputdir="${test.distributed.src}" timeout="${test.distributed.timeout}" forkmode="once" showoutput="true">
|
||||
<test unless:blank="${test.methods}" name="${test.name}" methods="${test.methods}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if:blank="${test.methods}" name="${test.name}" todir="${build.test.dir}/output/" outfile="TEST-${test.name}"/>
|
||||
<test unless:blank="${test.methods}" name="${test.name}" methods="${test.methods}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}-${test.methods}"/>
|
||||
<test if:blank="${test.methods}" name="${test.name}" todir="${build.test.output.dir}/" outfile="TEST-${test.name}"/>
|
||||
<jvmarg value="-Djvm_dtests.latest=true"/>
|
||||
<jvmarg value="-Dlogback.configurationFile=test/conf/logback-dtest.xml"/>
|
||||
<jvmarg value="-Dcassandra.ring_delay_ms=10000"/>
|
||||
|
|
@ -1847,20 +1840,20 @@
|
|||
</testmacro>
|
||||
</target>
|
||||
|
||||
|
||||
<target name="generate-unified-test-report" description="Merge all unit xml files into one, generate html pages, and print summary test numbers">
|
||||
<junitreport todir="${build.dir}">
|
||||
<fileset dir="${build.test.dir}/output">
|
||||
<include name="**/TEST*.xml"/>
|
||||
<include name="**/cqlshlib.xml"/>
|
||||
<include name="**/nosetests.xml"/>
|
||||
<target name="generate-test-report" description="Merge all unit xml files into one, generate html pages, and print summary test numbers">
|
||||
<echo message="Generating Test Summary for test files found under ${build.test.output.dir}" />
|
||||
<mkdir dir="${build.test.report.dir}/unitTestReport"/>
|
||||
<junitreport todir="${build.test.report.dir}/unitTestReport">
|
||||
<fileset dir="${build.test.output.dir}">
|
||||
<include name="**/TEST-*.xml"/>
|
||||
<include name="**/cqlshlib*.xml"/>
|
||||
<include name="**/nosetests*.xml"/>
|
||||
</fileset>
|
||||
<!-- FIXME this can easily OOM, need a workaround-->
|
||||
<report todir="${build.test.dir}/html" />
|
||||
<report todir="${build.test.report.dir}/unitTestReport/html" unless:true="${print-summary.skip}" />
|
||||
</junitreport>
|
||||
<!-- concat the report through a filter chain to extract what you want -->
|
||||
<concat>
|
||||
<fileset file="${build.test.dir}/html/overview-summary.html" />
|
||||
<concat unless:true="${print-summary.skip}">
|
||||
<fileset file="${build.test.report.dir}/unitTestReport/html/overview-summary.html" />
|
||||
<filterchain>
|
||||
<linecontainsregexp>
|
||||
<regexp pattern='title="Display all tests"' />
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ if [ "$cython" = "yes" ]; then
|
|||
else
|
||||
TESTSUITE_NAME="${TESTSUITE_NAME}.no_cython"
|
||||
fi
|
||||
TESTSUITE_NAME="${TESTSUITE_NAME}.$(uname -m)"
|
||||
|
||||
################################
|
||||
#
|
||||
|
|
@ -116,7 +117,7 @@ sed -r "s/<[\/]?testsuites>//g" ${BUILD_DIR}/test/output/cqlshlib.xml > /tmp/cql
|
|||
cat /tmp/cqlshlib.xml > ${BUILD_DIR}/test/output/cqlshlib.xml
|
||||
|
||||
# don't do inline sed for linux+mac compat
|
||||
sed "s/testsuite errors=\(\".*\"\) failures=\(\".*\"\) hostname=\(\".*\"\) name=\"pytest\"/testsuite errors=\1 failures=\2 hostname=\3 name=\"${TESTSUITE_NAME}\"/g" ${BUILD_DIR}/test/output/cqlshlib.xml > /tmp/cqlshlib.xml
|
||||
sed "s/testsuite name=\"pytest\"/testsuite name=\"${TESTSUITE_NAME}\"/g" ${BUILD_DIR}/test/output/cqlshlib.xml > /tmp/cqlshlib.xml
|
||||
cat /tmp/cqlshlib.xml > ${BUILD_DIR}/test/output/cqlshlib.xml
|
||||
sed "s/testcase classname=\"cqlshlib./testcase classname=\"${TESTSUITE_NAME}./g" ${BUILD_DIR}/test/output/cqlshlib.xml > /tmp/cqlshlib.xml
|
||||
cat /tmp/cqlshlib.xml > ${BUILD_DIR}/test/output/cqlshlib.xml
|
||||
|
|
|
|||
|
|
@ -66,11 +66,12 @@ import com.google.common.base.Joiner;
|
|||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import com.vdurmont.semver4j.SemverException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import org.apache.cassandra.audit.IAuditLogger;
|
||||
import org.apache.cassandra.auth.AllowAllNetworkAuthorizer;
|
||||
import org.apache.cassandra.auth.IAuthenticator;
|
||||
|
|
@ -1405,15 +1406,21 @@ public class FBUtilities
|
|||
if (!isLinux)
|
||||
return null;
|
||||
|
||||
String output = null;
|
||||
try
|
||||
{
|
||||
String output = exec(Map.of(), Duration.ofSeconds(5), 1024, 1024, "uname", "-r");
|
||||
output = exec(Map.of(), Duration.ofSeconds(5), 1024, 1024, "uname", "-r");
|
||||
|
||||
if (output.isEmpty())
|
||||
throw new RuntimeException("Error while trying to get kernel version, 'uname -r' returned empty output");
|
||||
|
||||
return parseKernelVersion(output);
|
||||
}
|
||||
catch (SemverException e)
|
||||
{
|
||||
logger.error("SemverException parsing {}", output, e);
|
||||
throw e;
|
||||
}
|
||||
catch (IOException | TimeoutException e)
|
||||
{
|
||||
throw new RuntimeException("Error while trying to get kernel version", e);
|
||||
|
|
@ -1429,6 +1436,7 @@ public class FBUtilities
|
|||
static Semver parseKernelVersion(String versionString)
|
||||
{
|
||||
Preconditions.checkNotNull(versionString, "kernel version cannot be null");
|
||||
// ignore blank lines
|
||||
try (Scanner scanner = new Scanner(versionString))
|
||||
{
|
||||
while (scanner.hasNextLine())
|
||||
|
|
@ -1436,6 +1444,11 @@ public class FBUtilities
|
|||
String version = scanner.nextLine().trim();
|
||||
if (version.isEmpty())
|
||||
continue;
|
||||
|
||||
if (version.endsWith("+"))
|
||||
// gcp's cos_containerd has a trailing +
|
||||
version = StringUtils.chop(version);
|
||||
|
||||
return new Semver(version, Semver.SemverType.LOOSE);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,9 @@ public class CassandraXMLJUnitResultFormatter implements JUnitResultFormatter, X
|
|||
*/
|
||||
private final Hashtable<String, Element> testElements = new Hashtable<String, Element>();
|
||||
|
||||
private Element propsElement;
|
||||
private Element systemOutputElement;
|
||||
|
||||
/**
|
||||
* tests that failed.
|
||||
*/
|
||||
|
|
@ -142,12 +145,12 @@ public class CassandraXMLJUnitResultFormatter implements JUnitResultFormatter, X
|
|||
|
||||
/** {@inheritDoc}. */
|
||||
public void setSystemOutput(final String out) {
|
||||
formatOutput(SYSTEM_OUT, out);
|
||||
systemOutputElement = formatOutput(SYSTEM_OUT, out);
|
||||
}
|
||||
|
||||
/** {@inheritDoc}. */
|
||||
public void setSystemError(final String out) {
|
||||
formatOutput(SYSTEM_ERR, out);
|
||||
rootElement.appendChild(formatOutput(SYSTEM_ERR, out));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -170,8 +173,7 @@ public class CassandraXMLJUnitResultFormatter implements JUnitResultFormatter, X
|
|||
rootElement.setAttribute(HOSTNAME, getHostname());
|
||||
|
||||
// Output properties
|
||||
final Element propsElement = doc.createElement(PROPERTIES);
|
||||
rootElement.appendChild(propsElement);
|
||||
propsElement = doc.createElement(PROPERTIES);
|
||||
final Properties props = suite.getProperties();
|
||||
if (props != null) {
|
||||
final Enumeration e = props.propertyNames();
|
||||
|
|
@ -212,8 +214,13 @@ public class CassandraXMLJUnitResultFormatter implements JUnitResultFormatter, X
|
|||
rootElement.setAttribute(ATTR_FAILURES, "" + suite.failureCount());
|
||||
rootElement.setAttribute(ATTR_ERRORS, "" + suite.errorCount());
|
||||
rootElement.setAttribute(ATTR_SKIPPED, "" + suite.skipCount());
|
||||
rootElement.setAttribute(
|
||||
ATTR_TIME, "" + (suite.getRunTime() / ONE_SECOND));
|
||||
rootElement.setAttribute(ATTR_TIME, "" + (suite.getRunTime() / ONE_SECOND));
|
||||
if (suite.failureCount() > 0 || suite.errorCount() > 0)
|
||||
{
|
||||
// only include properties and system-out if there's failure/error
|
||||
rootElement.appendChild(propsElement);
|
||||
rootElement.appendChild(systemOutputElement);
|
||||
}
|
||||
if (out != null) {
|
||||
Writer wri = null;
|
||||
try {
|
||||
|
|
@ -351,10 +358,10 @@ public class CassandraXMLJUnitResultFormatter implements JUnitResultFormatter, X
|
|||
nested.appendChild(trace);
|
||||
}
|
||||
|
||||
private void formatOutput(final String type, final String output) {
|
||||
private Element formatOutput(final String type, final String output) {
|
||||
final Element nested = doc.createElement(type);
|
||||
rootElement.appendChild(nested);
|
||||
nested.appendChild(doc.createCDATASection(output));
|
||||
return nested;
|
||||
}
|
||||
|
||||
public void testIgnored(final Test test) {
|
||||
|
|
|
|||
|
|
@ -38,13 +38,13 @@ import java.util.concurrent.Future;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import org.apache.cassandra.config.Config;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
|
|
@ -386,6 +386,9 @@ public class FBUtilitiesTest
|
|||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> parseKernelVersion("\n \n"))
|
||||
.withMessageContaining("no version found");
|
||||
|
||||
// gcp's cos_containerd example
|
||||
assertThat(parseKernelVersion("5.15.133+").toString()).isEqualTo("5.15.133");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Reference in New Issue