mirror of
https://gitlab.eurecom.fr/oai/openairinterface5g.git
synced 2026-07-13 04:30:28 +00:00
- all RAN code, CI code, configuration files, dockerfiles, in CSSL v1.0
- all deployment code (openshift, charts, ancillary files like shell
scripts), in MIT
- documentation in CC-BY-4.0
- exceptions might apply and are listed in NOTICE
- there is a new LICENSES folder with all licenses
- CONTRIBUTIONS.md has been updated accordingly
For automated changes based on OAI PL v1.1:
perl -i~ -0pe 's/\/\*.*Licensed to the OpenAirInterface.*openairinterface.org\n#?/\/*\n * SPDX-License-Identifier: LicenseRef-CSSL-1.0\n/s' **/*.{c,h,cpp}
perl -i~ -0pe 's/\/\*.*Licensed to the OpenAirInterface.*openairinterface.org\n#?/\/*\n * SPDX-License-Identifier: LicenseRef-CSSL-1.0\n/s' **/*.ts
perl -i~ -0pe 's/<!--.*Licensed to the OpenAirInterface.*openairinterface.org\n.*-->/<!-- SPDX-License-Identifier: LicenseRef-CSSL-1.0 -->/s' **/*.xml
The rest (cmake, files with missing license, cmake) manually.
228 lines
8.3 KiB
Python
228 lines
8.3 KiB
Python
# SPDX-License-Identifier: LicenseRef-CSSL-1.0
|
|
|
|
import re
|
|
import os
|
|
import logging
|
|
import yaml
|
|
import signal
|
|
|
|
import xml.etree.ElementTree as ET
|
|
import json
|
|
|
|
import importlib, inspect
|
|
|
|
class Analysis():
|
|
|
|
def _get_test_description(properties):
|
|
env_vars = None
|
|
for p in properties:
|
|
if p["name"] == "ENVIRONMENT":
|
|
env_vars = p["value"] # save for later if no custom property
|
|
if p["name"] == "TEST_DESCRIPTION":
|
|
return p["value"]
|
|
# if we came till here, it means there is no custom test property
|
|
# saved in JSON. See if we have a description in environment variables
|
|
if not env_vars:
|
|
return "<none>"
|
|
for ev in env_vars:
|
|
name, value = ev.split("=", 1)
|
|
if name == "TEST_DESCRIPTION":
|
|
return value
|
|
return "<none>"
|
|
|
|
def analyze_physim(result_junit, details_json, logPath):
|
|
try:
|
|
tree = ET.parse(result_junit)
|
|
root = tree.getroot()
|
|
nb_tests = int(root.attrib["tests"])
|
|
nb_failed = int(root.attrib["failures"])
|
|
except ET.ParseError as e:
|
|
return False, False, f'Could not parse XML log file {result_junit}: {e}'
|
|
except FileNotFoundError as e:
|
|
return False, False, f'JUnit XML log file {result_junit} not found: {e}'
|
|
except Exception as e:
|
|
return False, False, f'While parsing JUnit XML log file: exception: {e}'
|
|
|
|
try:
|
|
with open(details_json) as f:
|
|
j = json.load(f)
|
|
# prepare JSON for easier access of strings
|
|
json_test_desc = {}
|
|
for e in j["tests"]:
|
|
json_test_desc[e["name"]] = e
|
|
|
|
except json.JSONDecodeError as e:
|
|
return False, False, f'Could not decode JSON log file {details_json}: {e}'
|
|
except FileNotFoundError as e:
|
|
return False, False, f'Physim JSON log file {details_json} not found: {e}'
|
|
except Exception as e:
|
|
return False, False, f'While parsing physim JSON log file: exception: {e}'
|
|
|
|
test_result = {}
|
|
for test in root: # for each test
|
|
test_name = test.attrib["name"]
|
|
test_exec = json_test_desc[test_name]["properties"][1]["value"][0]
|
|
desc = Analysis._get_test_description(json_test_desc[test_name]["properties"])
|
|
# get runtime and checks
|
|
test_check = test.attrib["status"] == "run"
|
|
time = round(float(test.attrib["time"]), 1)
|
|
time_check = time < 150
|
|
output = test.findtext("system-out")
|
|
output_check = "exceeds the threshold" not in output
|
|
# collect logs
|
|
log_dir = f'{logPath}/{test_exec}'
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
with open(f'{log_dir}/{test_name}.log', 'w') as f:
|
|
f.write(output)
|
|
# prepare result and info
|
|
resultstr = 'PASS' if (test_check and time_check and output_check) else 'FAIL'
|
|
info = f"{test_name}.log: test {resultstr}"
|
|
for l in output.splitlines():
|
|
if l.startswith("CHECK "):
|
|
info += f"\n{l}"
|
|
if test_check:
|
|
if not output_check:
|
|
info += "\nTest log exceeds maximal allowed length 100 kB"
|
|
if not time_check:
|
|
info += "\nTest exceeds 150s"
|
|
if not (time_check and output_check):
|
|
nb_failed += 1 # time threshold/output length error, not counted for by ctest as of now
|
|
test_result[test_name] = [desc, info, resultstr]
|
|
|
|
test_summary = {}
|
|
test_summary['Nbtests'] = nb_tests
|
|
test_summary['Nbpass'] = nb_tests - nb_failed
|
|
test_summary['Nbfail'] = nb_failed
|
|
return nb_failed == 0, test_summary, test_result
|
|
|
|
def analyze_rt_stats(thresholds, L1_stats, MAC_stats):
|
|
with open(thresholds, 'r') as f:
|
|
datalog_rt_stats = yaml.load(f, Loader=yaml.FullLoader)
|
|
|
|
rt_keys = datalog_rt_stats['Ref']
|
|
real_time_stats = {}
|
|
for sf in [L1_stats, MAC_stats]:
|
|
with open(sf, 'r') as f:
|
|
for line in f.readlines():
|
|
for k in rt_keys:
|
|
result = re.search(k, line)
|
|
if result is not None:
|
|
tmp = re.match(rf'^.*?(\b{k}\b.*)', line.rstrip())
|
|
if tmp is not None:
|
|
real_time_stats[k] = tmp.group(1)
|
|
|
|
# datalog_rt_stats format must align with HTML.CreateHtmlDataLogTable()
|
|
datalog_rt_stats['Data']={}
|
|
for k in real_time_stats:
|
|
tmp = re.match(r'^(?P<metric>.*):\s+(?P<avg>\d+\.\d+) us;\s+(?P<count>\d+);\s+(?P<max>\d+\.\d+) us;', real_time_stats[k])
|
|
if tmp is not None:
|
|
metric = tmp.group('metric')
|
|
avg = float(tmp.group('avg'))
|
|
max = float(tmp.group('max'))
|
|
count = int(tmp.group('count'))
|
|
datalog_rt_stats['Data'][metric] = ["{:.0f}".format(avg),"{:.0f}".format(max),"{:d}".format(count),"{:.2f}".format(avg / datalog_rt_stats['Ref'][metric])]
|
|
|
|
success = True
|
|
for k in datalog_rt_stats['Data']:
|
|
valnorm = float(datalog_rt_stats['Data'][k][3])
|
|
dev = datalog_rt_stats['DeviationThreshold'][k]
|
|
if valnorm > 1.0 + dev or valnorm < 1.0 - dev: # condition for fail: avg/ref deviates by more than "deviation threshold"
|
|
logging.debug(f'\u001B[1;30;43m normalized metric {k}={valnorm} deviates by more than {dev}\u001B[0m')
|
|
success = False
|
|
return success, datalog_rt_stats
|
|
|
|
|
|
# returns tuple of (service, analyzer, option string)
|
|
def _lookupServiceAnalyzerOpt(s, analyzers):
|
|
res = s.split("=", 2)
|
|
name = res[0]
|
|
if len(res) == 1:
|
|
return res[0], analyzers["Default"], None
|
|
opt = res[2] if len(res) > 2 else None
|
|
func = res[1]
|
|
a = analyzers[func] if func in analyzers else None
|
|
return name, a, opt
|
|
|
|
# groups requested service analysis (service=func[=option]) on per service
|
|
# basis and looks up analyzer func. If log analysis is requested, will always
|
|
# run Default log analysis
|
|
def _groupServices(to_analyze):
|
|
req_analysis = {}
|
|
# get content of cls_loganalysis module, then get all analyzers (classes) in this module
|
|
mod = importlib.import_module("cls_loganalysis")
|
|
analyzers = {name:cl for name, cl in inspect.getmembers(mod, inspect.isclass)}
|
|
for req in to_analyze:
|
|
s, func, opt = _lookupServiceAnalyzerOpt(req, analyzers)
|
|
logging.debug(f"requested check '{req}' => service {s}, function {func.__name__}, options '{opt}'")
|
|
# always put default analyzer first
|
|
l = req_analysis[s] if s in req_analysis else [(analyzers["Default"], None, "default")]
|
|
if func is not analyzers["Default"]:
|
|
l.append((func, opt, req))
|
|
req_analysis[s] = l
|
|
return req_analysis
|
|
|
|
def _describe_exit_code(code):
|
|
if code > 128:
|
|
sig = code - 128
|
|
try:
|
|
return f"terminated by signal {sig} ({signal.Signals(sig).name})"
|
|
except ValueError:
|
|
return f"terminated by unknown signal {sig}"
|
|
else:
|
|
return f"custom exit code"
|
|
|
|
def AnalyzeServices(HTML, service_desc, to_analyze):
|
|
success = True
|
|
# hack: we want to give as a description "log analysis", but the description
|
|
# is set outside, so retain what was set before
|
|
orig_html_desc = HTML.desc
|
|
# group analysis on a per-service basis, then iterate
|
|
for serv, list_analysis in _groupServices(to_analyze).items():
|
|
HTML.desc = f"Log analysis for service {serv}"
|
|
if serv not in service_desc:
|
|
success = False
|
|
logging.error(f"requested service {serv} not in list of services")
|
|
HTML.CreateHtmlTestRowQueue("N/A", 'KO', ["service not detected"])
|
|
continue
|
|
# pre-initialize with return code
|
|
rc = service_desc[serv]["returncode"]
|
|
logging.info(f"analyze service {serv}: return code {rc}")
|
|
service_success = True # TODO rc == 0: too many functions (eNB, lteUE, RIC, nrUE) fail non-zero
|
|
logs = []
|
|
if rc != 0:
|
|
logs.append(f"=> return code {rc}, likely " + _describe_exit_code(rc) + " [ignored by CI]")
|
|
# skip if the file is too big
|
|
logfile = service_desc[serv]["logfile"]
|
|
filename = os.path.basename(logfile)
|
|
b = os.path.getsize(logfile)
|
|
logging.debug(f"using logfile {logfile} of size {b} bytes")
|
|
if b > 10 * 1024 * 1024:
|
|
success = False
|
|
logs.append(f"logfile too big (>10MB)")
|
|
logging.error(logs)
|
|
HTML.CreateHtmlTestRowQueue(filename, 'KO', ["\n".join(logs)])
|
|
continue
|
|
# run each analyzer with its options on the logfile
|
|
for (func, opt, desc) in list_analysis:
|
|
if func is None:
|
|
service_success = False
|
|
logging.error(f"request analysis function for desc {desc} not found")
|
|
HTML.CreateHtmlTestRowQueue(serv, 'KO', [f"no analysis function for {desc}"])
|
|
continue
|
|
result, l = func.run(logfile, opt)
|
|
logging.info(f"service {serv}: analysis with func {func}, result {result}, logs '{l}'")
|
|
service_success = service_success and result
|
|
if not result:
|
|
logs.append(f"=> {func.__name__} check (options '{opt}') FAILED")
|
|
logs.append(l)
|
|
logs = '\n'.join(logs)
|
|
if not service_success:
|
|
logging.error(l)
|
|
else:
|
|
logging.info(l)
|
|
all_funcs = ", ".join([f.__name__ for (f, _, _) in list_analysis])
|
|
HTML.CreateHtmlTestRowQueue(f"Check {all_funcs} on {filename}", 'OK' if service_success else 'KO', [logs])
|
|
success = success and service_success
|
|
HTML.desc = orig_html_desc
|
|
return success
|