chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
AP: ap_ssid
|
||||
|
||||

|
||||
|
||||
"""
|
||||
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)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user