chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:04:25 +08:00
commit 548b49ebc0
20937 changed files with 5455372 additions and 0 deletions
@@ -0,0 +1,131 @@
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import logging
import os
import socket
from typing import Any
from typing import List
from idf_ci_utils import IDF_PATH
try:
import netifaces
except ImportError:
from unittest.mock import MagicMock
netifaces = MagicMock()
logging.warning('netifaces is not installed. Please install it to get network interface information.')
import yaml
ENV_CONFIG_FILE_SEARCH = [
os.path.join(IDF_PATH, 'EnvConfig.yml'),
os.path.join(IDF_PATH, 'ci-test-runner-configs', os.environ.get('CI_RUNNER_DESCRIPTION', ''), 'EnvConfig.yml'),
]
ENV_CONFIG_TEMPLATE = '''
$IDF_PATH/EnvConfig.yml:
```yaml
<env_name>:
key: var
key2: var2
<another_env>:
key: var
```
'''
def get_host_ip_by_interface(interface_name: str, ip_type: int = netifaces.AF_INET) -> str:
if ip_type == netifaces.AF_INET:
for _addr in netifaces.ifaddresses(interface_name)[ip_type]:
host_ip = _addr['addr'].replace('%{}'.format(interface_name), '')
assert isinstance(host_ip, str)
return host_ip
elif ip_type == netifaces.AF_INET6:
ip6_addrs: List[str] = []
for _addr in netifaces.ifaddresses(interface_name)[ip_type]:
host_ip = _addr['addr'].replace('%{}'.format(interface_name), '')
assert isinstance(host_ip, str)
# prefer to use link local address due to example settings
if host_ip.startswith('FE80::'):
ip6_addrs.insert(0, host_ip)
else:
ip6_addrs.append(host_ip)
if ip6_addrs:
return ip6_addrs[0]
return ''
def get_host_ip4_by_dest_ip(dest_ip: str = '') -> str:
if not dest_ip:
dest_ip = '8.8.8.8'
s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s1.connect((dest_ip, 80))
host_ip = s1.getsockname()[0]
s1.close()
assert isinstance(host_ip, str)
print(f'Using host ip: {host_ip}')
return host_ip
def get_host_ip6_by_dest_ip(dest_ip: str, interface: str) -> str:
addr_info = socket.getaddrinfo(f'{dest_ip}%{interface}', 80, socket.AF_INET6, socket.SOCK_DGRAM)
s1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s1.connect(addr_info[0][-1])
host_ip = s1.getsockname()[0]
s1.close()
assert isinstance(host_ip, str)
print(f'Using host ip: {host_ip}')
return host_ip
def get_my_interface_by_dest_ip(dest_ip: str = '') -> str:
my_ip = get_host_ip4_by_dest_ip(dest_ip)
interfaces = netifaces.interfaces()
for interface in interfaces:
try:
addresses = netifaces.ifaddresses(interface)[netifaces.AF_INET]
except KeyError:
continue
if my_ip in [a['addr'] for a in addresses]:
assert isinstance(interface, str)
return interface
logging.error('Can not get interface, dest ip is {}'.format(dest_ip))
return ''
def get_env_config_variable(env_name: str, key: str, default: Any = None) -> Any:
"""
Get test environment related variable
config file format: $IDF_PATH/EnvConfig.yml
```
<env_name>:
key: var
key2: var2
<env_name2>:
key: var
```
"""
config = dict()
for _file in ENV_CONFIG_FILE_SEARCH:
try:
with open(_file, 'r') as f:
data = yaml.load(f.read(), Loader=yaml.SafeLoader)
config = data[env_name]
break
except (FileNotFoundError, KeyError):
pass
else:
pass
var = config.get(key, default)
if var is None:
logging.warning(f'Failed to get env variable {env_name}/{key}.')
logging.info(f'Env config file template: {ENV_CONFIG_TEMPLATE}')
if not os.environ.get('CI_JOB_ID'):
# Try to get variable from stdin
var = input(f'You can input the variable now:')
else:
raise ValueError(f'Env variable not found: {env_name}/{key}')
logging.debug(f'Got env variable {env_name}/{key}: {var}')
return var
+398
View File
@@ -0,0 +1,398 @@
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import argparse
import copy
import logging
import os
import re
import sys
import tarfile
import tempfile
import time
import zipfile
from collections.abc import Callable
from functools import wraps
from typing import Any
import gitlab
TR = Callable[..., Any]
logging.basicConfig(level=logging.INFO)
def retry(func: TR) -> TR:
"""
This wrapper will only catch several exception types associated with
"network issues" and retry the whole function.
"""
@wraps(func)
def wrapper(self: 'Gitlab', *args: Any, **kwargs: Any) -> Any:
retried = 0
while True:
try:
res = func(self, *args, **kwargs)
except (OSError, EOFError, gitlab.exceptions.GitlabError) as e:
if isinstance(e, gitlab.exceptions.GitlabError):
if e.response_code == 500:
# retry on this error
pass
elif e.response_code == 404 and os.environ.get('LOCAL_GITLAB_HTTPS_HOST', None):
# remove the environment variable "LOCAL_GITLAB_HTTPS_HOST" and retry
os.environ.pop('LOCAL_GITLAB_HTTPS_HOST', None)
else:
# other GitlabErrors aren't retried
raise e
retried += 1
if retried > self.DOWNLOAD_ERROR_MAX_RETRIES:
raise e # get out of the loop
else:
logging.warning(
'Network failure in {}, retrying ({})'.format(
getattr(func, '__name__', '(unknown callable)'), retried
)
)
time.sleep(2**retried) # wait a bit more after each retry
continue
else:
break
return res
return wrapper
class Gitlab:
JOB_NAME_PATTERN = re.compile(r'(\w+)(\s+(\d+)/(\d+))?')
DOWNLOAD_ERROR_MAX_RETRIES = 3
DEFAULT_BUILD_CHILD_PIPELINE_NAME = 'Build Child Pipeline'
def __init__(self, project_id: int | str | None = None):
config_data_from_env = os.getenv('PYTHON_GITLAB_CONFIG')
if config_data_from_env:
# prefer to load config from env variable
with tempfile.NamedTemporaryFile('w', delete=False) as temp_file:
temp_file.write(config_data_from_env)
config_files = [temp_file.name]
else:
# otherwise try to use config file at local filesystem
config_files = None
self._init_gitlab_inst(project_id, config_files)
@retry
def _init_gitlab_inst(self, project_id: int | None, config_files: list[str] | None) -> None:
gitlab_id = os.getenv('LOCAL_GITLAB_HTTPS_HOST') # if None, will use the default gitlab server
self.gitlab_inst = gitlab.Gitlab.from_config(gitlab_id=gitlab_id, config_files=config_files)
try:
self.gitlab_inst.auth()
except gitlab.exceptions.GitlabAuthenticationError:
msg = """To call gitlab apis locally, please create ~/.python-gitlab.cfg with the following content:
[global]
default = internal
ssl_verify = true
timeout = 5
[internal]
url = <OUR INTERNAL HTTPS SERVER URL>
private_token = <YOUR PERSONAL ACCESS TOKEN>
api_version = 4
"""
raise SystemExit(msg)
if project_id:
self.project = self.gitlab_inst.projects.get(project_id, lazy=True)
else:
self.project = None
@retry
def get_project_id(self, name: str, namespace: str | None = None) -> int:
"""
search project ID by name
:param name: project name
:param namespace: namespace to match when we have multiple project with same name
:return: project ID
"""
projects = self.gitlab_inst.projects.list(search=name)
res = []
for project in projects:
if namespace is None:
if len(projects) == 1:
res.append(project.id)
break
if project.namespace['path'] == namespace:
if project.name == name:
res.insert(0, project.id)
else:
res.append(project.id)
if not res:
raise ValueError("Can't find project")
return int(res[0])
@retry
def download_artifacts(self, job_id: int, destination: str) -> None:
"""
download full job artifacts and extract to destination.
:param job_id: Gitlab CI job ID
:param destination: extract artifacts to path.
"""
job = self.project.jobs.get(job_id)
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
job.artifacts(streamed=True, action=temp_file.write)
with zipfile.ZipFile(temp_file.name, 'r') as archive_file:
archive_file.extractall(destination)
@retry
def download_artifact(self, job_id: int, artifact_path: list[str], destination: str | None = None) -> list[bytes]:
"""
download specific path of job artifacts and extract to destination.
:param job_id: Gitlab CI job ID
:param artifact_path: list of path in artifacts (relative path to artifact root path)
:param destination: destination of artifact. Do not save to file if destination is None
:return: A list of artifact file raw data.
"""
job = self.project.jobs.get(job_id)
raw_data_list = []
for a_path in artifact_path:
try:
data = job.artifact(a_path) # type: bytes
except gitlab.GitlabGetError as e:
logging.error(f"Failed to download '{a_path}' from job {job_id}")
raise e
raw_data_list.append(data)
if destination:
file_path = os.path.join(destination, a_path)
try:
os.makedirs(os.path.dirname(file_path))
except OSError:
# already exists
pass
with open(file_path, 'wb') as f:
f.write(data)
return raw_data_list
@retry
def find_job_id(self, job_name: str, pipeline_id: str | None = None, job_status: str = 'success') -> list[dict]:
"""
Get Job ID from job name of specific pipeline
:param job_name: job name
:param pipeline_id: If None, will get pipeline id from CI pre-defined variable.
:param job_status: status of job. One pipeline could have multiple jobs with same name after retry.
job_status is used to filter these jobs.
:return: a list of job IDs (parallel job will generate multiple jobs)
"""
job_id_list = []
if pipeline_id is None:
pipeline_id = os.getenv('CI_PIPELINE_ID')
pipeline = self.project.pipelines.get(pipeline_id)
jobs = pipeline.jobs.list(all=True)
for job in jobs:
match = self.JOB_NAME_PATTERN.match(job.name)
if match:
if match.group(1) == job_name and job.status == job_status:
job_id_list.append({'id': job.id, 'parallel_num': match.group(3)})
return job_id_list
@retry
def download_archive(
self, ref: str, destination: str, project_id: int | None = None, cache_dir: str | None = None
) -> str:
"""
Download archive of certain commit of a repository and extract to destination path
:param ref: commit or branch name
:param destination: destination path of extracted archive file
:param project_id: download project of current instance if project_id is None
:return: root path name of archive file
"""
if project_id is None:
project = self.project
else:
project = self.gitlab_inst.projects.get(project_id)
if cache_dir:
local_archive_file = os.path.join(cache_dir, f'{ref}.tar.gz')
os.makedirs(os.path.dirname(local_archive_file), exist_ok=True)
if os.path.isfile(local_archive_file):
logging.info('Use cached archive file. Skipping download...')
else:
with open(local_archive_file, 'wb') as fw:
try:
project.repository_archive(sha=ref, streamed=True, action=fw.write)
except gitlab.GitlabGetError as e:
logging.error(f'Failed to archive from project {project_id}')
raise e
logging.info(
f'Downloaded archive size: {float(os.path.getsize(local_archive_file)) / (1024 * 1024):.03f}MB'
)
return self.decompress_archive(local_archive_file, destination)
# no cache
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
try:
project.repository_archive(sha=ref, streamed=True, action=temp_file.write)
except gitlab.GitlabGetError as e:
logging.error(f'Failed to archive from project {project_id}')
raise e
logging.info(f'Downloaded archive size: {float(os.path.getsize(temp_file.name)) / (1024 * 1024):.03f}MB')
return self.decompress_archive(temp_file.name, destination)
@staticmethod
def _to_win32_long_path(path: str) -> str:
normalized_path = os.path.normpath(os.path.abspath(path))
if normalized_path.startswith('\\\\?\\'):
return normalized_path
if normalized_path.startswith('\\\\'):
return '\\\\?\\UNC\\' + normalized_path[2:]
return '\\\\?\\' + normalized_path
@staticmethod
def decompress_archive(path: str, destination: str) -> str:
full_destination = os.path.abspath(destination)
try:
with tarfile.open(path, 'r') as archive_file:
members = archive_file.getmembers()
root_name = members[0].name
if sys.platform == 'win32':
# tarfile keeps archive member names in POSIX form. Normalize them before
# combining with a long-path-prefixed destination to avoid invalid mixed separators.
full_destination = Gitlab._to_win32_long_path(full_destination)
members = [copy.copy(member) for member in members]
for member in members:
member.name = member.name.replace('/', '\\')
member.linkname = member.linkname.replace('/', '\\')
archive_file.extractall(full_destination, members=members)
except tarfile.TarError as e:
logging.error(f'Error while decompressing archive {path}')
raise e
return os.path.join(os.path.realpath(destination), root_name)
def get_job_tags(self, job_id: int) -> str:
"""
Get tags of a job
:param job_id: job id
:return: comma-separated tags of the job
"""
job = self.project.jobs.get(job_id)
return ','.join(job.tag_list)
def get_downstream_pipeline_ids(self, main_pipeline_id: int) -> list[int]:
"""
Retrieve the IDs of all downstream child pipelines for a given main pipeline.
:param main_pipeline_id: The ID of the main pipeline to start the search.
:return: A list of IDs of all downstream child pipelines.
"""
bridge_pipeline_ids = []
child_pipeline_ids = []
main_pipeline_bridges = self.project.pipelines.get(main_pipeline_id).bridges.list()
for bridge in main_pipeline_bridges:
downstream_pipeline = bridge.attributes.get('downstream_pipeline')
if not downstream_pipeline:
continue
bridge_pipeline_ids.append(downstream_pipeline['id'])
for bridge_pipeline_id in bridge_pipeline_ids:
child_pipeline_ids.append(bridge_pipeline_id)
bridge_pipeline = self.project.pipelines.get(bridge_pipeline_id)
if not bridge_pipeline.name == self.DEFAULT_BUILD_CHILD_PIPELINE_NAME:
continue
child_bridges = bridge_pipeline.bridges.list()
for child_bridge in child_bridges:
downstream_child_pipeline = child_bridge.attributes.get('downstream_pipeline')
if not downstream_child_pipeline:
continue
child_pipeline_ids.append(downstream_child_pipeline.get('id'))
return [pid for pid in child_pipeline_ids if pid is not None]
def retry_failed_jobs(self, pipeline_id: int, retry_allowed_failures: bool = False) -> list[int]:
"""
Retry failed jobs for a specific pipeline. Optionally include jobs marked as 'allowed failures'.
:param pipeline_id: ID of the pipeline whose failed jobs are to be retried.
:param retry_allowed_failures: Whether to retry jobs that are marked as allowed failures.
"""
jobs_succeeded_retry = []
pipeline_ids = [pipeline_id] + self.get_downstream_pipeline_ids(pipeline_id)
logging.info(f'Retrying jobs for pipelines: {pipeline_ids}')
for pid in pipeline_ids:
pipeline = self.project.pipelines.get(pid)
job_ids_to_retry = [
job.id
for job in pipeline.jobs.list(scope='failed')
if retry_allowed_failures or not job.attributes.get('allow_failure', False)
]
logging.info(f'Failed jobs for pipeline {pid}: {job_ids_to_retry}')
for job_id in job_ids_to_retry:
try:
res = self.project.jobs.get(job_id).retry()
jobs_succeeded_retry.append(job_id)
logging.info(f'Retried job {job_id} with result {res}')
except Exception as e:
logging.error(f'Failed to retry job {job_id}: {str(e)}')
return jobs_succeeded_retry
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument('action')
parser.add_argument('project_id', type=int)
parser.add_argument('--pipeline_id', '-i', type=int, default=None)
parser.add_argument('--ref', '-r', default='master')
parser.add_argument('--job_id', '-j', type=int, default=None)
parser.add_argument('--job_name', '-n', default=None)
parser.add_argument('--project_name', '-m', default=None)
parser.add_argument('--destination', '-d', default=None)
parser.add_argument('--artifact_path', '-a', nargs='*', default=None)
parser.add_argument(
'--retry-allowed-failures', action='store_true', help='Flag to retry jobs marked as allowed failures'
)
args = parser.parse_args()
gitlab_inst = Gitlab(args.project_id)
if args.action == 'download_artifacts':
gitlab_inst.download_artifacts(args.job_id, args.destination)
if args.action == 'download_artifact':
gitlab_inst.download_artifact(args.job_id, args.artifact_path, args.destination)
elif args.action == 'find_job_id':
job_ids = gitlab_inst.find_job_id(args.job_name, args.pipeline_id)
print(';'.join([','.join([str(j['id']), j['parallel_num']]) for j in job_ids]))
elif args.action == 'download_archive':
gitlab_inst.download_archive(args.ref, args.destination)
elif args.action == 'get_project_id':
ret = gitlab_inst.get_project_id(args.project_name)
print(f'project id: {ret}')
elif args.action == 'retry_failed_jobs':
res = gitlab_inst.retry_failed_jobs(args.pipeline_id, args.retry_allowed_failures)
print(f'jobs retried successfully: {res}')
elif args.action == 'get_job_tags':
ret = gitlab_inst.get_job_tags(args.job_id)
print(ret)
if __name__ == '__main__':
main()
@@ -0,0 +1,90 @@
#!/usr/bin/env python
#
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import argparse
import http.client
import logging
def start_session(ip, port):
return http.client.HTTPConnection(ip, int(port), timeout=15)
def end_session(conn):
conn.close()
def getreq(conn, path, verbose=False):
conn.request('GET', path)
resp = conn.getresponse()
data = resp.read()
if verbose:
logging.info('GET : {}'.format(path))
logging.info('Status : {}'.format(resp.status))
logging.info('Reason : {}'.format(resp.reason))
logging.info('Data length : {}'.format(len(data)))
logging.info('Data content : {}'.format(data))
return data
def postreq(conn, path, data, verbose=False):
conn.request('POST', path, data)
resp = conn.getresponse()
data = resp.read()
if verbose:
logging.info('POST : {}'.format(data))
logging.info('Status : {}'.format(resp.status))
logging.info('Reason : {}'.format(resp.reason))
logging.info('Data length : {}'.format(len(data)))
logging.info('Data content : {}'.format(data))
return data
def putreq(conn, path, body, verbose=False):
conn.request('PUT', path, body)
resp = conn.getresponse()
data = resp.read()
if verbose:
logging.info('PUT : {} {}'.format(path, body))
logging.info('Status : {}'.format(resp.status))
logging.info('Reason : {}'.format(resp.reason))
logging.info('Data length : {}'.format(len(data)))
logging.info('Data content : {}'.format(data))
return data
if __name__ == '__main__':
# Configure argument parser
parser = argparse.ArgumentParser(description='Run HTTPd Test')
parser.add_argument('IP', metavar='IP', type=str, help='Server IP')
parser.add_argument('port', metavar='port', type=str, help='Server port')
parser.add_argument('N', metavar='integer', type=int, help='Integer to sum upto')
args = vars(parser.parse_args())
# Get arguments
ip = args['IP']
port = args['port']
N = args['N']
# Establish HTTP connection
logging.info('Connecting to => ' + ip + ':' + port)
conn = start_session(ip, port)
# Reset adder context to specified value(0)
# -- Not needed as new connection will always
# -- have zero value of the accumulator
logging.info('Reset the accumulator to 0')
putreq(conn, '/adder', str(0))
# Sum numbers from 1 to specified value(N)
logging.info('Summing numbers from 1 to {}'.format(N))
for i in range(1, N + 1):
postreq(conn, '/adder', str(i))
# Fetch the result
logging.info('Result :{}'.format(getreq(conn, '/adder')))
# Close HTTP connection
end_session(conn)
@@ -0,0 +1,258 @@
#!/usr/bin/env python
#
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import argparse
import errno
import http.client
import logging
def verbose_print(verbosity, *args):
if (verbosity):
logging.info(''.join(str(elems) for elems in args))
def test_val(text, expected, received):
if expected != received:
logging.info(' Fail!')
logging.info(' [reason] {} :'.format(text))
logging.info(' expected: {}'.format(expected))
logging.info(' received: {}'.format(received))
return False
return True
def test_get_handler(ip, port, verbosity=False):
verbose_print(verbosity, '======== GET HANDLER TEST =============')
# Establish HTTP connection
verbose_print(verbosity, 'Connecting to => ' + ip + ':' + port)
sess = http.client.HTTPConnection(ip + ':' + port, timeout=15)
uri = '/hello?query1=value1&query2=value2&query3=value3'
# GET hello response
test_headers = {'Test-Header-1':'Test-Value-1', 'Test-Header-2':'Test-Value-2'}
verbose_print(verbosity, 'Sending GET to URI : ', uri)
verbose_print(verbosity, 'Sending additional headers : ')
for k, v in test_headers.items():
verbose_print(verbosity, '\t', k, ': ', v)
sess.request('GET', url=uri, headers=test_headers)
resp = sess.getresponse()
resp_hdrs = resp.getheaders()
resp_data = resp.read().decode()
# Close HTTP connection
sess.close()
if not (
test_val('Status code mismatch', 200, resp.status) and
test_val('Response mismatch', 'Custom-Value-1', resp.getheader('Custom-Header-1')) and
test_val('Response mismatch', 'Custom-Value-2', resp.getheader('Custom-Header-2')) and
test_val('Response mismatch', 'Hello World!', resp_data)
):
return False
verbose_print(verbosity, 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv')
verbose_print(verbosity, 'Server response to GET /hello')
verbose_print(verbosity, 'Response Headers : ')
for k, v in resp_hdrs:
verbose_print(verbosity, '\t', k, ': ', v)
verbose_print(verbosity, 'Response Data : ' + resp_data)
verbose_print(verbosity, '========================================\n')
return True
def test_post_handler(ip, port, msg, verbosity=False):
verbose_print(verbosity, '======== POST HANDLER TEST ============')
# Establish HTTP connection
verbose_print(verbosity, 'Connecting to => ' + ip + ':' + port)
sess = http.client.HTTPConnection(ip + ':' + port, timeout=15)
# POST message to /echo and get back response
sess.request('POST', url='/echo', body=msg)
resp = sess.getresponse()
resp_data = resp.read().decode()
verbose_print(verbosity, 'Server response to POST /echo (' + msg + ')')
verbose_print(verbosity, 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv')
verbose_print(verbosity, resp_data)
verbose_print(verbosity, '========================================\n')
# Close HTTP connection
sess.close()
return test_val('Response mismatch', msg, resp_data)
def test_put_handler(ip, port, verbosity=False):
verbose_print(verbosity, '======== PUT HANDLER TEST =============')
# Establish HTTP connection
verbose_print(verbosity, 'Connecting to => ' + ip + ':' + port)
sess = http.client.HTTPConnection(ip + ':' + port, timeout=15)
# PUT message to /ctrl to disable /hello and /echo URI handlers
# and set 404 error handler to custom http_404_error_handler()
verbose_print(verbosity, 'Disabling /hello and /echo handlers')
sess.request('PUT', url='/ctrl', body='0')
resp = sess.getresponse()
resp.read()
try:
# Send HTTP request to /hello URI
sess.request('GET', url='/hello')
resp = sess.getresponse()
resp_data = resp.read().decode()
# 404 Error must be returned from server as URI /hello is no longer available.
# But the custom error handler http_404_error_handler() will not close the
# session if the requested URI is /hello
if not test_val('Status code mismatch', 404, resp.status):
raise AssertionError
# Compare error response string with expectation
verbose_print(verbosity, 'Response on GET /hello : ' + resp_data)
if not test_val('Response mismatch', '/hello URI is not available', resp_data):
raise AssertionError
# Using same session for sending an HTTP request to /echo, as it is expected
# that the custom error handler http_404_error_handler() would not have closed
# the session
sess.request('POST', url='/echo', body='Some content')
resp = sess.getresponse()
resp_data = resp.read().decode()
# 404 Error must be returned from server as URI /hello is no longer available.
# The custom error handler http_404_error_handler() will close the session
# this time as the requested URI is /echo
if not test_val('Status code mismatch', 404, resp.status):
raise AssertionError
# Compare error response string with expectation
verbose_print(verbosity, 'Response on POST /echo : ' + resp_data)
if not test_val('Response mismatch', '/echo URI is not available', resp_data):
raise AssertionError
try:
# Using same session should fail as by now the session would have closed
sess.request('POST', url='/hello', body='Some content')
resp = sess.getresponse()
resp.read().decode()
# If control reaches this point then the socket was not closed.
# This is not expected
verbose_print(verbosity, 'Socket not closed by server')
raise AssertionError
except http.client.HTTPException:
# Catch socket error as we tried to communicate with an already closed socket
pass
except IOError as err:
if err.errno == errno.EPIPE:
# Sometimes Broken Pipe error is returned
# when sending data to a closed socket
pass
except http.client.HTTPException:
verbose_print(verbosity, 'Socket closed by server')
return False
except AssertionError:
return False
finally:
# Close HTTP connection
sess.close()
verbose_print(verbosity, 'Enabling /hello handler')
# Create new connection
sess = http.client.HTTPConnection(ip + ':' + port, timeout=15)
# PUT message to /ctrl to enable /hello URI handler
# and restore 404 error handler to default
sess.request('PUT', url='/ctrl', body='1')
resp = sess.getresponse()
resp.read()
# Close HTTP connection
sess.close()
# Create new connection
sess = http.client.HTTPConnection(ip + ':' + port, timeout=15)
try:
# Sending HTTP request to /hello should work now
sess.request('GET', url='/hello')
resp = sess.getresponse()
resp_data = resp.read().decode()
if not test_val('Status code mismatch', 200, resp.status):
raise AssertionError
verbose_print(verbosity, 'Response on GET /hello : ' + resp_data)
if not test_val('Response mismatch', 'Hello World!', resp_data):
raise AssertionError
# 404 Error handler should have been restored to default
sess.request('GET', url='/invalid')
resp = sess.getresponse()
resp_data = resp.read().decode()
if not test_val('Status code mismatch', 404, resp.status):
raise AssertionError
verbose_print(verbosity, 'Response on GET /invalid : ' + resp_data)
if not test_val('Response mismatch', 'Nothing matches the given URI', resp_data):
raise AssertionError
except http.client.HTTPException:
verbose_print(verbosity, 'Socket closed by server')
return False
except AssertionError:
return False
finally:
# Close HTTP connection
sess.close()
return True
def test_custom_uri_query(ip, port, query, verbosity=False):
verbose_print(verbosity, '======== GET HANDLER TEST =============')
# Establish HTTP connection
verbose_print(verbosity, 'Connecting to => ' + ip + ':' + port)
sess = http.client.HTTPConnection(ip + ':' + port, timeout=15)
uri = '/hello?' + query
# GET hello response
verbose_print(verbosity, 'Sending GET to URI : ', uri)
sess.request('GET', url=uri, headers={})
resp = sess.getresponse()
resp_data = resp.read().decode()
verbose_print(verbosity, 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv')
verbose_print(verbosity, 'Server response to GET /hello')
verbose_print(verbosity, 'Response Data : ' + resp_data)
verbose_print(verbosity, '========================================\n')
# Close HTTP connection
sess.close()
return 'Hello World!' == resp_data
if __name__ == '__main__':
# Configure argument parser
parser = argparse.ArgumentParser(description='Run HTTPd Test')
parser.add_argument('IP', metavar='IP', type=str, help='Server IP')
parser.add_argument('port', metavar='port', type=str, help='Server port')
parser.add_argument('msg', metavar='message', type=str, help='Message to be sent to server')
args = vars(parser.parse_args())
# Get arguments
ip = args['IP']
port = args['port']
msg = args['msg']
if not (
test_get_handler(ip, port, True) and
test_put_handler(ip, port, True) and
test_post_handler(ip, port, msg, True)
):
logging.info('Failed!')
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
# SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
"""
Internal use only.
This file provide method to control programmable attenuator.
"""
import codecs
import time
import serial
def set_att(port, att, att_fix=False):
"""
set attenuation value on the attenuator
:param port: serial port for attenuator
:param att: attenuation value we want to set
:param att_fix: fix the deviation with experience value
:return: True or False
"""
assert 0 <= att <= 62
# fix att
if att_fix:
if att >= 33 and (att - 30 + 1) % 4 == 0:
att_t = att - 1
elif att >= 33 and (att - 30) % 4 == 0:
att_t = att + 1
else:
att_t = att
else:
att_t = att
serial_port = serial.Serial(port, baudrate=9600, rtscts=False, timeout=0.1)
if serial_port.isOpen() is False:
raise IOError('attenuator control, failed to open att port')
cmd_hex = '7e7e10{:02x}{:x}'.format(att_t, 0x10 + att_t)
exp_res_hex = '7e7e20{:02x}00{:x}'.format(att_t, 0x20 + att_t)
cmd = codecs.decode(cmd_hex, 'hex')
exp_res = codecs.decode(exp_res_hex, 'hex')
serial_port.write(cmd)
res = b''
for i in range(5):
res += serial_port.read(20)
if res == exp_res:
result = True
break
time.sleep(0.1)
else:
result = False
serial_port.close()
return result
@@ -0,0 +1,518 @@
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import logging
import os
import re
import subprocess
import time
import pexpect
from pytest_embedded import Dut
from idf_iperf_test_util import LineChart
try:
from typing import Any
except ImportError:
# Only used for type annotations
pass
# configurations
TEST_TIME = TEST_TIMEOUT = 66
WAIT_AP_POWER_ON_TIMEOUT = 90
SCAN_TIMEOUT = 3
SCAN_RETRY_COUNT = 3
# constants
FAILED_TO_SCAN_RSSI = -97
INVALID_HEAP_SIZE = 0xFFFFFFFF
PC_IPERF_TEMP_LOG_FILE = '.tmp_iperf.log'
class TestResult:
"""record, analysis test result and convert data to output format"""
PC_BANDWIDTH_LOG_PATTERN = re.compile(r'(\d+\.\d+)\s*-\s*(\d+.\d+)\s+sec\s+[\d.]+\s+MBytes\s+([\d.]+)\s+Mbits\/sec')
DUT_BANDWIDTH_LOG_PATTERN = re.compile(r'([\d.]+)\s*-\s*([\d.]+)\s+sec\s+([\d.]+)\s+Mbits/sec')
ZERO_POINT_THRESHOLD = -88 # RSSI, dbm
ZERO_THROUGHPUT_THRESHOLD = -92 # RSSI, dbm
BAD_POINT_RSSI_THRESHOLD = -75 # RSSI, dbm
BAD_POINT_MIN_THRESHOLD = 10 # Mbps
BAD_POINT_PERCENTAGE_THRESHOLD = 0.3
# we need at least 1/2 valid points to qualify the test result
THROUGHPUT_QUALIFY_COUNT = TEST_TIME // 2
RSSI_RANGE = [-x for x in range(10, 100)]
ATT_RANGE = [x for x in range(0, 64)]
def __init__(self, proto: str, direction: str, config_name: str) -> None:
self.proto = proto
self.direction = direction
self.config_name = config_name
self.throughput_by_rssi = dict() # type: dict
self.throughput_by_att = dict() # type: dict
self.att_rssi_map = dict() # type: dict
self.heap_size = INVALID_HEAP_SIZE
self.error_list = [] # type: list[str]
def _save_result(self, throughput: float, ap_ssid: str, att: int, rssi: int, heap_size: str) -> None:
"""
save the test results:
* record the better throughput if att/rssi is the same.
* record the min heap size.
"""
if ap_ssid not in self.att_rssi_map:
# for new ap, create empty dict()
self.throughput_by_att[ap_ssid] = dict()
self.throughput_by_rssi[ap_ssid] = dict()
self.att_rssi_map[ap_ssid] = dict()
self.att_rssi_map[ap_ssid][att] = rssi
def record_throughput(database: dict, key_value: int) -> None:
try:
# we save the larger value for same att
if throughput > database[ap_ssid][key_value]:
database[ap_ssid][key_value] = throughput
except KeyError:
database[ap_ssid][key_value] = throughput
record_throughput(self.throughput_by_att, att)
record_throughput(self.throughput_by_rssi, rssi)
if int(heap_size) < self.heap_size:
self.heap_size = int(heap_size)
def add_result(self, raw_data: str, ap_ssid: str, att: int, rssi: int, heap_size: str) -> float:
"""
add result for one test
:param raw_data: iperf raw data
:param ap_ssid: ap ssid that tested
:param att: attenuate value
:param rssi: AP RSSI
:param heap_size: min heap size during test
:return: throughput
"""
fall_to_0_recorded = 0
throughput_list = []
max_throughput = 0.0
result_list = self.PC_BANDWIDTH_LOG_PATTERN.findall(raw_data)
if not result_list:
# failed to find raw data by PC pattern, it might be DUT pattern
result_list = self.DUT_BANDWIDTH_LOG_PATTERN.findall(raw_data)
for result in result_list:
t_start = float(result[0])
t_end = float(result[1])
throughput = float(result[2])
if int(t_end - t_start) != 1:
# this could be summary, ignore this
continue
throughput_list.append(throughput)
max_throughput = max(max_throughput, throughput)
if throughput == 0 and rssi > self.ZERO_POINT_THRESHOLD and fall_to_0_recorded < 1:
# throughput fall to 0 error. we only record 1 records for one test
self.error_list.append(
f'[Error][fall to 0][{ap_ssid}][att: {att}][rssi: {rssi}]: 0 '
f'throughput interval: {result[0]}-{result[1]}'
)
fall_to_0_recorded += 1
if len(throughput_list) < self.THROUGHPUT_QUALIFY_COUNT:
self.error_list.append(
f'[Error][Fatal][{ap_ssid}][att: {att}][rssi: {rssi}]: '
f'Only {len(throughput_list)} throughput values found, '
f'expected at least {self.THROUGHPUT_QUALIFY_COUNT}'
)
max_throughput = 0.0
if max_throughput == 0 and rssi > self.ZERO_THROUGHPUT_THRESHOLD:
self.error_list.append(f'[Error][Fatal][{ap_ssid}][att: {att}][rssi: {rssi}]: No throughput data found')
self._save_result(max_throughput, ap_ssid, att, rssi, heap_size)
return max_throughput
def post_analysis(self) -> None:
"""
some rules need to be checked after we collected all test raw data:
1. throughput value 30% worse than the next point with lower RSSI
2. throughput value 30% worse than the next point with larger attenuate
"""
def analysis_bad_point(data: dict, index_type: str) -> None:
for ap_ssid in data:
result_dict = data[ap_ssid]
index_list = list(result_dict.keys())
index_list.sort()
if index_type == 'att':
index_list.reverse()
for i, index_value in enumerate(index_list[1:]):
if (
index_value < self.BAD_POINT_RSSI_THRESHOLD
or result_dict[index_list[i]] < self.BAD_POINT_MIN_THRESHOLD
):
continue
_percentage = result_dict[index_value] / result_dict[index_list[i]]
if _percentage < 1 - self.BAD_POINT_PERCENTAGE_THRESHOLD:
self.error_list.append(
f'[Error][Bad point][{ap_ssid}][{index_type}: {index_value}]: '
f'drop {(1 - _percentage) * 100:.02f}%'
)
analysis_bad_point(self.throughput_by_rssi, 'rssi')
analysis_bad_point(self.throughput_by_att, 'att')
def draw_throughput_figure(self, path: str, ap_ssid: str, draw_type: str) -> str:
"""
:param path: folder to save figure. make sure the folder is already created.
:param ap_ssid: ap ssid string or a list of ap ssid string
:param draw_type: "att" or "rssi"
:return: file_name
"""
if draw_type == 'rssi':
type_name = 'RSSI'
data = self.throughput_by_rssi
range_list = self.RSSI_RANGE
elif draw_type == 'att':
type_name = 'Att'
data = self.throughput_by_att
range_list = self.ATT_RANGE
else:
raise AssertionError('draw type not supported')
if isinstance(ap_ssid, list):
file_name = f'ThroughputVs{type_name}_{self.proto}_{self.direction}.html'
else:
file_name = f'ThroughputVs{type_name}_{self.proto}_{self.direction}.html'
LineChart.draw_line_chart(
os.path.join(path, file_name),
f'Throughput Vs {type_name} ({self.proto} {self.direction})',
f'{type_name} (dbm)',
'Throughput (Mbps)',
data,
range_list,
)
return file_name
def draw_rssi_vs_att_figure(self, path: str, ap_ssid: str) -> str:
"""
:param path: folder to save figure. make sure the folder is already created.
:param ap_ssid: ap to use
:return: file_name
"""
if isinstance(ap_ssid, list):
file_name = 'AttVsRSSI.html'
else:
file_name = 'AttVsRSSI.html'
LineChart.draw_line_chart(
os.path.join(path, file_name), 'Att Vs RSSI', 'Att (dbm)', 'RSSI (dbm)', self.att_rssi_map, self.ATT_RANGE
)
return file_name
def get_best_throughput(self) -> Any:
"""get the best throughput during test"""
best_for_aps = [max(self.throughput_by_att[ap_ssid].values()) for ap_ssid in self.throughput_by_att]
return max(best_for_aps)
def __str__(self) -> str:
"""
returns summary for this test:
1. test result (success or fail)
2. best performance for each AP
3. min free heap size during test
"""
if self.throughput_by_att:
ret = '[{}_{}][{}]: {}\r\n\r\n'.format(
self.proto, self.direction, self.config_name, 'Fail' if self.error_list else 'Success'
)
ret += 'Performance for each AP:\r\n'
for ap_ssid in self.throughput_by_att:
ret += f'[{ap_ssid}]: {max(self.throughput_by_att[ap_ssid].values()):.02f} Mbps\r\n'
if self.heap_size != INVALID_HEAP_SIZE:
ret += f'Minimum heap size: {self.heap_size}'
else:
ret = ''
return ret
class IperfTestUtility:
"""iperf test implementation"""
def __init__(
self,
dut: Dut,
config_name: str,
ap_ssid: str,
ap_password: str,
pc_nic_ip: str,
pc_iperf_log_file: str,
test_result: Any = None,
) -> None:
self.config_name = config_name
self.dut = dut
self.pc_iperf_log_file = pc_iperf_log_file
self.ap_ssid = ap_ssid
self.ap_password = ap_password
self.pc_nic_ip = pc_nic_ip
self.fail_to_scan = 0
self.lowest_rssi_scanned = 0
if test_result:
self.test_result = test_result
else:
self.test_result = {
'tcp_tx': TestResult('tcp', 'tx', config_name),
'tcp_rx': TestResult('tcp', 'rx', config_name),
'udp_tx': TestResult('udp', 'tx', config_name),
'udp_rx': TestResult('udp', 'rx', config_name),
}
def setup(self) -> tuple[str, int]:
"""
setup iperf test:
1. kill current iperf process
2. reboot DUT (currently iperf is not very robust, need to reboot DUT)
3. scan to get AP RSSI
4. connect to AP
"""
try:
subprocess.check_output('sudo killall iperf 2>&1 > /dev/null', shell=True)
except subprocess.CalledProcessError:
pass
time.sleep(5)
self.dut.write('restart')
self.dut.expect_exact("Type 'help' to get the list of commands.")
self.dut.expect('iperf>')
self.dut.write(f'sta_scan {self.ap_ssid}')
for _ in range(SCAN_RETRY_COUNT):
try:
rssi = int(self.dut.expect(rf'\[{self.ap_ssid}]\[rssi=(-\d+)]', timeout=SCAN_TIMEOUT).group(1))
break
except pexpect.TIMEOUT:
continue
else:
raise AssertionError('Failed to scan AP')
self.dut.write(f'sta_connect {self.ap_ssid} {self.ap_password}')
dut_ip = self.dut.expect(r'sta ip: ([\d.]+), mask: ([\d.]+), gw: ([\d.]+)').group(1)
return dut_ip, rssi
def _save_test_result(self, test_case: str, raw_data: str, att: int, rssi: int, heap_size: int) -> Any:
return self.test_result[test_case].add_result(raw_data, self.ap_ssid, att, rssi, heap_size)
def _test_once(self, proto: str, direction: str, bw_limit: int) -> tuple[str, int, int]:
"""do measure once for one type"""
# connect and scan to get RSSI
dut_ip, rssi = self.setup()
assert direction in ['rx', 'tx']
assert proto in ['tcp', 'udp']
# run iperf test
if direction == 'tx':
with open(PC_IPERF_TEMP_LOG_FILE, 'w') as f:
if proto == 'tcp':
process = subprocess.Popen(
['iperf', '-s', '-B', self.pc_nic_ip, '-t', str(TEST_TIME), '-i', '1', '-f', 'm'],
stdout=f,
stderr=f,
)
if bw_limit > 0:
self.dut.write(f'iperf -c {self.pc_nic_ip} -i 1 -t {TEST_TIME} -b {bw_limit}m')
else:
self.dut.write(f'iperf -c {self.pc_nic_ip} -i 1 -t {TEST_TIME}')
else:
process = subprocess.Popen(
['iperf', '-s', '-u', '-B', self.pc_nic_ip, '-t', str(TEST_TIME), '-i', '1', '-f', 'm'],
stdout=f,
stderr=f,
)
if bw_limit > 0:
self.dut.write(f'iperf -c {self.pc_nic_ip} -u -i 1 -t {TEST_TIME} -b {bw_limit}m')
else:
self.dut.write(f'iperf -c {self.pc_nic_ip} -u -i 1 -t {TEST_TIME}')
for _ in range(TEST_TIMEOUT):
if process.poll() is not None:
break
time.sleep(1)
else:
process.terminate()
with open(PC_IPERF_TEMP_LOG_FILE) as f:
pc_raw_data = server_raw_data = f.read()
else:
with open(PC_IPERF_TEMP_LOG_FILE, 'w') as f:
if proto == 'tcp':
self.dut.write(f'iperf -s -i 1 -t {TEST_TIME}')
# wait until DUT TCP server created
try:
self.dut.expect('Socket created', timeout=5)
except pexpect.TIMEOUT:
# compatible with old iperf example binary
logging.info('create iperf tcp server fail')
if bw_limit > 0:
process = subprocess.Popen(
['iperf', '-c', dut_ip, '-b', str(bw_limit) + 'm', '-t', str(TEST_TIME), '-f', 'm'],
stdout=f,
stderr=f,
)
else:
process = subprocess.Popen(
['iperf', '-c', dut_ip, '-t', str(TEST_TIME), '-f', 'm'], stdout=f, stderr=f
)
for _ in range(TEST_TIMEOUT):
if process.poll() is not None:
break
time.sleep(1)
else:
process.terminate()
else:
if bw_limit > 0:
self.dut.write(f'iperf -s -u -i 1 -t {TEST_TIME}')
# wait until DUT TCP server created
try:
self.dut.expect('Socket created', timeout=5)
except pexpect.TIMEOUT:
# compatible with old iperf example binary
logging.info('No "Socket created" confirmation received after starting UDP server')
process = subprocess.Popen(
['iperf', '-c', dut_ip, '-u', '-b', str(bw_limit) + 'm', '-t', str(TEST_TIME), '-f', 'm'],
stdout=f,
stderr=f,
)
for _ in range(TEST_TIMEOUT):
if process.poll() is not None:
break
time.sleep(1)
else:
process.terminate()
else:
start_bw = 50
stop_bw = 100
n = 10
step = int((stop_bw - start_bw) / n)
self.dut.write(
f'iperf -s -u -i 1 -t {TEST_TIME + 4 * (n + 1)}'
) # 4 sec for each bw step instance start/stop
# wait until DUT TCP server created
try:
self.dut.expect('Socket created', timeout=5)
except pexpect.TIMEOUT:
# compatible with old iperf example binary
logging.info('No "Socket created" confirmation received after starting UDP server')
for bandwidth in range(start_bw, stop_bw, step):
process = subprocess.Popen(
[
'iperf',
'-c',
dut_ip,
'-u',
'-b',
str(bandwidth) + 'm',
'-t',
str(TEST_TIME / (n + 1)),
'-f',
'm',
],
stdout=f,
stderr=f,
)
for _ in range(TEST_TIMEOUT):
if process.poll() is not None:
break
time.sleep(1)
else:
process.terminate()
server_raw_data = self.dut.expect(pexpect.TIMEOUT, timeout=5).decode('utf-8')
with open(PC_IPERF_TEMP_LOG_FILE) as f:
pc_raw_data = f.read()
if os.path.exists(PC_IPERF_TEMP_LOG_FILE):
os.remove(PC_IPERF_TEMP_LOG_FILE)
# save PC iperf logs to console
with open(self.pc_iperf_log_file, 'a+') as f:
f.write(
'## [{}] `{}`\r\n##### {}'.format(
self.config_name,
f'{proto}_{direction}',
time.strftime('%m-%d %H:%M:%S', time.localtime(time.time())),
)
)
f.write('\r\n```\r\n\r\n' + pc_raw_data + '\r\n```\r\n')
self.dut.write('heap')
heap_size = self.dut.expect(r'min heap size: (\d+)\D', timeout=120).group(1)
# return server raw data (for parsing test results) and RSSI
return server_raw_data, rssi, heap_size
def run_test(self, proto: str, direction: str, atten_val: int, bw_limit: int) -> None:
"""
run test for one type, with specified atten_value and save the test result
:param proto: tcp or udp
:param direction: tx or rx
:param atten_val: attenuate value
:param bw_limit: bandwidth limit
"""
rssi = FAILED_TO_SCAN_RSSI
heap_size = INVALID_HEAP_SIZE
try:
server_raw_data, rssi, heap_size = self._test_once(proto, direction, bw_limit)
throughput = self._save_test_result(f'{proto}_{direction}', server_raw_data, atten_val, rssi, heap_size)
logging.info(f'[{self.config_name}][{proto}_{direction}][{rssi}][{self.ap_ssid}]: {throughput:.02f}')
self.lowest_rssi_scanned = min(self.lowest_rssi_scanned, rssi)
except (ValueError, IndexError):
self._save_test_result(f'{proto}_{direction}', '', atten_val, rssi, heap_size)
logging.info('Fail to get throughput results.')
except AssertionError:
self.fail_to_scan += 1
logging.info('Fail to scan AP.')
def run_all_cases(self, atten_val: int, bw_limit: int) -> None:
"""
run test for all types (udp_tx, udp_rx, tcp_tx, tcp_rx).
:param atten_val: attenuate value
:param bw_limit: bandwidth limit
"""
self.run_test('tcp', 'tx', atten_val, bw_limit)
self.run_test('tcp', 'rx', atten_val, bw_limit)
self.run_test('udp', 'tx', atten_val, bw_limit)
self.run_test('udp', 'rx', atten_val, bw_limit)
if self.fail_to_scan > 10:
logging.info(f'Fail to scan AP for more than 10 times. Lowest RSSI scanned is {self.lowest_rssi_scanned}')
raise AssertionError
def wait_ap_power_on(self) -> bool:
"""
AP need to take sometime to power on. It changes for different APs.
This method will scan to check if the AP powers on.
:return: True or False
"""
self.dut.write('restart')
self.dut.expect('iperf>')
for _ in range(WAIT_AP_POWER_ON_TIMEOUT // SCAN_TIMEOUT):
try:
self.dut.write(f'scan {self.ap_ssid}')
self.dut.expect(rf'\[{self.ap_ssid}]\[rssi=(-\d+)]', timeout=SCAN_TIMEOUT)
ret = True
break
except pexpect.TIMEOUT:
pass
else:
ret = False
return ret
@@ -0,0 +1,47 @@
# SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from collections import OrderedDict
import pyecharts.options as opts
from pyecharts.charts import Line
def draw_line_chart(file_name, title, x_label, y_label, data_series, range_list):
"""
draw line chart and save to file.
:param file_name: abs/relative file name to save chart figure
:param title: chart title
:param x_label: x-axis label
:param y_label: y-axis label
:param data_series: a dict {"name": data}. data is a dict.
:param range_list: a list of x-axis range
"""
line = Line()
# echarts do not support minus number for x axis, convert to string
_range_list = [str(x) for x in range_list]
line.add_xaxis(_range_list)
for item in data_series:
_data = OrderedDict.fromkeys(_range_list, None)
for key in data_series[item]:
_data[str(key)] = data_series[item][key]
_data = list(_data.values())
try:
legend = item + ' (max: {:.02f})'.format(max([x for x in _data if x]))
except TypeError:
legend = item
line.add_yaxis(legend, _data, is_smooth=True, is_connect_nones=True,
label_opts=opts.LabelOpts(is_show=False))
line.set_global_opts(
datazoom_opts=opts.DataZoomOpts(range_start=0, range_end=100),
title_opts=opts.TitleOpts(title=title, pos_left='center'),
legend_opts=opts.LegendOpts(pos_top='10%', pos_left='right', orient='vertical'),
tooltip_opts=opts.TooltipOpts(trigger='axis'),
xaxis_opts=opts.AxisOpts(type_='category', name=x_label, splitline_opts=opts.SplitLineOpts(is_show=True)),
yaxis_opts=opts.AxisOpts(type_='value', name=y_label,
axistick_opts=opts.AxisTickOpts(is_show=True),
splitline_opts=opts.SplitLineOpts(is_show=True)),
)
line.render(file_name)
@@ -0,0 +1,84 @@
# SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
"""
Internal use only.
This file implements controlling APC PDU via telnet.
"""
import telnetlib
class Control(object):
""" control APC via telnet """
@classmethod
def apc_telnet_make_choice(cls, telnet, choice):
""" select a choice """
telnet.read_until(b'Event Log')
telnet.read_until(b'>')
telnet.write(choice.encode() + b'\r\n')
@classmethod
def apc_telnet_common_action(cls, telnet, check_str, action):
""" wait until a pattern and then write a line """
telnet.read_until(check_str.encode())
telnet.write(action.encode() + b'\r\n')
@classmethod
def control(cls, apc_ip, control_dict):
"""
control APC
:param apc_ip: IP of APC
:param control_dict: dict with outlet ID and "ON" or "OFF"
"""
for _outlet in control_dict:
assert 0 < _outlet < 9
assert control_dict[_outlet] in ['ON', 'OFF']
# telnet
# set timeout as 2s so that it won't waste time even can't access APC
tn = telnetlib.Telnet(host=apc_ip, timeout=5)
# log on
cls.apc_telnet_common_action(tn, 'User Name :', 'apc')
cls.apc_telnet_common_action(tn, 'Password :', 'apc')
# go to Device Manager
cls.apc_telnet_make_choice(tn, '1')
# go to Outlet Management
cls.apc_telnet_make_choice(tn, '2')
# go to Outlet Control/Configuration
cls.apc_telnet_make_choice(tn, '1')
# do select Outlet and control
for _outlet in control_dict:
# choose Outlet
cls.apc_telnet_make_choice(tn, str(_outlet))
# choose Control Outlet
cls.apc_telnet_make_choice(tn, '1')
# choose action
_action = control_dict[_outlet]
if 'ON' in _action:
cls.apc_telnet_make_choice(tn, '1')
else:
cls.apc_telnet_make_choice(tn, '2')
# do confirm
cls.apc_telnet_common_action(tn, 'cancel :', 'YES')
cls.apc_telnet_common_action(tn, 'continue...', '')
# return to Outlet Control/Configuration
cls.apc_telnet_make_choice(tn, '\033')
cls.apc_telnet_make_choice(tn, '\033')
# exit to main menu and logout
tn.write(b'\033\r\n')
tn.write(b'\033\r\n')
tn.write(b'\033\r\n')
tn.write(b'4\r\n')
@classmethod
def control_rest(cls, apc_ip, outlet, action):
outlet_list = list(range(1, 9)) # has to be a list if we want to remove from it under Python 3
outlet_list.remove(outlet)
cls.control(apc_ip, dict.fromkeys(outlet_list, action))
@@ -0,0 +1,247 @@
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
"""
this module generates markdown format test report for throughput test.
The test report contains 2 parts:
1. throughput with different configs
2. throughput with RSSI
"""
import os
class ThroughputForConfigsReport(object):
THROUGHPUT_TYPES = ['tcp_tx', 'tcp_rx', 'udp_tx', 'udp_rx']
REPORT_FILE_NAME = 'ThroughputForConfigs.md'
def __init__(self, output_path, ap_ssid, throughput_results, sdkconfig_files):
"""
:param ap_ssid: the ap we expected to use
:param throughput_results: config with the following type::
{
"config_name": {
"tcp_tx": result,
"tcp_rx": result,
"udp_tx": result,
"udp_rx": result,
},
"config_name2": {},
}
"""
self.output_path = output_path
self.ap_ssid = ap_ssid
self.results = throughput_results
self.sdkconfigs = dict()
for config_name in sdkconfig_files:
self.sdkconfigs[config_name] = self._parse_config_file(sdkconfig_files[config_name])
if not os.path.exists(output_path):
os.makedirs(output_path)
self.sort_order = list(self.sdkconfigs.keys())
self.sort_order.sort()
@staticmethod
def _parse_config_file(config_file_path):
sdkconfig = {}
with open(config_file_path, 'r') as f:
for line in f:
if not line.isspace():
if line[0] == '#':
continue
name, value = line.split('=')
value = value.strip('\r\n')
sdkconfig[name] = value if value else 'n'
return sdkconfig
def _generate_the_difference_between_configs(self):
"""
generate markdown list for different configs::
default: esp-idf default
low:
* `config name 1`: old value -> new value
* `config name 2`: old value -> new value
* ...
...
"""
data = '## Config Definition:\r\n\r\n'
def find_difference(base, new):
_difference = {}
all_configs = set(base.keys())
all_configs.update(set(new.keys()))
for _config in all_configs:
try:
_base_value = base[_config]
except KeyError:
_base_value = 'null'
try:
_new_value = new[_config]
except KeyError:
_new_value = 'null'
if _base_value != _new_value:
_difference[_config] = '{} -> {}'.format(_base_value, _new_value)
return _difference
for i, _config_name in enumerate(self.sort_order):
current_config = self.sdkconfigs[_config_name]
if i > 0:
previous_config_name = self.sort_order[i - 1]
previous_config = self.sdkconfigs[previous_config_name]
else:
previous_config = previous_config_name = None
if previous_config:
# log the difference
difference = find_difference(previous_config, current_config)
data += '* {} (compared to {}):\r\n'.format(_config_name, previous_config_name)
for diff_name in difference:
data += ' * `{}`: {}\r\n'.format(diff_name, difference[diff_name])
return data
def _generate_report_for_one_type(self, throughput_type):
"""
generate markdown table with the following format::
| config name | throughput (Mbps) | free heap size (bytes) |
|-------------|-------------------|------------------------|
| default | 32.11 | 147500 |
| low | 32.11 | 147000 |
| medium | 33.22 | 120000 |
| high | 43.11 | 100000 |
| max | 45.22 | 79000 |
"""
empty = True
ret = '\r\n### {} {}\r\n\r\n'.format(*throughput_type.split('_'))
ret += '| config name | throughput (Mbps) | free heap size (bytes) |\r\n'
ret += '|-------------|-------------------|------------------------|\r\n'
for config in self.sort_order:
try:
result = self.results[config][throughput_type]
throughput = '{:.02f}'.format(max(result.throughput_by_att[self.ap_ssid].values()))
heap_size = str(result.heap_size)
# although markdown table will do alignment
# do align here for better text editor presentation
ret += '| {:<12}| {:<18}| {:<23}|\r\n'.format(config, throughput, heap_size)
empty = False
except KeyError:
pass
return ret if not empty else ''
def generate_report(self):
data = '# Throughput for different configs\r\n'
data += '\r\nAP: {}\r\n'.format(self.ap_ssid)
for throughput_type in self.THROUGHPUT_TYPES:
data += self._generate_report_for_one_type(throughput_type)
data += '\r\n------\r\n'
data += self._generate_the_difference_between_configs()
with open(os.path.join(self.output_path, self.REPORT_FILE_NAME), 'w') as f:
f.write(data)
class ThroughputVsRssiReport(object):
REPORT_FILE_NAME = 'ThroughputVsRssi.md'
def __init__(self, output_path, throughput_results):
"""
:param throughput_results: config with the following type::
{
"tcp_tx": result,
"tcp_rx": result,
"udp_tx": result,
"udp_rx": result,
}
"""
self.output_path = output_path
self.raw_data_path = os.path.join(output_path, 'raw_data')
self.results = throughput_results
self.throughput_types = list(self.results.keys())
self.throughput_types.sort()
if not os.path.exists(self.raw_data_path):
os.makedirs(self.raw_data_path)
def _generate_summary(self):
"""
generate summary with the following format::
| item | curve analysis | max throughput (Mbps) |
|---------|----------------|-----------------------|
| tcp tx | Success | 32.11 |
| tcp rx | Success | 32.11 |
| udp tx | Success | 45.22 |
| udp rx | Failed | 55.44 |
"""
ret = '\r\n### Summary\r\n\r\n'
ret += '| item | curve analysis | max throughput (Mbps) |\r\n'
ret += '|---------|----------------|-----------------------|\r\n'
for _type in self.throughput_types:
result = self.results[_type]
max_throughput = 0.0
curve_analysis = 'Failed' if result.error_list else 'Success'
for ap_ssid in result.throughput_by_att:
_max_for_ap = max(result.throughput_by_rssi[ap_ssid].values())
if _max_for_ap > max_throughput:
max_throughput = _max_for_ap
max_throughput = '{:.02f}'.format(max_throughput)
ret += '| {:<8}| {:<15}| {:<22}|\r\n'.format('{}_{}'.format(result.proto, result.direction),
curve_analysis, max_throughput)
return ret
def _generate_report_for_one_type(self, result):
"""
generate markdown table with the following format::
### tcp rx
Errors:
* detected error 1
* ...
AP: ap_ssid
![throughput Vs RSSI](path to figure)
AP: ap_ssid
![throughput Vs RSSI](path to figure)
"""
result.post_analysis()
ret = '\r\n### {} {}\r\n'.format(result.proto, result.direction)
if result.error_list:
ret += '\r\nErrors:\r\n\r\n'
for error in result.error_list:
ret += '* ' + error + '\r\n'
for ap_ssid in result.throughput_by_rssi:
ret += '\r\nAP: {}\r\n'.format(ap_ssid)
# draw figure
file_name = result.draw_throughput_figure(self.raw_data_path, ap_ssid, 'rssi')
result.draw_throughput_figure(self.raw_data_path, ap_ssid, 'att')
result.draw_rssi_vs_att_figure(self.raw_data_path, ap_ssid)
ret += '\r\n[throughput Vs RSSI]({})\r\n'.format(os.path.join('raw_data', file_name))
return ret
def generate_report(self):
data = '# Throughput Vs RSSI\r\n'
data += self._generate_summary()
for _type in self.throughput_types:
data += self._generate_report_for_one_type(self.results[_type])
with open(os.path.join(self.output_path, self.REPORT_FILE_NAME), 'w') as f:
f.write(data)
+120
View File
@@ -0,0 +1,120 @@
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
import time
import dbus
import dbus.mainloop.glib
import netifaces
def get_wiface_name():
for iface in netifaces.interfaces():
if iface.startswith('w'):
return iface
return None
def get_wiface_IPv4(iface):
try:
[info] = netifaces.ifaddresses(iface)[netifaces.AF_INET]
return info['addr']
except KeyError:
return None
class wpa_cli:
def __init__(self, iface, reset_on_exit=False):
self.iface_name = iface
self.iface_obj = None
self.iface_ifc = None
self.old_network = None
self.new_network = None
self.connected = False
self.reset_on_exit = reset_on_exit
try:
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
service = dbus.Interface(bus.get_object('fi.w1.wpa_supplicant1', '/fi/w1/wpa_supplicant1'),
'fi.w1.wpa_supplicant1')
iface_path = service.GetInterface(self.iface_name)
self.iface_obj = bus.get_object('fi.w1.wpa_supplicant1', iface_path)
self.iface_ifc = dbus.Interface(self.iface_obj, 'fi.w1.wpa_supplicant1.Interface')
self.iface_props = dbus.Interface(self.iface_obj, 'org.freedesktop.DBus.Properties')
if self.iface_ifc is None:
raise RuntimeError('supplicant : Failed to fetch interface')
self.old_network = self._get_iface_property('CurrentNetwork')
print('Old network is %s' % self.old_network)
if self.old_network == '/':
self.old_network = None
else:
self.connected = True
except Exception as err:
raise Exception('Failure in wpa_cli init: {}'.format(err))
def _get_iface_property(self, name):
""" Read the property with 'name' from the wi-fi interface object
Note: The result is a dbus wrapped type, so should usually convert it to the corresponding native
Python type
"""
return self.iface_props.Get('fi.w1.wpa_supplicant1.Interface', name)
def connect(self, ssid, password):
try:
if self.connected is True:
self.iface_ifc.Disconnect()
self.connected = False
if self.new_network is not None:
self.iface_ifc.RemoveNetwork(self.new_network)
print('Pre-connect state is %s, IP is %s' % (self._get_iface_property('State'), get_wiface_IPv4(self.iface_name)))
self.new_network = self.iface_ifc.AddNetwork({'ssid': ssid, 'psk': password})
self.iface_ifc.SelectNetwork(self.new_network)
time.sleep(10)
ip = None
retry = 10
while retry > 0:
time.sleep(5)
state = str(self._get_iface_property('State'))
print('wpa iface state %s (scanning %s)' % (state, bool(self._get_iface_property('Scanning'))))
if state in ['disconnected', 'inactive']:
self.iface_ifc.Reconnect()
ip = get_wiface_IPv4(self.iface_name)
print('wpa iface %s IP %s' % (self.iface_name, ip))
if ip is not None:
self.connected = True
return ip
retry -= 1
time.sleep(3)
self.reset()
print('wpa_cli : Connection failed')
except Exception as err:
raise Exception('Failure in wpa_cli init: {}'.format(err))
def reset(self):
if self.iface_ifc is not None:
if self.connected is True:
self.iface_ifc.Disconnect()
self.connected = False
if self.new_network is not None:
self.iface_ifc.RemoveNetwork(self.new_network)
self.new_network = None
if self.old_network is not None:
self.iface_ifc.SelectNetwork(self.old_network)
self.old_network = None
def __del__(self):
if self.reset_on_exit is True:
self.reset()