diff --git a/.github/workflows/continuous-integration-tools.yml b/.github/workflows/continuous-integration-tools.yml index 887e406..43cb605 100644 --- a/.github/workflows/continuous-integration-tools.yml +++ b/.github/workflows/continuous-integration-tools.yml @@ -20,12 +20,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: [3.9] + python-version: ['3.13'] steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -34,8 +34,8 @@ jobs: python -m pip install tox - name: Test sub tools run: | - tox -e py$(echo ${{ matrix.python-version }} | tr -d .)-tools + tox -e py$(echo ${{ matrix.python-version }} | tr -d .)-tools - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 with: fail_ci_if_error: true diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 68c46f4..03a5aa1 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -20,51 +20,51 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.13', '3.14'] steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install tox + python -m pip install tox hatch - name: Test with tox run: | tox -e py$(echo ${{ matrix.python-version }} | tr -d .)-tests - name: Test cli run: | - tox -e py$(echo ${{ matrix.python-version }} | tr -d .)-cli + tox -e py$(echo ${{ matrix.python-version }} | tr -d .)-cli - name: Check style - if: ${{ matrix.python-version == 3.8 }} + if: ${{ matrix.python-version == '3.13' }} run: | - tox -e py$(echo ${{ matrix.python-version }} | tr -d .)-lint + tox -e py313-lint tox -e copying - name: Upload coverage to Codecov - if: ${{ (matrix.python-version == 3.8 || matrix.python-version == 2.7) }} - uses: codecov/codecov-action@v1 + if: ${{ matrix.python-version == '3.13' }} + uses: codecov/codecov-action@v4 with: fail_ci_if_error: true - - name: Build a source tarball - if: matrix.python-version == 3.8 - run: python setup.py sdist check --strict --metadata + - name: Build package + if: matrix.python-version == '3.13' + run: hatch build - name: Publish package to TestPyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 if: >- - matrix.python-version == 3.8 && + matrix.python-version == '3.13' && github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') with: user: __token__ password: ${{ secrets.test_pypi_password }} - repository_url: https://test.pypi.org/legacy/ + repository-url: https://test.pypi.org/legacy/ - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 if: >- - matrix.python-version == 3.8 && + matrix.python-version == '3.13' && github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') with: diff --git a/AUTHORS b/AUTHORS index 778da1a..68af953 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,2 +1,3 @@ frederic.saint-marcel@inria.fr gaetan.harter@inria.fr +alexandre.abadie@inria.fr diff --git a/DEV.md b/DEV.md index 4457053..d457896 100644 --- a/DEV.md +++ b/DEV.md @@ -13,7 +13,7 @@ You can run all tests with: Running test for one specific version - tox -e py37 + tox -e py313 Step by step validation @@ -24,12 +24,3 @@ Step by step validation Development depencencies can be installed with pip install -r tests_utils/test-requirements - - -### Manually running tests ### - - python setup.py lint - python setup.py pep8 - flake8 # it does not work from setup.py script - python setup.py nosetests - diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 6a514f6..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -# include help .md files -include iotlabcli/parser/help/* -include CHANGELOG.rst -include AUTHORS -include COPYING diff --git a/README.rst b/README.rst index de45315..e6e09a2 100644 --- a/README.rst +++ b/README.rst @@ -76,14 +76,11 @@ Description The cli-tools leverage the IoT-LAB ``REST API`` and simply wrap calls to module ``iotlabcli``, which is a Python client for the API. -The cli-tools come as an installable Python package and require that -module ``setuptools`` be installed before tools installation can happen. -Please grab the relevant python-setuptools package for your -distribution. +The cli-tools come as an installable Python package. To install cli-tools from Pypi, use ``pip install iotlabcli``. -To install cli-tools from source, use ``pip install --user .`` or ``python setup.py install`` +To install cli-tools from source, use ``pip install --user .`` Installing cli-tools automatically fetches additional dependencies as needed. diff --git a/iotlab b/iotlab deleted file mode 100755 index 6c75372..0000000 --- a/iotlab +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -# This file is a part of IoT-LAB cli-tools -# Copyright (C) 2015 INRIA (Contact: admin@iot-lab.info) -# Contributor(s) : see AUTHORS file -# -# This software is governed by the CeCILL license under French law -# and abiding by the rules of distribution of free software. You can use, -# modify and/ or redistribute the software under the terms of the CeCILL -# license as circulated by CEA, CNRS and INRIA at the following URL -# http://www.cecill.info. -# -# As a counterpart to the access to the source code and rights to copy, -# modify and redistribute granted by the license, users are provided only -# with a limited warranty and the software's author, the holder of the -# economic rights, and the successive licensors have only limited -# liability. -# -# The fact that you are presently reading this means that you have had -# knowledge of the CeCILL license and that you accept its terms. - -import iotlabcli.parser.main -iotlabcli.parser.main.main() diff --git a/iotlab-auth b/iotlab-auth deleted file mode 100755 index c46b885..0000000 --- a/iotlab-auth +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -# This file is a part of IoT-LAB cli-tools -# Copyright (C) 2015 INRIA (Contact: admin@iot-lab.info) -# Contributor(s) : see AUTHORS file -# -# This software is governed by the CeCILL license under French law -# and abiding by the rules of distribution of free software. You can use, -# modify and/ or redistribute the software under the terms of the CeCILL -# license as circulated by CEA, CNRS and INRIA at the following URL -# http://www.cecill.info. -# -# As a counterpart to the access to the source code and rights to copy, -# modify and redistribute granted by the license, users are provided only -# with a limited warranty and the software's author, the holder of the -# economic rights, and the successive licensors have only limited -# liability. -# -# The fact that you are presently reading this means that you have had -# knowledge of the CeCILL license and that you accept its terms. - -import iotlabcli.parser.auth -iotlabcli.parser.auth.main() diff --git a/iotlab-experiment b/iotlab-experiment deleted file mode 100755 index 4d11697..0000000 --- a/iotlab-experiment +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -# This file is a part of IoT-LAB cli-tools -# Copyright (C) 2015 INRIA (Contact: admin@iot-lab.info) -# Contributor(s) : see AUTHORS file -# -# This software is governed by the CeCILL license under French law -# and abiding by the rules of distribution of free software. You can use, -# modify and/ or redistribute the software under the terms of the CeCILL -# license as circulated by CEA, CNRS and INRIA at the following URL -# http://www.cecill.info. -# -# As a counterpart to the access to the source code and rights to copy, -# modify and redistribute granted by the license, users are provided only -# with a limited warranty and the software's author, the holder of the -# economic rights, and the successive licensors have only limited -# liability. -# -# The fact that you are presently reading this means that you have had -# knowledge of the CeCILL license and that you accept its terms. - -import iotlabcli.parser.experiment -iotlabcli.parser.experiment.main() diff --git a/iotlab-node b/iotlab-node deleted file mode 100755 index eb14c4b..0000000 --- a/iotlab-node +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -# This file is a part of IoT-LAB cli-tools -# Copyright (C) 2015 INRIA (Contact: admin@iot-lab.info) -# Contributor(s) : see AUTHORS file -# -# This software is governed by the CeCILL license under French law -# and abiding by the rules of distribution of free software. You can use, -# modify and/ or redistribute the software under the terms of the CeCILL -# license as circulated by CEA, CNRS and INRIA at the following URL -# http://www.cecill.info. -# -# As a counterpart to the access to the source code and rights to copy, -# modify and redistribute granted by the license, users are provided only -# with a limited warranty and the software's author, the holder of the -# economic rights, and the successive licensors have only limited -# liability. -# -# The fact that you are presently reading this means that you have had -# knowledge of the CeCILL license and that you accept its terms. - -import iotlabcli.parser.node -iotlabcli.parser.node.main() diff --git a/iotlab-profile b/iotlab-profile deleted file mode 100755 index cad7dc8..0000000 --- a/iotlab-profile +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -# This file is a part of IoT-LAB cli-tools -# Copyright (C) 2015 INRIA (Contact: admin@iot-lab.info) -# Contributor(s) : see AUTHORS file -# -# This software is governed by the CeCILL license under French law -# and abiding by the rules of distribution of free software. You can use, -# modify and/ or redistribute the software under the terms of the CeCILL -# license as circulated by CEA, CNRS and INRIA at the following URL -# http://www.cecill.info. -# -# As a counterpart to the access to the source code and rights to copy, -# modify and redistribute granted by the license, users are provided only -# with a limited warranty and the software's author, the holder of the -# economic rights, and the successive licensors have only limited -# liability. -# -# The fact that you are presently reading this means that you have had -# knowledge of the CeCILL license and that you accept its terms. - -import iotlabcli.parser.profile -iotlabcli.parser.profile.main() diff --git a/iotlab-robot b/iotlab-robot deleted file mode 100755 index e24fa54..0000000 --- a/iotlab-robot +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -# This file is a part of IoT-LAB cli-tools -# Copyright (C) 2015 INRIA (Contact: admin@iot-lab.info) -# Contributor(s) : see AUTHORS file -# -# This software is governed by the CeCILL license under French law -# and abiding by the rules of distribution of free software. You can use, -# modify and/ or redistribute the software under the terms of the CeCILL -# license as circulated by CEA, CNRS and INRIA at the following URL -# http://www.cecill.info. -# -# As a counterpart to the access to the source code and rights to copy, -# modify and redistribute granted by the license, users are provided only -# with a limited warranty and the software's author, the holder of the -# economic rights, and the successive licensors have only limited -# liability. -# -# The fact that you are presently reading this means that you have had -# knowledge of the CeCILL license and that you accept its terms. - -import iotlabcli.parser.robot -iotlabcli.parser.robot.main() diff --git a/iotlab-status b/iotlab-status deleted file mode 100755 index cb97e66..0000000 --- a/iotlab-status +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -# This file is a part of IoT-LAB cli-tools -# Copyright (C) 2015 INRIA (Contact: admin@iot-lab.info) -# Contributor(s) : see AUTHORS file -# -# This software is governed by the CeCILL license under French law -# and abiding by the rules of distribution of free software. You can use, -# modify and/ or redistribute the software under the terms of the CeCILL -# license as circulated by CEA, CNRS and INRIA at the following URL -# http://www.cecill.info. -# -# As a counterpart to the access to the source code and rights to copy, -# modify and redistribute granted by the license, users are provided only -# with a limited warranty and the software's author, the holder of the -# economic rights, and the successive licensors have only limited -# liability. -# -# The fact that you are presently reading this means that you have had -# knowledge of the CeCILL license and that you accept its terms. - -import iotlabcli.parser.status -iotlabcli.parser.status.main() diff --git a/iotlabcli/__init__.py b/iotlabcli/__init__.py index 2a790c0..642a847 100644 --- a/iotlabcli/__init__.py +++ b/iotlabcli/__init__.py @@ -1,4 +1,4 @@ -""" iotlabcli package implementing a cli for iotlab REST API """ +"""iotlabcli package implementing a cli for iotlab REST API""" # This file is a part of IoT-LAB cli-tools # Copyright (C) 2015 INRIA (Contact: admin@iot-lab.info) @@ -22,8 +22,7 @@ # flake8: noqa # simpler access for external usage from iotlabcli.auth import get_user_credentials -from iotlabcli.rest import Api from iotlabcli.helpers import get_current_experiment +from iotlabcli.rest import Api - -__version__ = '3.3.0-dev0' +__version__ = "3.3.0-dev0" diff --git a/iotlabcli/associations.py b/iotlabcli/associations.py index 246ab0c..49c426d 100644 --- a/iotlabcli/associations.py +++ b/iotlabcli/associations.py @@ -54,14 +54,17 @@ def setattrdefault(obj, attribute, default=None): class _Association( - # pylint: disable=too-many-ancestors - collections.abc.MutableMapping, dict): + # pylint: disable=too-many-ancestors + collections.abc.MutableMapping, + dict, +): """_Association class key->value. Inherit from dict to be dumped as a dict by json. """ + __metaclass__ = abc.ABCMeta - KEYFMT = '{}name' + KEYFMT = "{}name" KEY = None VALUE = None VALUE_SORT_KEY = None @@ -109,6 +112,10 @@ def _value(self): def _sort(self): """Sort values with key.""" self._value().sort(key=self.VALUE_SORT_KEY) + # Sync C-level dict storage so CPython's json C encoder (which uses + # PyDict_GET_SIZE bypassing __len__) sees the correct data (Python>=3.12) + dict.clear(self) + dict.update(self, self.__dict__) # Get actual attributes name @@ -138,13 +145,15 @@ def staticclassattribute(function): @classmethod def for_key_value(cls, key, value, sortkey=None): """Create association class for assoctype.""" - name = f'{key.title()}{value.title()}Association' + name = f"{key.title()}{value.title()}Association" class KeyValuesAssociation(cls): # pylint:disable=too-many-ancestors """KeyValuesAssociation class->Nodes.""" + KEY = key VALUE = value VALUE_SORT_KEY = cls.staticclassattribute(sortkey) + KeyValuesAssociation.__name__ = name return KeyValuesAssociation @@ -152,8 +161,7 @@ class KeyValuesAssociation(cls): # pylint:disable=too-many-ancestors def __repr__(self): """Representation. Ignore 'sortkey'.""" return ( - f"{self.__class__.__name__}({self.key!r}, " - f"{getattr(self, 'value', None)!r})" + f"{self.__class__.__name__}({self.key!r}, {getattr(self, 'value', None)!r})" ) @classmethod @@ -161,7 +169,8 @@ def _concrete_class(cls): """Verify that class is concrete and not abstract.""" if cls.KEY is None or cls.VALUE is None: raise NotImplementedError( - "AbstractClass, create real class with 'for_key_value'") + "AbstractClass, create real class with 'for_key_value'" + ) def dict(self): """Dump as a dict.""" @@ -203,9 +212,11 @@ def __setitem__(self, *_): # pragma: no cover class AssociationsMap( - # pylint:disable=too-many-public-methods - # pylint: disable=too-many-ancestors - collections.abc.MutableMapping, list): + # pylint:disable=too-many-public-methods + # pylint: disable=too-many-ancestors + collections.abc.MutableMapping, + list, +): """Sorted Map of Associations objects. Inherit list to be json serialized to a list. @@ -215,8 +226,7 @@ def __init__(self, assoctype, resource, sortkey=None): # pylint:disable=super-init-not-called list.__init__(self) - self.assoc_class = _Association.for_key_value(assoctype, resource, - sortkey) + self.assoc_class = _Association.for_key_value(assoctype, resource, sortkey) self._map = {} def __getitem__(self, key): @@ -302,6 +312,7 @@ def associationsmapdict_from_dict(assocsdict, resource, sortkey=None): return None assocs = {} for assoctype, assoclist in assocsdict.items(): - assocs[assoctype] = AssociationsMap.from_list(assoclist, assoctype, - resource, sortkey) + assocs[assoctype] = AssociationsMap.from_list( + assoclist, assoctype, resource, sortkey + ) return assocs diff --git a/iotlabcli/auth.py b/iotlabcli/auth.py index cd94042..fa6cb08 100644 --- a/iotlabcli/auth.py +++ b/iotlabcli/auth.py @@ -19,22 +19,22 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Authentication file management """ +"""Authentication file management""" +import getpass import os import os.path -import getpass -from base64 import b64encode, b64decode +from base64 import b64decode, b64encode + from iotlabcli.rest import Api -RC_FILE = (os.getenv('IOTLAB_PASSWORD_FILE') or - os.path.expanduser('~/.iotlabrc')) +RC_FILE = os.getenv("IOTLAB_PASSWORD_FILE") or os.path.expanduser("~/.iotlabrc") def get_user_credentials(username=None, password=None): - """ Return user credentials. + """Return user credentials. If provided in arguments return them, if password missing, ask on console, - or try to read them from password file """ + or try to read them from password file""" if (password is not None) and (username is not None): pass @@ -46,13 +46,13 @@ def get_user_credentials(username=None, password=None): def check_user_credentials(username, password): - """ Check that the given credentials are valid """ + """Check that the given credentials are valid""" api = Api(username, password) return api.check_credential() def write_password_file(username, password): - """ Create a password file for basic authentication http when + """Create a password file for basic authentication http when command-line option username and password are used We write .iotlabrc file in user home directory with format username:base64(password) @@ -62,14 +62,14 @@ def write_password_file(username, password): :type password: string """ assert (username is not None) and (password is not None) - with open(RC_FILE, 'w') as pass_file: + with open(RC_FILE, "w") as pass_file: # encode/decode for python3 - enc_password = b64encode(password.encode('utf-8')).decode('utf-8') - pass_file.write(f'{username}:{enc_password}') + enc_password = b64encode(password.encode("utf-8")).decode("utf-8") + pass_file.write(f"{username}:{enc_password}") def _read_password_file(): - """ Try to read password file (.iotlabrc) in user home directory when + """Try to read password file (.iotlabrc) in user home directory when command-line option username and password are not used. If password file exist whe return username and password for basic auth http authentication @@ -77,16 +77,16 @@ def _read_password_file(): if not os.path.exists(RC_FILE): return None, None try: - with open(RC_FILE, 'r') as password_file: - username, enc_password = password_file.readline().split(':') + with open(RC_FILE, "r") as password_file: + username, enc_password = password_file.readline().split(":") # encode/decode for python3 - password = b64decode(enc_password.encode('utf-8')).decode('utf-8') + password = b64decode(enc_password.encode("utf-8")).decode("utf-8") return username, password except ValueError: - raise ValueError(f'Bad password file format: {RC_FILE!r}') + raise ValueError(f"Bad password file format: {RC_FILE!r}") -IDENTITY_FILE = os.path.expanduser('~/.ssh/id_rsa') +IDENTITY_FILE = os.path.expanduser("~/.ssh/id_rsa") def add_ssh_key(identity_file=None): @@ -97,11 +97,11 @@ def add_ssh_key(identity_file=None): api = Api(*get_user_credentials()) keys_json = api.get_ssh_keys() - pub_key = identity_file + '.pub' + pub_key = identity_file + ".pub" with open(pub_key) as key_fh: key = key_fh.read().strip() - keys = keys_json['sshkeys'] + keys = keys_json["sshkeys"] if key in keys: msg = f'Key is already configured:\n"{key}"' raise ValueError(msg) @@ -118,5 +118,5 @@ def ssh_keys(): keys_json = api.get_ssh_keys() print("SSH keys:") - for key in keys_json['sshkeys']: + for key in keys_json["sshkeys"]: print(key) diff --git a/iotlabcli/experiment.py b/iotlabcli/experiment.py index 3fb5ae3..fce22b7 100644 --- a/iotlabcli/experiment.py +++ b/iotlabcli/experiment.py @@ -19,11 +19,12 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Implement the 'experiment' requests """ +"""Implement the 'experiment' requests""" -from os.path import basename import json import time +from os.path import basename + try: # pylint: disable=import-error,no-name-in-module import backport_collections as collections @@ -32,24 +33,29 @@ import collections from iotlabcli import helpers -from iotlabcli.associations import AssociationsMap -from iotlabcli.associations import associationsmapdict_from_dict +from iotlabcli.associations import AssociationsMap, associationsmapdict_from_dict # static name for experiment file : rename by server-rest -EXP_FILENAME = 'new_exp.json' -RUN_FILENAME = 'script.json' +EXP_FILENAME = "new_exp.json" +RUN_FILENAME = "script.json" -NODES_ASSOCIATIONS_FILE_ASSOCS = ('firmware',) -SITE_ASSOCIATIONS_FILE_ASSOCS = ('script', 'scriptconfig') +NODES_ASSOCIATIONS_FILE_ASSOCS = ("firmware",) +SITE_ASSOCIATIONS_FILE_ASSOCS = ("script", "scriptconfig") # Default wait timeout when waiting for an experiment to be in Running state -WAIT_TIMEOUT_DEFAULT = float('+inf') - - -def submit_experiment(api, name, duration, # pylint:disable=too-many-arguments - resources, start_time=None, print_json=False, - sites_assocs=None): - """ Submit user experiment with JSON Encoder serialization object +WAIT_TIMEOUT_DEFAULT = float("+inf") + + +def submit_experiment( # pylint:disable=too-many-arguments,too-many-positional-arguments + api, + name, + duration, + resources, + start_time=None, + print_json=False, + sites_assocs=None, +): + """Submit user experiment with JSON Encoder serialization object Experiment and firmware(s). If submission is accepted by scheduler OAR we print JSONObject response with id submission. @@ -62,13 +68,14 @@ def submit_experiment(api, name, duration, # pylint:disable=too-many-arguments :param sites_assocs: list of 'site_association' """ - assert resources, f'Empty resources: {resources!r}' + assert resources, f"Empty resources: {resources!r}" experiment = _Experiment(name, duration, start_time) exp_files = helpers.FilesDict() for res_dict in resources: inserted_resources = exp_files.add_files_from_dict( - NODES_ASSOCIATIONS_FILE_ASSOCS, res_dict) + NODES_ASSOCIATIONS_FILE_ASSOCS, res_dict + ) res_dict.update(inserted_resources) experiment.add_exp_resources(res_dict) @@ -76,7 +83,8 @@ def submit_experiment(api, name, duration, # pylint:disable=too-many-arguments for site_assoc in sites_assocs: assocs = site_assoc.associations inserted_assocs = exp_files.add_files_from_dict( - SITE_ASSOCIATIONS_FILE_ASSOCS, assocs) + SITE_ASSOCIATIONS_FILE_ASSOCS, assocs + ) assocs.update(inserted_assocs) experiment.add_site_association(site_assoc) @@ -90,7 +98,7 @@ def submit_experiment(api, name, duration, # pylint:disable=too-many-arguments def stop_experiment(api, exp_id): - """ Stop user experiment submission. + """Stop user experiment submission. :param api: API Rest api object :param exp_id: scheduler OAR id submission @@ -99,7 +107,7 @@ def stop_experiment(api, exp_id): def get_experiments_list(api, state, limit, offset): - """ Get the experiment list with the specific restriction: + """Get the experiment list with the specific restriction: :param state: State of the experiment :param limit: maximum number of outputs :param offset: offset of experiments to start at @@ -108,8 +116,8 @@ def get_experiments_list(api, state, limit, offset): return api.get_experiments(state, limit, offset) -def get_experiment(api, exp_id, option=''): - """ Get user experiment's description : +def get_experiment(api, exp_id, option=""): + """Get user experiment's description : :param api: API Rest api object :param exp_id: experiment id @@ -123,9 +131,9 @@ def get_experiment(api, exp_id, option=''): * 'deployment': deployment info """ result = api.get_experiment_info(exp_id, option) - if option == 'data': + if option == "data": _write_experiment_archive(exp_id, result) - result = 'Written' + result = "Written" return result @@ -137,13 +145,13 @@ def get_active_experiments(api, running_only=True): :param running_only: if False search for a waiting/starting experiment :returns: {'Running': [EXP_ID], 'Waiting': [EXP_ID, EXP_ID]} """ - states = ['Running'] if running_only else helpers.ACTIVE_STATES + states = ["Running"] if running_only else helpers.ACTIVE_STATES exp_by_states = helpers.exps_by_states_dict(api, states) return exp_by_states def load_experiment(api, exp_desc_path, files_list=()): - """ Load and submit user experiment description with firmware(s) + """Load and submit user experiment description with firmware(s) Firmwares and scripts required for experiment will be loaded from current directory, except if their path is given in files_list @@ -194,8 +202,8 @@ def _files_with_filespath(files, filespath): # Error if there are remaining files in filespath if filespathdict: raise ValueError( - f'Filespath {list(filespathdict.values())} not in ' - f'files list {sorted(set(files))}' + f"Filespath {list(filespathdict.values())} not in " + f"files list {sorted(set(files))}" ) return sorted(updatedfiles) @@ -214,15 +222,15 @@ def reload_experiment(api, exp_id, duration=None, start_time=None): # API needs strings and values shoud be absent if None if duration is not None: - exp_json['duration'] = str(duration) + exp_json["duration"] = str(duration) if start_time is not None: - exp_json['reservation'] = str(start_time) + exp_json["reservation"] = str(start_time) return api.reload_experiment(exp_id, exp_json) def info_experiment(api, list_id=False, site=None, **selections): - """ Print testbed information for user experiment submission: + """Print testbed information for user experiment submission: * nodes description * nodes description in short mode @@ -244,16 +252,16 @@ def script_experiment(api, exp_id, command, *options): list of sites for 'kill', 'status' may be None """ res = None - if command == 'run': + if command == "run": files_dict = _script_run_files_dict(*options) res = api.script_command(exp_id, command, files=files_dict) - elif command in ('kill', 'status'): + elif command in ("kill", "status"): sites_list = sorted(options) res = api.script_command(exp_id, command, json=sites_list) if res is None: - raise ValueError(f'Unknown script command {command!r}') + raise ValueError(f"Unknown script command {command!r}") return res @@ -269,7 +277,7 @@ def _script_run_files_dict(*site_associations): """ if not site_associations: - raise ValueError(f'Got empty site_associations: {site_associations}') + raise ValueError(f"Got empty site_associations: {site_associations}") _check_sites_uniq(*site_associations) @@ -281,7 +289,8 @@ def _script_run_files_dict(*site_associations): for assoctype, assocname in assocs.items(): _add_siteassoc_to_dict(associations, sites, assoctype, assocname) inserted_assocs = files_dict.add_files_from_dict( - SITE_ASSOCIATIONS_FILE_ASSOCS, assocs) + SITE_ASSOCIATIONS_FILE_ASSOCS, assocs + ) assocs.update(inserted_assocs) # Add scrit sites association to files_dict @@ -292,7 +301,7 @@ def _script_run_files_dict(*site_associations): def _add_siteassoc_to_dict(assocs, sites, assoctype, assocname): """Add given site association to 'assocs' dict.""" name = site_association_name(assoctype, assocname) - assoc = assocs.setdefault(assoctype, AssociationsMap(assoctype, 'sites')) + assoc = assocs.setdefault(assoctype, AssociationsMap(assoctype, "sites")) assoc.extendvalues(name, sites) @@ -312,13 +321,17 @@ def _check_sites_uniq(*site_associations): duplicates = [s for s, c in collections.Counter(sites).items() if c > 1] if duplicates: - raise ValueError(f'Sites may only be given once: {duplicates}') + raise ValueError(f"Sites may only be given once: {duplicates}") -def wait_experiment(api, exp_id, states='Running', - step=5, timeout=WAIT_TIMEOUT_DEFAULT, - cancel_on_timeout=False): - # pylint: disable=too-many-arguments +def wait_experiment( # pylint: disable=too-many-arguments,too-many-positional-arguments + api, + exp_id, + states="Running", + step=5, + timeout=WAIT_TIMEOUT_DEFAULT, + cancel_on_timeout=False, +): """Wait for the experiment to be in `states`. Also returns if Terminated or Error @@ -330,17 +343,26 @@ def wait_experiment(api, exp_id, states='Running', :param timeout: timeout if wait takes too long :param cancel_on_timeout: cancel the experiment if the timeout is reached """ + def _state_function(): """Get current user experiment state.""" - return get_experiment(api, exp_id, '')['state'] + return get_experiment(api, exp_id, "")["state"] def _stop_function(): """Cancel submitted user experiment.""" stop_experiment(api, exp_id) - exp_str = f'{exp_id}' - return wait_state(_state_function, _stop_function, - exp_str, states, step, timeout, cancel_on_timeout) + exp_str = f"{exp_id}" + + return wait_state( + _state_function, + _stop_function, + exp_str, + states, + step, + timeout, + cancel_on_timeout, + ) def _states_from_str(states_str): @@ -348,24 +370,30 @@ def _states_from_str(states_str): Also verify given states are valid. """ - return helpers.check_experiment_state(states_str).split(',') + return helpers.check_experiment_state(states_str).split(",") -STOPPED_STATES = set(_states_from_str('Terminated,Error')) +STOPPED_STATES = set(_states_from_str("Terminated,Error")) def _raise_timeout_msg(exp_str, stop_fct, cancel_on_timeout): - msg = 'Timeout reached' + msg = "Timeout reached" if cancel_on_timeout: - msg += f', cancelling experiment {exp_str}' + msg += f", cancelling experiment {exp_str}" stop_fct() raise RuntimeError(msg) -def wait_state(state_fct, stop_fct, exp_str, states='Running', - step=5, timeout=WAIT_TIMEOUT_DEFAULT, cancel_on_timeout=False): - # pylint: disable=too-many-arguments +def wait_state( # pylint: disable=too-many-arguments,too-many-positional-arguments + state_fct, + stop_fct, + exp_str, + states="Running", + step=5, + timeout=WAIT_TIMEOUT_DEFAULT, + cancel_on_timeout=False, +): """Wait until `state_fct` returns a state in `states` and also Terminated or Error @@ -392,6 +420,7 @@ def wait_state(state_fct, stop_fct, exp_str, states='Running', time.sleep(step) _raise_timeout_msg(exp_str, stop_fct, cancel_on_timeout) + return None # pragma: no cover def _timeout(start_time, timeout): @@ -404,8 +433,7 @@ def _timeout(start_time, timeout): return time.time() > start_time + timeout -def exp_resources(nodes, firmware_path=None, profile_name=None, - **associations): +def exp_resources(nodes, firmware_path=None, profile_name=None, **associations): """Create an experiment resources dict. :param nodes: a list of nodes url or a AliasNodes object @@ -417,41 +445,42 @@ def exp_resources(nodes, firmware_path=None, profile_name=None, """ if isinstance(nodes, AliasNodes): - exp_type = 'alias' + exp_type = "alias" else: - exp_type = 'physical' + exp_type = "physical" resources = { - 'type': exp_type, - 'nodes': nodes, - 'firmware': firmware_path, - 'profile': profile_name, - 'associations': associations, + "type": exp_type, + "nodes": nodes, + "firmware": firmware_path, + "profile": profile_name, + "associations": associations, } return resources SiteAssociationTuple = collections.namedtuple( - 'SiteAssociationTuple', ['sites', 'associations']) + "SiteAssociationTuple", ["sites", "associations"] +) def site_association(*sites, **kwassociations): """Return a site_association tuple.""" if not sites: - raise ValueError('No sites given') + raise ValueError("No sites given") if len(sites) != len(set(sites)): - raise ValueError(f'Sites are not uniq {sites}') + raise ValueError(f"Sites are not uniq {sites}") # Associations are mandatory if not kwassociations: - raise ValueError('No association given') + raise ValueError("No association given") return SiteAssociationTuple(sites, kwassociations) -class AliasNodes(): # pylint: disable=too-few-public-methods +class AliasNodes: # pylint: disable=too-few-public-methods """An AliasNodes class >>> AliasNodes(5, 'grenoble', 'm3:at86rf231', False) @@ -464,6 +493,7 @@ class AliasNodes(): # pylint: disable=too-few-public-methods True """ + _alias = 0 # static count of current alias number def __init__(self, nbnodes, site, archi, mobile=False, _alias=None): @@ -513,13 +543,13 @@ def __eq__(self, other): # pragma: no cover # # # # # # # # # # # Kwargs to initialize 'AssociationsMap' for nodes sorted. -_NODESMAPKWARGS = dict(resource='nodes', sortkey=helpers.node_url_sort_key) +_NODESMAPKWARGS = {"resource": "nodes", "sortkey": helpers.node_url_sort_key} -class _Experiment(): # pylint:disable=too-many-instance-attributes - """ Class describing an experiment """ +class _Experiment: # pylint:disable=too-many-instance-attributes + """Class describing an experiment""" - ASSOCATTR_FMT = '{}associations' + ASSOCATTR_FMT = "{}associations" def __init__(self, name, duration, start_time=None): self.duration = duration @@ -537,90 +567,103 @@ def __init__(self, name, duration, start_time=None): def _firmwareassociations(self): """Init and return firmwareassociations.""" - return setattr_if_none(self, 'firmwareassociations', - AssociationsMap('firmware', **_NODESMAPKWARGS)) + return setattr_if_none( + self, "firmwareassociations", AssociationsMap("firmware", **_NODESMAPKWARGS) + ) def _profileassociations(self): """Init and return profileassociations.""" - return setattr_if_none(self, 'profileassociations', - AssociationsMap('profile', **_NODESMAPKWARGS)) + return setattr_if_none( + self, "profileassociations", AssociationsMap("profile", **_NODESMAPKWARGS) + ) def _associations(self, assoctype): """Init and return associations[assoctype].""" - assocs = setattr_if_none(self, 'associations', {}) - return assocs.setdefault(assoctype, - AssociationsMap(assoctype, **_NODESMAPKWARGS)) + assocs = setattr_if_none(self, "associations", {}) + return assocs.setdefault( + assoctype, AssociationsMap(assoctype, **_NODESMAPKWARGS) + ) def _siteassociations(self, assoctype): """Init and return associations[assoctype].""" - assocs = setattr_if_none(self, 'siteassociations', {}) - return assocs.setdefault(assoctype, - AssociationsMap(assoctype, 'sites')) + assocs = setattr_if_none(self, "siteassociations", {}) + return assocs.setdefault(assoctype, AssociationsMap(assoctype, "sites")) @classmethod def from_dict(cls, exp_dict): """Create an _Experiment object from given `exp_dict`.""" - experiment = cls(exp_dict.pop('name', None), exp_dict.pop('duration'), - exp_dict.pop('reservation', None)) + experiment = cls( + exp_dict.pop("name", None), + exp_dict.pop("duration"), + exp_dict.pop("reservation", None), + ) - experiment.type = exp_dict.pop('type') - experiment.nodes = exp_dict.pop('nodes') + experiment.type = exp_dict.pop("type") + experiment.nodes = exp_dict.pop("nodes") - if 'profiles' in exp_dict.keys(): - experiment.profiles = exp_dict.pop('profiles') + if "profiles" in exp_dict.keys(): + experiment.profiles = exp_dict.pop("profiles") - if 'mobilities' in exp_dict.keys(): - experiment.mobilities = exp_dict.pop('mobilities') + if "mobilities" in exp_dict.keys(): + experiment.mobilities = exp_dict.pop("mobilities") experiment._load_assocs(**exp_dict) # pylint:disable=protected-access # No checking return experiment - def _load_assocs(self, firmwareassociations=None, profileassociations=None, - associations=None, siteassociations=None): + def _load_assocs( + self, + firmwareassociations=None, + profileassociations=None, + associations=None, + siteassociations=None, + ): """Load associations to AssociationsMap and set attributes.""" self.firmwareassociations = AssociationsMap.from_list( - firmwareassociations, 'firmware', **_NODESMAPKWARGS) + firmwareassociations, "firmware", **_NODESMAPKWARGS + ) self.profileassociations = AssociationsMap.from_list( - profileassociations, 'profile', **_NODESMAPKWARGS) - self.associations = associationsmapdict_from_dict(associations, - **_NODESMAPKWARGS) - self.siteassociations = associationsmapdict_from_dict(siteassociations, - 'sites') + profileassociations, "profile", **_NODESMAPKWARGS + ) + self.associations = associationsmapdict_from_dict( + associations, **_NODESMAPKWARGS + ) + self.siteassociations = associationsmapdict_from_dict(siteassociations, "sites") def _set_type(self, exp_type): - """ Set current experiment type. + """Set current experiment type. If type was already set and is different ValueError is raised """ if self.type is not None and self.type != exp_type: raise ValueError( - "Invalid experiment, should be only physical or only alias") + "Invalid experiment, should be only physical or only alias" + ) self.type = exp_type def add_exp_resources(self, resources): - """ Add 'exp_resources' to current experiment + """Add 'exp_resources' to current experiment It will update node type, nodes, firmware and profile associations """ # Alias/Physical - self._set_type(resources['type']) + self._set_type(resources["type"]) # register nodes in experiment - nodes = resources['nodes'] + nodes = resources["nodes"] self._register_nodes(nodes) # pylint:disable=not-callable nodes = self._nodes_to_assoc(nodes) # register firmware - if resources['firmware'] is not None: - name = nodes_association_name('firmware', resources['firmware']) + if resources["firmware"] is not None: + name = nodes_association_name("firmware", resources["firmware"]) self._firmwareassociations().extendvalues(name, nodes) # register profile, may be None - if resources['profile'] is not None: - name = nodes_association_name('profile', resources['profile']) + if resources["profile"] is not None: + name = nodes_association_name("profile", resources["profile"]) self._profileassociations().extendvalues(name, nodes) # Add other associations - associations = resources.get('associations', {}) + associations = resources.get("associations", {}) for assoctype, assocname in associations.items(): self._add_nodes_association(nodes, assoctype, assocname) @@ -631,7 +674,7 @@ def _add_nodes_association(self, nodes, assoctype, assocname): def _nodes_to_assoc(self, nodes): """Returns nodes to use in association.""" - return [nodes.alias] if self.type == 'alias' else nodes + return [nodes.alias] if self.type == "alias" else nodes def add_site_association(self, assoc): """Add a site_association.""" @@ -644,8 +687,8 @@ def _add_site_association(self, sites, assoctype, assocname): self._siteassociations(assoctype).extendvalues(name, sites) def set_physical_nodes(self, nodes_list): - """Set physical nodes list """ - self._set_type('physical') + """Set physical nodes list""" + self._set_type("physical") # Check that nodes are not already present _intersect = list(set(self.nodes) & set(nodes_list)) @@ -654,27 +697,26 @@ def set_physical_nodes(self, nodes_list): self.nodes.extend(nodes_list) # Keep unique values and sorted - self.nodes = sorted(list(set(self.nodes)), - key=helpers.node_url_sort_key) + self.nodes = sorted(list(set(self.nodes)), key=helpers.node_url_sort_key) def set_alias_nodes(self, alias_nodes): - """Set alias nodes list """ - self._set_type('alias') + """Set alias nodes list""" + self._set_type("alias") self.nodes.append(alias_nodes) @property def _register_nodes(self): """Register nodes with the correct method according to exp `type`.""" _register_fct_dict = { - 'physical': self.set_physical_nodes, - 'alias': self.set_alias_nodes, + "physical": self.set_physical_nodes, + "alias": self.set_alias_nodes, } return _register_fct_dict[self.type] def filenames(self): """Extract list of filenames required.""" # No need to check nodes associations if there is only 'firmware' - assert NODES_ASSOCIATIONS_FILE_ASSOCS == ('firmware',) + assert NODES_ASSOCIATIONS_FILE_ASSOCS == ("firmware",) files = [] # Handle None attributes @@ -698,8 +740,8 @@ def setattr_if_none(obj, attr, default): def _write_experiment_archive(exp_id, data): - """ Write experiment archive contained in 'data' to 'exp_id.tar.gz' """ - with open(f'{exp_id}.tar.gz', 'wb') as archive: + """Write experiment archive contained in 'data' to 'exp_id.tar.gz'""" + with open(f"{exp_id}.tar.gz", "wb") as archive: archive.write(data) @@ -708,8 +750,7 @@ def nodes_association_name(assoctype, assocname): Return basename(assocname) if assoctype is a file-association. """ - return _basename_if_in(assocname, assoctype, - NODES_ASSOCIATIONS_FILE_ASSOCS) + return _basename_if_in(assocname, assoctype, NODES_ASSOCIATIONS_FILE_ASSOCS) def site_association_name(assoctype, assocname): diff --git a/iotlabcli/helpers.py b/iotlabcli/helpers.py index 98e0e07..c15c3cb 100644 --- a/iotlabcli/helpers.py +++ b/iotlabcli/helpers.py @@ -20,34 +20,43 @@ # knowledge of the CeCILL license and that you accept its terms. """Helpers methods""" + import hashlib -import sys -import os -import json import itertools +import json +import os +import sys import warnings -OAR_STATES = ["Waiting", "toLaunch", "Launching", - "Running", - "Finishing", - "Terminated", "Stopped", "Error"] -ACTIVE_STATES = OAR_STATES[OAR_STATES.index('Running')::-1] - -DEPRECATION_MESSAGE = ("{old_cmd} command is deprecated and will be removed " - "in next release. Please \033[1muse {new_cmd} " - "instead\033[0m.\n\n") +OAR_STATES = [ + "Waiting", + "toLaunch", + "Launching", + "Running", + "Finishing", + "Terminated", + "Stopped", + "Error", +] +ACTIVE_STATES = OAR_STATES[OAR_STATES.index("Running") :: -1] + +DEPRECATION_MESSAGE = ( + "{old_cmd} command is deprecated and will be removed " + "in next release. Please \033[1muse {new_cmd} " + "instead\033[0m.\n\n" +) def get_current_experiment(api, experiment_id=None, running_only=True): - """ Return the given experiment or get the currently running one. + """Return the given experiment or get the currently running one. If running_only is false, try to return the experiment the most advanced - Waiting < toLaunch < Launching < Running """ + Waiting < toLaunch < Launching < Running""" if experiment_id is not None: return experiment_id if running_only: # no experiment given, try to find the currently running one - states = ['Running'] + states = ["Running"] else: # or experiment that are starting (from waiting to Running') states = ACTIVE_STATES @@ -60,22 +69,22 @@ def get_current_experiment(api, experiment_id=None, running_only=True): def exps_by_states_dict(api, states): - """ Return current experiment in `states` as a per state dict """ + """Return current experiment in `states` as a per state dict""" # exps == [{'state': 'Waiting', 'id': 10134, ...}, # {'state': 'Waiting', 'id': 10135, ...}, # {'state': 'Running', 'id': 10130, ...}] - exps = api.get_experiments(state=','.join(states))['items'] + exps = api.get_experiments(state=",".join(states))["items"] exp_states_d = {} for exp in exps: - exp_states_d.setdefault(str(exp['state']), []).append(exp['id']) + exp_states_d.setdefault(str(exp["state"]), []).append(exp["id"]) return exp_states_d # {'Waiting': [10134, 10135], 'Running': [10130]} def get_current_exp(exp_by_states, states): # noqa: C901 - """ Current experiment is the first state in `states` where there is only + """Current experiment is the first state in `states` where there is only one experiment in `exp_by_states`. :raises: ValueError if there is no experiment or if there are multiple experiments of the same state @@ -106,7 +115,7 @@ def get_current_exp(exp_by_states, states): # noqa: C901 ValueError: You have no 'Running, ..., Waiting' experiment """ - states_str = ', '.join(states) + states_str = ", ".join(states) res = None for state in states: # keep order of states @@ -148,10 +157,10 @@ def node_url_sort_key(node_url): """ if node_url.isdigit(): return int(node_url) - _node, _, domain = node_url.partition('.') - site = domain.split('.')[0] + _node, _, domain = node_url.partition(".") + site = domain.split(".")[0] - node_type, num_str = _node.rsplit('-', 1) + node_type, num_str = _node.rsplit("-", 1) return site, node_type, int(num_str) @@ -163,19 +172,20 @@ def md5(data): class FilesDict(dict): - """ Dictionary to store experiment files. + """Dictionary to store experiment files. We don't want adding two different values for the same key, so __setitem__ is overriden to check that """ + def __init__(self): dict.__init__(self) def __setitem__(self, key, val): - """ Prevent adding a new different value to an existing key """ + """Prevent adding a new different value to an existing key""" if key not in self: dict.__setitem__(self, key, val) elif self[key] != val: - raise ValueError(f'Has different values for same key {key!r}') + raise ValueError(f"Has different values for same key {key!r}") def add_file(self, file_path): """Add a file to the dictionary. @@ -188,13 +198,13 @@ def add_file(self, file_path): if file_path is None: return None key = os.path.basename(file_path) - value = read_file(file_path, 'b') + value = read_file(file_path, "b") try: self[key] = value except ValueError: # use md5 hash as prefix to handle duplicated basenames # with different contents - key = f'{md5(value)}_{key}' + key = f"{md5(value)}_{key}" self[key] = value return key @@ -216,30 +226,30 @@ def add_files_from_dict(self, keys, files_dict): def read_custom_api_url(): - """ Return the customized api url from: - * config file in /.iotlab.api-url - * or environment variable IOTLAB_API_URL + """Return the customized api url from: + * config file in /.iotlab.api-url + * or environment variable IOTLAB_API_URL """ try: # try getting url from config file - api_url = read_file('~/.iotlab.api-url').strip() + api_url = read_file("~/.iotlab.api-url").strip() except IOError: # try getting url from environment variable, None if undefined - api_url = os.getenv('IOTLAB_API_URL') + api_url = os.getenv("IOTLAB_API_URL") if api_url: sys.stderr.write(f"Using custom api_url: {api_url}\n") return api_url -def read_file(file_path, opt=''): - """ Open and read a file """ - with open(os.path.expanduser(file_path), 'r' + opt) as _fd: # expand '~' +def read_file(file_path, opt=""): + """Open and read a file""" + with open(os.path.expanduser(file_path), "r" + opt) as _fd: # expand '~' return _fd.read() def check_experiment_state(state_str=None): - """ Check that given states are valid if None given, return all states + """Check that given states are valid if None given, return all states >>> check_experiment_state('Running') 'Running' @@ -252,25 +262,28 @@ def check_experiment_state(state_str=None): Traceback (most recent call last): ValueError: Invalid experiment states: ['invalid'] should be in [...]. """ - state_str = state_str or ','.join(OAR_STATES) # default to all states + state_str = state_str or ",".join(OAR_STATES) # default to all states # Check states are all in OAR_STATES - invalid = set(state_str.split(',')) - set(OAR_STATES) + invalid = set(state_str.split(",")) - set(OAR_STATES) if invalid: raise ValueError( - f'Invalid experiment states: {sorted(list(invalid))} ' - f'should be in {OAR_STATES}.' + f"Invalid experiment states: {sorted(list(invalid))} " + f"should be in {OAR_STATES}." ) return state_str def json_dumps(obj): - """ Dumps data to json """ + """Dumps data to json""" + class _Encoder(json.JSONEncoder): # pylint: disable=too-few-public-methods - """ Encoder for serialization object python to JSON format """ + """Encoder for serialization object python to JSON format""" + def default(self, o): # pylint: disable=method-hidden return o.__dict__ + return json.dumps(obj, cls=_Encoder, sort_keys=True, indent=4) @@ -284,10 +297,13 @@ def flatten_list_list(list_list): def deprecate_warn_cmd(old_cmd, new_cmd, stacklevel): - """ Display a deprecation warning message """ - warnings.simplefilter('always', DeprecationWarning) - warnings.warn(DEPRECATION_MESSAGE.format(old_cmd=old_cmd, new_cmd=new_cmd), - DeprecationWarning, stacklevel) + """Display a deprecation warning message""" + warnings.simplefilter("always", DeprecationWarning) + warnings.warn( + DEPRECATION_MESSAGE.format(old_cmd=old_cmd, new_cmd=new_cmd), + DeprecationWarning, + stacklevel, + ) def deprecate_cmd(cmd_func, old_cmd, new_cmd): diff --git a/iotlabcli/integration/test_integration.py b/iotlabcli/integration/test_integration.py index eeb1a28..2db1ff3 100644 --- a/iotlabcli/integration/test_integration.py +++ b/iotlabcli/integration/test_integration.py @@ -19,7 +19,7 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Integration tests for cli-tools """ +"""Integration tests for cli-tools""" # pylint:disable=I0011,too-many-public-methods # pylint:disable=I0011,attribute-defined-outside-init @@ -32,172 +32,168 @@ # * Validates JSON outputs # * run commands without exp_id in error cases no exp or many exps running - import argparse -import os import json +import logging +import os +import runpy import shlex import time -import runpy import unittest -import logging from tempfile import NamedTemporaryFile from iotlabcli.parser.common import expand_short_nodes_list try: # pylint:disable=I0011,F0401,E0611 - from mock import patch from cStringIO import StringIO + from mock import patch except ImportError: # pragma: no cover - from unittest.mock import patch # pylint:disable=I0011,F0401,E0611 from io import StringIO + from unittest.mock import patch # pylint:disable=I0011,F0401,E0611 LOGGER = logging.getLogger(__file__) LOGGER.setLevel(logging.INFO) -_FMT = logging.Formatter('%(asctime)s :: %(levelname)s :: %(message)s') +_FMT = logging.Formatter("%(asctime)s :: %(levelname)s :: %(message)s") _HANDLER = logging.StreamHandler() _HANDLER.setFormatter(_FMT) LOGGER.addHandler(_HANDLER) SITES = { - 'prod': { - 'm3': 'grenoble', - 'cc1101': 'strasbourg', + "prod": { + "m3": "grenoble", + "cc1101": "strasbourg", + }, + "dev": { + "m3": "devgrenoble", }, - 'dev': { - 'm3': 'devgrenoble', - } } -NODES = SITES['prod'] +NODES = SITES["prod"] CUR_DIR = os.path.dirname(__file__) FIRMWARE = { - 'm3': os.path.join(CUR_DIR, 'firmwares', 'm3_autotest.elf'), - 'cc1101': os.path.join(CUR_DIR, 'firmwares', 'cc1101.hex'), + "m3": os.path.join(CUR_DIR, "firmwares", "m3_autotest.elf"), + "cc1101": os.path.join(CUR_DIR, "firmwares", "cc1101.hex"), } class TestCliToolsExperiments(unittest.TestCase): - """ Test the cli tools experiments """ + """Test the cli tools experiments""" def test_an_experiment_alias_multi_same_node_firmware(self): - """ Exp alias multiple time same reservation firmware """ + """Exp alias multiple time same reservation firmware""" nodes = f"5,site={NODES['m3']}+archi=m3:at86rf231,{FIRMWARE['m3']}" - cmd = ('iotlab-experiment submit -d 5 -n test_cli' - ' -l {0} -l {0} -l {0}'.format(nodes)) + cmd = "iotlab-experiment submit -d 5 -n test_cli -l {0} -l {0} -l {0}".format( + nodes + ) self._start_experiment(cmd) - self.assertEqual('Running', self._wait_state_or_finished('Running')) + self.assertEqual("Running", self._wait_state_or_finished("Running")) time.sleep(1) self._get_exp_info() self._stop_experiment() self._wait_state_or_finished() # Test get start-time - cmd = f'iotlab-experiment get --start-time -i {self.exp_id}' - self.assertNotEqual(0, call_cli(cmd)['start_time']) + cmd = f"iotlab-experiment get --start-time -i {self.exp_id}" + self.assertNotEqual(0, call_cli(cmd)["start_time"]) def test_an_experiment_alias_multi_same_node(self): - """ Run an experiment with alias and multiple time same reservation """ + """Run an experiment with alias and multiple time same reservation""" nodes = f"5,site={NODES['m3']}+archi=m3:at86rf231" - cmd = 'iotlab-experiment submit -d 5 -n test_cli -l {0} -l {0}'.format( - nodes) + cmd = "iotlab-experiment submit -d 5 -n test_cli -l {0} -l {0}".format(nodes) self._start_experiment(cmd) - self.assertEqual('Running', self._wait_state_or_finished('Running')) + self.assertEqual("Running", self._wait_state_or_finished("Running")) time.sleep(1) self._get_exp_info() self._reset_nodes_no_expid() - f_nodes = self.nodes_str_from_desc(archi='m3', n_type='-l') - self._flash_nodes(FIRMWARE['m3'], f_nodes) + f_nodes = self.nodes_str_from_desc(archi="m3", n_type="-l") + self._flash_nodes(FIRMWARE["m3"], f_nodes) self._stop_experiment() self._wait_state_or_finished() def test_an_experiment_alias_multisite(self): - """ Run an experiment with multisite/archi """ + """Run an experiment with multisite/archi""" # ensure profile test_m3 exists - call_cli('iotlab-profile addm3 -n test_m3 -sniffer -channels 11') + call_cli("iotlab-profile addm3 -n test_m3 -sniffer -channels 11") - cmd = 'iotlab-experiment submit -d 5 -n test_cli' - cmd += ( - f" -l 5," - f"site={NODES['m3']}+archi=m3:at86rf231,{FIRMWARE['m3']},test_m3" - ) - cmd += ( - f" -l 1," - f"site={NODES['cc1101']}+archi=wsn430:cc1101,{FIRMWARE['cc1101']}" - ) + cmd = "iotlab-experiment submit -d 5 -n test_cli" + cmd += f" -l 5,site={NODES['m3']}+archi=m3:at86rf231,{FIRMWARE['m3']},test_m3" + cmd += f" -l 1,site={NODES['cc1101']}+archi=wsn430:cc1101,{FIRMWARE['cc1101']}" self._start_experiment(cmd) - self.assertEqual('Running', self._wait_state_or_finished('Running')) + self.assertEqual("Running", self._wait_state_or_finished("Running")) time.sleep(1) self._get_exp_info() self._reset_nodes_no_expid() # flash all but wsn430 nodes - nodes = self.nodes_str_from_desc(archi='wsn430', n_type='-e') - self._flash_nodes(FIRMWARE['m3'], nodes) + nodes = self.nodes_str_from_desc(archi="wsn430", n_type="-e") + self._flash_nodes(FIRMWARE["m3"], nodes) self._stop_experiment() self._wait_state_or_finished() @staticmethod def _find_working_nodes(site, archi, num): - """ Find working nodes, there should be at least num nodes """ - cmd = f'iotlab-experiment info -li --site {site}' - nodes = expand_short_nodes_list(next( - s['ids'] - for n in call_cli(cmd)["items"][0]['archis'] if n['archi'] == archi - for s in n['states'] if s['state'] == 'Alive' - )) - nodes_str = '+'.join(str(n) for n in nodes[0:num]) + """Find working nodes, there should be at least num nodes""" + cmd = f"iotlab-experiment info -li --site {site}" + nodes = expand_short_nodes_list( + next( + s["ids"] + for n in call_cli(cmd)["items"][0]["archis"] + if n["archi"] == archi + for s in n["states"] + if s["state"] == "Alive" + ) + ) + nodes_str = "+".join(str(n) for n in nodes[0:num]) LOGGER.debug("nodes_str: %r", nodes_str) return f"{site},{archi.split(':')[0]},{nodes_str}" def test_an_experiment_physical_one_site(self): - """ Run an experiment on m3 nodes simple""" - site = NODES['m3'] + """Run an experiment on m3 nodes simple""" + site = NODES["m3"] - cmd = f'iotlab-experiment info -li --site {site}' + cmd = f"iotlab-experiment info -li --site {site}" - nodes = self._find_working_nodes(site, 'm3:at86rf231', 3) - cmd = f'iotlab-experiment submit -d 5 -n test_cli -l {nodes} ' + nodes = self._find_working_nodes(site, "m3:at86rf231", 3) + cmd = f"iotlab-experiment submit -d 5 -n test_cli -l {nodes} " self._start_experiment(cmd) - self.assertEqual('Running', self._wait_state_or_finished('Running')) + self.assertEqual("Running", self._wait_state_or_finished("Running")) time.sleep(1) self._get_exp_info() self._reset_nodes_no_expid() - f_nodes = self.nodes_str_from_desc(archi='m3', n_type='-l') - self._flash_nodes(FIRMWARE['m3'], f_nodes) - self._debug('start', f_nodes) - self._debug('stop', f_nodes) + f_nodes = self.nodes_str_from_desc(archi="m3", n_type="-l") + self._flash_nodes(FIRMWARE["m3"], f_nodes) + self._debug("start", f_nodes) + self._debug("stop", f_nodes) self._stop_experiment() self._wait_state_or_finished() # helpers methods @staticmethod def _reset_nodes_no_expid(): - """ Flash all nodes of type archi """ - cmd = 'iotlab-node --reset' + """Flash all nodes of type archi""" + cmd = "iotlab-node --reset" LOGGER.info(cmd) call_cli(cmd) - def nodes_str_from_desc(self, site='', archi='', n_type='-l'): - """ extract nodes that match description """ - _nodes = self.exp_desc["nodes"]['items'] - nodes = [n for n in _nodes - if (archi in n['archi']) and (site in n['site'])] + def nodes_str_from_desc(self, site="", archi="", n_type="-l"): + """extract nodes that match description""" + _nodes = self.exp_desc["nodes"]["items"] + nodes = [n for n in _nodes if (archi in n["archi"]) and (site in n["site"])] nodes_dict = {} for n in nodes: - archi, num = n['network_address'].split('.')[0].split('-')[0:2] - nodes_dict.setdefault((n['site'], archi), []).append(num) + archi, num = n["network_address"].split(".")[0].split("-")[0:2] + nodes_dict.setdefault((n["site"], archi), []).append(num) LOGGER.debug(nodes_dict) @@ -208,67 +204,64 @@ def nodes_str_from_desc(self, site='', archi='', n_type='-l'): nodes_list.append(node_str) # create the joined string - node_str = ''.join(f' {n_type} {n}' for n in nodes_list) + node_str = "".join(f" {n_type} {n}" for n in nodes_list) return node_str - def _flash_nodes(self, firmware, cmd_append=''): - """ Flash all nodes of type archi """ - cmd = f'iotlab-node -i {self.exp_id} -up {firmware} {cmd_append}' + def _flash_nodes(self, firmware, cmd_append=""): + """Flash all nodes of type archi""" + cmd = f"iotlab-node -i {self.exp_id} -up {firmware} {cmd_append}" LOGGER.info(cmd) ret = call_cli(cmd) LOGGER.debug(ret) - def _debug(self, mode, cmd_append=''): - """ Flash all nodes of type archi """ - cmd = f'iotlab-node -i {self.exp_id} --debug-{mode} {cmd_append}' + def _debug(self, mode, cmd_append=""): + """Flash all nodes of type archi""" + cmd = f"iotlab-node -i {self.exp_id} --debug-{mode} {cmd_append}" LOGGER.info(cmd) ret = call_cli(cmd) LOGGER.debug(ret) def _start_experiment(self, cmd, firmwares=()): - """ Start an experiment using 'cmd'. - Add firmwares path to allow checking later """ + """Start an experiment using 'cmd'. + Add firmwares path to allow checking later""" LOGGER.info(cmd) self.firmwares = firmwares - call_cli(cmd + ' --print') + call_cli(cmd + " --print") self.exp_id = call_cli(cmd)["id"] - self.id_str = f' --id {self.exp_id} ' + self.id_str = f" --id {self.exp_id} " LOGGER.info(self.exp_id) def _stop_experiment(self): - """ Stop current experiment """ - cmd = f'iotlab-experiment stop -i {self.exp_id}' + """Stop current experiment""" + cmd = f"iotlab-experiment stop -i {self.exp_id}" ret = call_cli(cmd) - LOGGER.info("%s: %r", cmd, ret['status']) + LOGGER.info("%s: %r", cmd, ret["status"]) def _get_exp_info(self): - """ Get experiment info and check them """ - cmd = 'iotlab-experiment get -d' + self.id_str + """Get experiment info and check them""" + cmd = "iotlab-experiment get -d" + self.id_str self.exp_desc = {} - self.exp_desc['deploymentresults'] = exp_deployment = call_cli(cmd) + self.exp_desc["deploymentresults"] = exp_deployment = call_cli(cmd) try: - self.assertNotEqual([], exp_deployment['0']) + self.assertNotEqual([], exp_deployment["0"]) except KeyError: LOGGER.warning("No 0 Deploymentresults: %r", exp_deployment) - cmd = 'iotlab-experiment get -n' + self.id_str - self.exp_desc['nodes'] = call_cli(cmd) + cmd = "iotlab-experiment get -n" + self.id_str + self.exp_desc["nodes"] = call_cli(cmd) - self.assertIsInstance(self.exp_desc['nodes'], dict) - self.assertIsInstance(self.exp_desc['nodes']['items'][0], dict) + self.assertIsInstance(self.exp_desc["nodes"], dict) + self.assertIsInstance(self.exp_desc["nodes"]["items"][0], dict) - cmd = f'iotlab-experiment get --nodes-id -i {self.exp_id}' + cmd = f"iotlab-experiment get --nodes-id -i {self.exp_id}" call_cli(cmd) - cmd = f'iotlab-experiment get --nodes -i {self.exp_id}' + cmd = f"iotlab-experiment get --nodes -i {self.exp_id}" call_cli(cmd) - call_cli(f'iotlab-experiment get -a -i {self.exp_id}') + call_cli(f"iotlab-experiment get -a -i {self.exp_id}") - def _wait_state_or_finished(self, states='Stopped,Error,Terminated'): - """ Wait experiment get in state, or states error and terminated """ - cmd = ( - f"iotlab-experiment wait --state {states} " - f"--step 5 -i {self.exp_id}" - ) + def _wait_state_or_finished(self, states="Stopped,Error,Terminated"): + """Wait experiment get in state, or states error and terminated""" + cmd = f"iotlab-experiment wait --state {states} --step 5 -i {self.exp_id}" LOGGER.info(cmd) return call_cli(cmd) @@ -288,54 +281,55 @@ def tearDownClass(cls): @classmethod def cleanup(cls): - """ Cleanup currently running experiments """ + """Cleanup currently running experiments""" LOGGER.debug("cleanup") - cmd = 'iotlab-experiment get --list --state Running,Waiting' + cmd = "iotlab-experiment get --list --state Running,Waiting" experiments = call_cli(cmd)["items"] for exp in experiments: exp_id = exp["id"] - call_cli(f'iotlab-experiment stop -i {exp_id}') + call_cli(f"iotlab-experiment stop -i {exp_id}") - remote_profs = call_cli('iotlab-profile get --list') - profiles_names = [p['profilename'] for p in remote_profs] - if 'test_m3' in profiles_names: - call_cli('iotlab-profile del -n test_m3') + remote_profs = call_cli("iotlab-profile get --list") + profiles_names = [p["profilename"] for p in remote_profs] + if "test_m3" in profiles_names: + call_cli("iotlab-profile del -n test_m3") class TestCliToolsProfile(unittest.TestCase): - """ Test the cli tools profile """ + """Test the cli tools profile""" + profile = { - 'm3': 'test_cli_profile_m3', - 'm3_full': 'test_cli_profile_m3_full', - 'm3_sniffer': 'test_cli_profile_m3_sniffer', - 'wsn430': 'test_cli_profile_wsn430', - 'wsn430_full': 'test_cli_profile_wsn430_full', + "m3": "test_cli_profile_m3", + "m3_full": "test_cli_profile_m3_full", + "m3_sniffer": "test_cli_profile_m3_sniffer", + "wsn430": "test_cli_profile_wsn430", + "wsn430_full": "test_cli_profile_wsn430_full", } @classmethod def setUpClass(cls): - """ Remove the tests profiles if they are here """ - remote_profs = call_cli('iotlab-profile get --list') - profiles_names = [p['profilename'] for p in remote_profs] + """Remove the tests profiles if they are here""" + remote_profs = call_cli("iotlab-profile get --list") + profiles_names = [p["profilename"] for p in remote_profs] for prof in cls.profile.values(): if prof in profiles_names: - call_cli(f'iotlab-profile del --name {prof}') + call_cli(f"iotlab-profile del --name {prof}") def _del_prof(self, name): - """ Add a profile and get it to check it's the same """ - cmd_result = call_cli(f'iotlab-profile del --name {name}') + """Add a profile and get it to check it's the same""" + cmd_result = call_cli(f"iotlab-profile del --name {name}") self.assertIsNone(cmd_result) def _add_profile_simple(self, cmd, name): - """ Add a profile and get it to check it's the same """ - profile_dict = call_cli(cmd + ' --json') + """Add a profile and get it to check it's the same""" + profile_dict = call_cli(cmd + " --json") profile_dict = {k: v for k, v in profile_dict.items() if v is not None} # add profile return name self.assertEqual(profile_dict, call_cli(cmd)) - get_profile_dict = call_cli(f'iotlab-profile get --name {name}') + get_profile_dict = call_cli(f"iotlab-profile get --name {name}") # compare that initial dict is a subset of result dict # can't '<=' dict in python3, so update result dict with initial # values and see if it stays equals @@ -344,157 +338,193 @@ def _add_profile_simple(self, cmd, name): self.assertEqual(cmp_prof_dict, get_profile_dict) def _get_and_load(self, name): - """ Get a profile and try loading it + """Get a profile and try loading it We then check that getting both profiles return the same output """ - get_profile_dict = call_cli(f'iotlab-profile get --name {name}') + get_profile_dict = call_cli(f"iotlab-profile get --name {name}") - call_cli(f'iotlab-profile del --name {name}') + call_cli(f"iotlab-profile del --name {name}") - with NamedTemporaryFile(mode='w+') as prof: + with NamedTemporaryFile(mode="w+") as prof: prof.write(json.dumps(get_profile_dict)) prof.flush() - load_ret = call_cli(f'iotlab-profile load --file {prof.name}') + load_ret = call_cli(f"iotlab-profile load --file {prof.name}") # returned name are the same self.assertEqual(get_profile_dict, load_ret) - get_loaded_profile = call_cli(f'iotlab-profile get --name {name}') + get_loaded_profile = call_cli(f"iotlab-profile get --name {name}") # returned profile are the same self.assertEqual(get_profile_dict, get_loaded_profile) def _add_prof(self, cmd, name): - """ Test adding and loading a user profile """ + """Test adding and loading a user profile""" self._add_profile_simple(cmd.format(name), name) self._get_and_load(name) # erase same profile def test_m3_profile(self): - """ Test creating M3 profiles and deleting them """ + """Test creating M3 profiles and deleting them""" - profs = call_cli('iotlab-profile get -l') - profiles_names = {p['profilename'] for p in profs} + profs = call_cli("iotlab-profile get -l") + profiles_names = {p["profilename"] for p in profs} self._add_prof( - 'iotlab-profile addm3 -n {} -sniffer -channels 11', - self.profile['m3'] + "iotlab-profile addm3 -n {} -sniffer -channels 11", self.profile["m3"] ) - profiles_names.add(self.profile['m3']) + profiles_names.add(self.profile["m3"]) - prof_cmd = 'iotlab-profile addm3 -n {} -p dc' - prof_cmd += ' -power -voltage -current -period 8244 -avg 1024' - prof_cmd += ' -rssi -channels 11 16 21 26 -num 255 -rperiod 65535' - self._add_prof(prof_cmd, self.profile['m3_full']) - profiles_names.add(self.profile['m3_full']) + prof_cmd = "iotlab-profile addm3 -n {} -p dc" + prof_cmd += " -power -voltage -current -period 8244 -avg 1024" + prof_cmd += " -rssi -channels 11 16 21 26 -num 255 -rperiod 65535" + self._add_prof(prof_cmd, self.profile["m3_full"]) + profiles_names.add(self.profile["m3_full"]) - prof_cmd = 'iotlab-profile addm3 -n {}' - prof_cmd += ' -sniffer -channels 11' - self._add_prof(prof_cmd, self.profile['m3_sniffer']) - profiles_names.add(self.profile['m3_sniffer']) + prof_cmd = "iotlab-profile addm3 -n {}" + prof_cmd += " -sniffer -channels 11" + self._add_prof(prof_cmd, self.profile["m3_sniffer"]) + profiles_names.add(self.profile["m3_sniffer"]) # check that profiles have been added - profs = call_cli('iotlab-profile get -l') - profiles_names_new = {p['profilename'] for p in profs} + profs = call_cli("iotlab-profile get -l") + profiles_names_new = {p["profilename"] for p in profs} self.assertEqual(profiles_names, profiles_names_new) - self._del_prof(self.profile['m3']) - self._del_prof(self.profile['m3_full']) - self._del_prof(self.profile['m3_sniffer']) + self._del_prof(self.profile["m3"]) + self._del_prof(self.profile["m3_full"]) + self._del_prof(self.profile["m3_sniffer"]) class TestAnErrorCase(unittest.TestCase): - """ Test cli tools error cases """ + """Test cli tools error cases""" def test_node_parser_errors(self): - """ Test some node parser errors """ + """Test some node parser errors""" # invalid argument number self.assertRaises( - SystemExit, call_cli, - 'iotlab-node --reset -l {m3},m3'.format(**NODES), - print_err=False) + SystemExit, + call_cli, + "iotlab-node --reset -l {m3},m3".format(**NODES), + print_err=False, + ) # invalid site self.assertRaises( - SystemExit, call_cli, 'iotlab-node --reset -l invalid_site,m3,1', - print_err=False) + SystemExit, + call_cli, + "iotlab-node --reset -l invalid_site,m3,1", + print_err=False, + ) # invalid archi self.assertRaises( - SystemExit, call_cli, - 'iotlab-node --reset -l {m3},m4,1'.format(**NODES), - print_err=False) + SystemExit, + call_cli, + "iotlab-node --reset -l {m3},m4,1".format(**NODES), + print_err=False, + ) # invalid state self.assertRaises( - SystemExit, call_cli, - ('iotlab-experiment --user test --password test get ' + - ' -l --state=Terminateded'), - print_err=False) + SystemExit, + call_cli, + ( + "iotlab-experiment --user test --password test get " + + " -l --state=Terminateded" + ), + print_err=False, + ) def test_experiment_parser_errors(self): - """ Test some experiment parser errors """ + """Test some experiment parser errors""" self.assertRaises( - SystemExit, call_cli, - 'iotlab-experiment submit -d 20 -l {m3},m3,70-1'.format(**NODES), - print_err=False) + SystemExit, + call_cli, + "iotlab-experiment submit -d 20 -l {m3},m3,70-1".format(**NODES), + print_err=False, + ) # alias invalid archi self.assertRaises( - SystemExit, call_cli, - ('iotlab-experiment submit -d 20' + - ' -l 3,site={m3}+archi=inval+mobile=1'.format(**NODES)), - print_err=False) + SystemExit, + call_cli, + ( + "iotlab-experiment submit -d 20" + + " -l 3,site={m3}+archi=inval+mobile=1".format(**NODES) + ), + print_err=False, + ) # too many values self.assertRaises( - SystemExit, call_cli, - ('iotlab-experiment submit -d 20' + - '-l {m3},m3,1,fw,prof,extra'.format(**NODES)), - print_err=False) + SystemExit, + call_cli, + ( + "iotlab-experiment submit -d 20" + + "-l {m3},m3,1,fw,prof,extra".format(**NODES) + ), + print_err=False, + ) # alias and physical self.assertRaises( - SystemExit, call_cli, - ('iotlab-experiment submit -d 20' + - ' -l {m3},m3,1' + - ' -l 3,site={m3}+archi=m3:at86rf231+mobile=true').format(**NODES), - print_err=False) + SystemExit, + call_cli, + ( + "iotlab-experiment submit -d 20" + + " -l {m3},m3,1" + + " -l 3,site={m3}+archi=m3:at86rf231+mobile=true" + ).format(**NODES), + print_err=False, + ) # alias invalid values self.assertRaises( - SystemExit, call_cli, - ('iotlab-experiment submit -d 20' + - ' -l 3,site={m3}+archi=m3:at86rf231+inval_prop=2'.format(**NODES) - ), - print_err=False) + SystemExit, + call_cli, + ( + "iotlab-experiment submit -d 20" + + " -l 3,site={m3}+archi=m3:at86rf231+inval_prop=2".format(**NODES) + ), + print_err=False, + ) # No site or archi self.assertRaises( - SystemExit, call_cli, - ('iotlab-experiment submit -d 20 -l 3,archi=m3:at86rf231'), - print_err=False) + SystemExit, + call_cli, + ("iotlab-experiment submit -d 20 -l 3,archi=m3:at86rf231"), + print_err=False, + ) # invalid properties self.assertRaises( - SystemExit, call_cli, - ('iotlab-experiment submit -d 20 -l 3,archi='), - print_err=False) + SystemExit, + call_cli, + ("iotlab-experiment submit -d 20 -l 3,archi="), + print_err=False, + ) self.assertRaises( - SystemExit, call_cli, - ('iotlab-experiment submit -d 20 -l 3,archi=val_1+archi=val_2'), - print_err=False) + SystemExit, + call_cli, + ("iotlab-experiment submit -d 20 -l 3,archi=val_1+archi=val_2"), + print_err=False, + ) # invalid mobile self.assertRaises( - SystemExit, call_cli, - ('iotlab-experiment submit -d 20' + - ' -l 3,archi=m3:at86rf231+site={m3}+mobile=turtlebot').format( - **NODES), - print_err=False) + SystemExit, + call_cli, + ( + "iotlab-experiment submit -d 20" + + " -l 3,archi=m3:at86rf231+site={m3}+mobile=turtlebot" + ).format(**NODES), + print_err=False, + ) def test_rest_errors(self): - """ Test some rest error """ - self.assertRaises(SystemExit, call_cli, - 'iotlab-experiment get -p -i 0', - print_err=False) + """Test some rest error""" + self.assertRaises( + SystemExit, call_cli, "iotlab-experiment get -p -i 0", print_err=False + ) def treat_cli_return(stdout, stderr): @@ -510,53 +540,62 @@ def treat_cli_return(stdout, stderr): def call_cli(cmd, print_err=True): - """ Call cli tool """ + """Call cli tool""" argv = shlex.split(cmd) stdout = StringIO() stderr = StringIO() LOGGER.info(cmd) try: - with patch('sys.stderr', stderr): - with patch('sys.stdout', stdout): - with patch('sys.argv', argv): + with patch("sys.stderr", stderr): + with patch("sys.stdout", stdout): + with patch("sys.argv", argv): runpy.run_path(argv[0]) except SystemExit as err: if print_err: - LOGGER.error('%r', stderr.getvalue()) + LOGGER.error("%r", stderr.getvalue()) raise err return treat_cli_return(stdout, stderr) def try_config_iotlab_test_account(): - """ Try to configure tests to use 'iotlab' account """ + """Try to configure tests to use 'iotlab' account""" try: - password = os.environ['IOTLAB_TEST_PASSWORD'] + password = os.environ["IOTLAB_TEST_PASSWORD"] # use a fake auth_file - os.environ['IOTLAB_PASSWORD_FILE'] = 'test_auth_file' + os.environ["IOTLAB_PASSWORD_FILE"] = "test_auth_file" call_cli(f"iotlab-auth --user iotlab --password {password}") except KeyError: pass def opts_parser(): - """ Argument parser """ - parser = argparse.ArgumentParser('Integration tests') - parser.add_argument('--dev', action='store_true', default=False, - help='Run againt dev platform, default to prod') - parser.add_argument('--stop', action='store_true', default=False, - help='Stop tests after first error') - parser.add_argument('--verbose', action='store_const', default=1, - const=4, help='Verbose output') + """Argument parser""" + parser = argparse.ArgumentParser("Integration tests") + parser.add_argument( + "--dev", + action="store_true", + default=False, + help="Run againt dev platform, default to prod", + ) + parser.add_argument( + "--stop", + action="store_true", + default=False, + help="Stop tests after first error", + ) + parser.add_argument( + "--verbose", action="store_const", default=1, const=4, help="Verbose output" + ) return parser -if __name__ == '__main__': +if __name__ == "__main__": failfast = False opts = opts_parser().parse_args() if opts.dev: - os.environ['IOTLAB_API_URL'] = 'https://devwww.iot-lab.info/rest/' - NODES = SITES['dev'] + os.environ["IOTLAB_API_URL"] = "https://devwww.iot-lab.info/rest/" + NODES = SITES["dev"] try_config_iotlab_test_account() unittest.main(argv=[__file__], verbosity=opts.verbose, failfast=opts.stop) diff --git a/iotlabcli/node.py b/iotlabcli/node.py index 38c0b34..ea67870 100644 --- a/iotlabcli/node.py +++ b/iotlabcli/node.py @@ -19,22 +19,23 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Implement the 'node' requests """ +"""Implement the 'node' requests""" import json + from iotlabcli import helpers -NODE_FILENAME = 'nodes.json' -EXPERIMENT = 'experiment.json' +NODE_FILENAME = "nodes.json" +EXPERIMENT = "experiment.json" def _node_command_flash(api, exp_id, nodes_list, cmd_opt): - assert cmd_opt is not None, '`cmd_opt` required for update' + assert cmd_opt is not None, "`cmd_opt` required for update" files = helpers.FilesDict() files.add_file(cmd_opt) - if cmd_opt.endswith('.bin'): - files[EXPERIMENT] = json.dumps({'nodes': nodes_list, 'offset': 0}) + if cmd_opt.endswith(".bin"): + files[EXPERIMENT] = json.dumps({"nodes": nodes_list, "offset": 0}) return api.node_update(exp_id, files, binary=True) files[NODE_FILENAME] = json.dumps(nodes_list) @@ -42,7 +43,7 @@ def _node_command_flash(api, exp_id, nodes_list, cmd_opt): def _node_command_profile_load(api, exp_id, nodes_list, cmd_opt): - assert cmd_opt is not None, '`cmd_opt` required for update' + assert cmd_opt is not None, "`cmd_opt` required for update" files = helpers.FilesDict() files.add_file(cmd_opt) @@ -51,7 +52,7 @@ def _node_command_profile_load(api, exp_id, nodes_list, cmd_opt): def node_command(api, command, exp_id, nodes_list=(), cmd_opt=None): - """ Launch commands (start, stop, reset, update) + """Launch commands (start, stop, reset, update) on nodes (JSONArray) user experiment :param api: API Rest api object @@ -61,18 +62,26 @@ def node_command(api, command, exp_id, nodes_list=(), cmd_opt=None): Empty list runs on all nodes :param cmd_opt: Firmware path for update, profile name for profile """ - assert command in ('flash', 'flash-idle', - 'profile', 'profile-load', 'profile-reset', - 'start', 'stop', 'reset', - 'debug-start', 'debug-stop') + assert command in ( + "flash", + "flash-idle", + "profile", + "profile-load", + "profile-reset", + "start", + "stop", + "reset", + "debug-start", + "debug-stop", + ) result = None - if command == 'flash': + if command == "flash": result = _node_command_flash(api, exp_id, nodes_list, cmd_opt) - elif command == 'profile-load': + elif command == "profile-load": result = _node_command_profile_load(api, exp_id, nodes_list, cmd_opt) - elif command == 'profile': - result = api.node_command('monitoring', exp_id, nodes_list, cmd_opt) + elif command == "profile": + result = api.node_command("monitoring", exp_id, nodes_list, cmd_opt) else: result = api.node_command(command, exp_id, nodes_list) diff --git a/iotlabcli/parser/__init__.py b/iotlabcli/parser/__init__.py index f4c6aaa..4b131c3 100644 --- a/iotlabcli/parser/__init__.py +++ b/iotlabcli/parser/__init__.py @@ -19,4 +19,4 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" iotlabcli parser submodule """ +"""iotlabcli parser submodule""" diff --git a/iotlabcli/parser/auth.py b/iotlabcli/parser/auth.py index 7d44930..a29a05d 100644 --- a/iotlabcli/parser/auth.py +++ b/iotlabcli/parser/auth.py @@ -21,13 +21,13 @@ """Authentication parser""" -import sys -import getpass import argparse +import getpass +import sys from argparse import RawTextHelpFormatter -from iotlabcli.parser import common from iotlabcli import auth +from iotlabcli.parser import common AUTH_PARSER = """ @@ -39,40 +39,53 @@ def parse_options(): - """ Handle iotlab-auth command-line options with argparse """ + """Handle iotlab-auth command-line options with argparse""" parent_parser = common.base_parser() # We create top level parser parser = argparse.ArgumentParser( - parents=[parent_parser], formatter_class=RawTextHelpFormatter, - description=AUTH_PARSER) - - parser.add_argument('-k', '--add-ssh-key', dest='add_key', - action='store_true', - help="add user's ssh public key to iot-lab account") - parser.add_argument('-i', '--identity-file', dest='identity_file', - default=auth.IDENTITY_FILE, - help="specify ssh identity file to use") - parser.add_argument('-l', '--list-ssh-keys', dest='list_keys', - action='store_true', - help="list ssh keys configured on IoT-LAB account") + parents=[parent_parser], + formatter_class=RawTextHelpFormatter, + description=AUTH_PARSER, + ) + + parser.add_argument( + "-k", + "--add-ssh-key", + dest="add_key", + action="store_true", + help="add user's ssh public key to iot-lab account", + ) + parser.add_argument( + "-i", + "--identity-file", + dest="identity_file", + default=auth.IDENTITY_FILE, + help="specify ssh identity file to use", + ) + parser.add_argument( + "-l", + "--list-ssh-keys", + dest="list_keys", + action="store_true", + help="list ssh keys configured on IoT-LAB account", + ) return parser def auth_parse_and_run(opts): # noqa: C901 - """ Parse namespace 'opts' object and execute requested command + """Parse namespace 'opts' object and execute requested command :returns: result object """ if opts.username is None: if not opts.add_key and not opts.list_keys: - raise RuntimeError( - 'the following arguments are required: -u/--user') + raise RuntimeError("the following arguments are required: -u/--user") if opts.list_keys: auth.ssh_keys() if opts.add_key: auth.add_ssh_key(opts.identity_file) - return 'Key added' + return "Key added" return None password = opts.password or getpass.getpass() @@ -85,13 +98,13 @@ def auth_parse_and_run(opts): # noqa: C901 auth.add_ssh_key(opts.identity_file) except ValueError as exc: print(exc) - return 'Written' + return "Written" - raise RuntimeError('Wrong login:password') + raise RuntimeError("Wrong login:password") def main(args=None): - """ Main command-line execution loop." """ + """Main command-line execution loop." """ args = args or sys.argv[1:] parser = parse_options() common.main_cli(auth_parse_and_run, parser, args) diff --git a/iotlabcli/parser/common.py b/iotlabcli/parser/common.py index a392d09..9d886bb 100644 --- a/iotlabcli/parser/common.py +++ b/iotlabcli/parser/common.py @@ -19,12 +19,12 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Common parsing methods """ +"""Common parsing methods""" -import sys -import errno import argparse import contextlib +import errno +import sys from collections import OrderedDict # pylint: disable=wrong-import-order @@ -38,15 +38,14 @@ import jmespath import iotlabcli -from iotlabcli import helpers -from iotlabcli import rest +from iotlabcli import helpers, rest -DOMAIN_DNS = 'iot-lab.info' +DOMAIN_DNS = "iot-lab.info" def base_parser(user_required=False): - """ Base parser giving 'user' 'password' and 'version' arguments - :param user_required: set 'user' argument as required or not """ + """Base parser giving 'user' 'password' and 'version' arguments + :param user_required: set 'user' argument as required or not""" parser = argparse.ArgumentParser(add_help=False) add_auth_arguments(parser, user_required) add_version(parser) @@ -56,32 +55,46 @@ def base_parser(user_required=False): def add_auth_arguments(parser, usr_required=False): - """ Add 'user' and 'password' arguments - :param user_required: set 'user' argument as required or not """ - parser.add_argument('-u', '--user', dest='username', required=usr_required) - parser.add_argument('-p', '--password', dest='password') + """Add 'user' and 'password' arguments + :param user_required: set 'user' argument as required or not""" + parser.add_argument("-u", "--user", dest="username", required=usr_required) + parser.add_argument("-p", "--password", dest="password") def add_version(parser): - """ Add 'version' argument """ + """Add 'version' argument""" parser.add_argument( - '-v', '--version', action='version', version=iotlabcli.__version__) + "-v", "--version", action="version", version=iotlabcli.__version__ + ) def add_output_formatter(parser): - """ Add '--jmespath' argument """ + """Add '--jmespath' argument""" group = parser.add_argument_group("Output Format") - group.add_argument('--jmespath', '--jp', type=jmespath.compile, - help="Query output using `jmespath` syntax") - group.add_argument('--format', '--fmt', type=eval, - help="Format function, default `helpers.json_dumps`") + group.add_argument( + "--jmespath", + "--jp", + type=jmespath.compile, + help="Query output using `jmespath` syntax", + ) + group.add_argument( + "--format", + "--fmt", + type=eval, + help="Format function, default `helpers.json_dumps`", + ) def add_expid_arg(parser, required=False): """Add '-i' / '--id' for 'experiment_id' option.""" - parser.add_argument('-i', '--id', dest='experiment_id', type=int, - required=required, - help='experiment id submission') + parser.add_argument( + "-i", + "--id", + dest="experiment_id", + type=int, + required=required, + help="experiment id submission", + ) class HelpAction(argparse.Action): @@ -90,15 +103,18 @@ class HelpAction(argparse.Action): HELPMSG = None def __call__(self, parser, namespace, values, option_string=None): - print(self.HELPMSG, end='') + print(self.HELPMSG, end="") parser.exit() @classmethod def for_message(cls, msg): """Create action for help message.""" + class HelpActionWithMessage(cls): """Action with custom 'help' message.""" + HELPMSG = msg + return HelpActionWithMessage @classmethod @@ -114,8 +130,8 @@ def add_help(cls, parser, name, description, msg): parser.add_argument(name, action=action, nargs=0, help=description) -def print_result(result, jmespath_expr=None, format_function=None): - """ Print result vule """ +def print_result(result, jmespath_expr=None, format_function=None): # noqa: C901 + """Print result vule""" format_function = format_function or helpers.json_dumps # early bail out if nothing was returned @@ -142,8 +158,10 @@ def print_result(result, jmespath_expr=None, format_function=None): def catch_missing_auth_cli(): """Catch HTTPError 401 and display a message on missing iotlab-auth.""" - auth_cli_err = ("HTTP Error 401: Unauthorized: Wrong login/password\n\n" - "\tRegister your login:password using `iotlab-auth`\n") + auth_cli_err = ( + "HTTP Error 401: Unauthorized: Wrong login/password\n\n" + "\tRegister your login:password using `iotlab-auth`\n" + ) try: yield except HTTPError as err: @@ -153,8 +171,8 @@ def catch_missing_auth_cli(): sys.exit(1) -def main_cli(function, parser, args=None): # flake8: noqa - """ Main command-line execution. """ +def main_cli(function, parser, args=None): # noqa: C901 + """Main command-line execution.""" args = args or sys.argv[1:] try: with catch_missing_auth_cli(): @@ -177,13 +195,13 @@ def main_cli(function, parser, args=None): # flake8: noqa def sites_list(): - """ Return the list of sites """ + """Return the list of sites""" sites_dict = rest.Api.get_sites() return [site["site"] for site in sites_dict["items"]] def check_site_with_server(site_name, _sites_list=None): - """ Check if the given site exists by requesting the server list. + """Check if the given site exists by requesting the server list. If sites_list is given, it is used instead of doing a remote request >>> _sites_list = ['strasbourg', 'grenoble'] @@ -196,13 +214,13 @@ def check_site_with_server(site_name, _sites_list=None): sites = _sites_list or sites_list() if site_name in sites: return # site_name is valid - raise argparse.ArgumentTypeError(f'Unknown site name {site_name!r}') + raise argparse.ArgumentTypeError(f"Unknown site name {site_name!r}") def site_with_domain_checked(site, domain=DOMAIN_DNS): """Return site with domain and check site exists.""" check_site_with_server(site) - return f'{site}.{domain}' + return f"{site}.{domain}" def nodes_list_from_info(site, archi, nodes_str): @@ -231,23 +249,23 @@ def nodes_list_from_info(site, archi, nodes_str): def nodes_id_list(archi, nodes_list): - """ Expand short nodes_list 'archi', '1-5+6+8-12' + """Expand short nodes_list 'archi', '1-5+6+8-12' to a regular nodes list """ nodes_num_list = expand_short_nodes_list(nodes_list) - node_fmt = f'{archi}-%u' + node_fmt = f"{archi}-%u" nodes = [node_fmt % num for num in nodes_num_list] return nodes def _expand_minus_str(minus_nodes_str): - """ Expand a '1-5' or '6' string to a list on integer + """Expand a '1-5' or '6' string to a list on integer :raises: ValueError on invalid values """ - minus_node = minus_nodes_str.split('-') + minus_node = minus_nodes_str.split("-") res = None if len(minus_node) == 1: # ['6'] @@ -266,7 +284,7 @@ def _expand_minus_str(minus_nodes_str): def expand_short_nodes_list(nodes_str): - """ Expand short nodes_list '1-5+6+8-12' to a regular nodes list + """Expand short nodes_list '1-5+6+8-12' to a regular nodes list >>> expand_short_nodes_list('1-4+6+7-8') [1, 2, 3, 4, 6, 7, 8] @@ -290,29 +308,41 @@ def expand_short_nodes_list(nodes_str): try: # '1-4+6+8-8' - nodes_ll = [_expand_minus_str(minus_nodes_str) for minus_nodes_str in - nodes_str.split('+')] + nodes_ll = [ + _expand_minus_str(minus_nodes_str) + for minus_nodes_str in nodes_str.split("+") + ] # [[1, 2, 3], [6], [12]] return helpers.flatten_list_list(nodes_ll) except ValueError: # invalid: 6-3 or 6-7-8 or non int values - raise ValueError(f'Invalid nodes list: {nodes_str} ([0-9+-])') + raise ValueError(f"Invalid nodes list: {nodes_str} ([0-9+-])") def add_nodes_selection_list(parser): - """ Add '-l' and '-e' experiment nodes selection """ + """Add '-l' and '-e' experiment nodes selection""" list_group = parser.add_mutually_exclusive_group() list_group.add_argument( - '-e', '--exclude', action='append', type=nodes_list_from_str, - dest='exclude_nodes_list', help='exclude nodes list') + "-e", + "--exclude", + action="append", + type=nodes_list_from_str, + dest="exclude_nodes_list", + help="exclude nodes list", + ) list_group.add_argument( - '-l', '--list', action='append', type=nodes_list_from_str, - dest='nodes_list', help='nodes list') + "-l", + "--list", + action="append", + type=nodes_list_from_str, + dest="nodes_list", + help="nodes list", + ) def list_nodes(api, exp_id, nodes_ll=None, excl_nodes_ll=None): - """ Return the list of nodes where the command will apply """ + """Return the list of nodes where the command will apply""" if nodes_ll is not None: # flatten lists into one @@ -332,14 +362,14 @@ def list_nodes(api, exp_id, nodes_ll=None, excl_nodes_ll=None): def _get_experiment_nodes_list(api, exp_id): - """ Get the nodes_list for given experiment""" - exp_nodes = api.get_experiment_info(exp_id, 'nodes') + """Get the nodes_list for given experiment""" + exp_nodes = api.get_experiment_info(exp_id, "nodes") nodes = [res["network_address"] for res in exp_nodes["items"]] return nodes def nodes_list_from_str(nodes_list_str): - """ Convert the nodes_list_str to a list of nodes hostname + """Convert the nodes_list_str to a list of nodes hostname Checks that given site exist :param nodes_list_str: short nodes format: site_name,archi,node_id_list example: 'grenoble,m3,1-34+72' @@ -347,9 +377,10 @@ def nodes_list_from_str(nodes_list_str): """ try: # 'grenoble,m3,1-34+72' -> ['grenoble', 'm3', '1-34+72'] - site, archi, nodes_str = nodes_list_str.split(',') + site, archi, nodes_str = nodes_list_str.split(",") except ValueError: raise argparse.ArgumentTypeError( - f'Invalid number of argument in nodes list: {nodes_list_str!r}') + f"Invalid number of argument in nodes list: {nodes_list_str!r}" + ) check_site_with_server(site) # needs an external request return nodes_list_from_info(site, archi, nodes_str) diff --git a/iotlabcli/parser/experiment.py b/iotlabcli/parser/experiment.py index 8f675fb..538e941 100644 --- a/iotlabcli/parser/experiment.py +++ b/iotlabcli/parser/experiment.py @@ -19,19 +19,15 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Experiment parser """ +"""Experiment parser""" +import argparse import sys import time -from datetime import datetime - -import argparse from argparse import ArgumentParser, RawTextHelpFormatter +from datetime import datetime -from iotlabcli import experiment -from iotlabcli import helpers -from iotlabcli import rest -from iotlabcli import auth +from iotlabcli import auth, experiment, helpers, rest from iotlabcli.parser import common, help_msgs EXPERIMENT_PARSER = """ @@ -42,18 +38,18 @@ def parse_options(): - """ Handle iotlab-experiment command-line options with argparse """ + """Handle iotlab-experiment command-line options with argparse""" parent_parser = common.base_parser() # We create top level parser parser = ArgumentParser( description=EXPERIMENT_PARSER, parents=[parent_parser], - epilog=help_msgs.PARSER_EPILOG.format( - cli='experiment', option='submit'), - formatter_class=RawTextHelpFormatter) + epilog=help_msgs.PARSER_EPILOG.format(cli="experiment", option="submit"), + formatter_class=RawTextHelpFormatter, + ) - subparsers = parser.add_subparsers(dest='command') + subparsers = parser.add_subparsers(dest="command") subparsers.required = True # not required by default in Python3 # ####### SUBMIT PARSER ############### @@ -63,130 +59,217 @@ def parse_options(): parser_add_script_subparser(subparsers) # ####### STOP PARSER ############### - stop_parser = subparsers.add_parser('stop', help='stop user experiment') + stop_parser = subparsers.add_parser("stop", help="stop user experiment") common.add_expid_arg(stop_parser) # ####### GET PARSER ############### get_parser = subparsers.add_parser( - 'get', + "get", epilog=help_msgs.GET_EPILOG, - help='get user\'s experiment', - formatter_class=RawTextHelpFormatter) + help="get user's experiment", + formatter_class=RawTextHelpFormatter, + ) common.add_expid_arg(get_parser) get_group = get_parser.add_mutually_exclusive_group(required=True) get_group.add_argument( - '-d', '--deployment', dest='get_cmd', action='store_const', - const='deployment', help='get an experiment deployment') + "-d", + "--deployment", + dest="get_cmd", + action="store_const", + const="deployment", + help="get an experiment deployment", + ) get_group.add_argument( - '-n', '--nodes', dest='get_cmd', action='store_const', - const='nodes', help='get an experiment nodes list') + "-n", + "--nodes", + dest="get_cmd", + action="store_const", + const="nodes", + help="get an experiment nodes list", + ) get_group.add_argument( - '-ni', '--nodes-id', dest='get_cmd', action='store_const', - const='nodes_ids', help=('get an experiment nodes id list ' - '(EXP_LIST format : 1-34+72)')) + "-ni", + "--nodes-id", + dest="get_cmd", + action="store_const", + const="nodes_ids", + help=("get an experiment nodes id list (EXP_LIST format : 1-34+72)"), + ) get_group.add_argument( - '-p', '--print', dest='get_cmd', action='store_const', - const='', help='get an experiment description') + "-p", + "--print", + dest="get_cmd", + action="store_const", + const="", + help="get an experiment description", + ) get_group.add_argument( - '-a', '--archive', dest='get_cmd', action='store_const', - const='data', help='get an experiment archive (tar.gz)') + "-a", + "--archive", + dest="get_cmd", + action="store_const", + const="data", + help="get an experiment archive (tar.gz)", + ) # --list with its options get_group.add_argument( - '-l', '--list', dest='get_cmd', action='store_const', - const='experiment_list', help='get user\'s experiment list') + "-l", + "--list", + dest="get_cmd", + action="store_const", + const="experiment_list", + help="get user's experiment list", + ) get_group.add_argument( - '-r', '--resources', dest='get_cmd', action='store_const', - const='resources', help='DEPRECATED: use -n or --nodes option') + "-r", + "--resources", + dest="get_cmd", + action="store_const", + const="resources", + help="DEPRECATED: use -n or --nodes option", + ) get_group.add_argument( - '-ri', '--resources-id', dest='get_cmd', action='store_const', - const='resources_ids', - help=('DEPRECATED: use -ni or --nodes-id option')) + "-ri", + "--resources-id", + dest="get_cmd", + action="store_const", + const="resources_ids", + help=("DEPRECATED: use -ni or --nodes-id option"), + ) get_group.add_argument( - '-s', '--exp-state', dest='get_cmd', action='store_const', - const='state', help='DEPRECATED: use -p or --print option') + "-s", + "--exp-state", + dest="get_cmd", + action="store_const", + const="state", + help="DEPRECATED: use -p or --print option", + ) get_group.add_argument( - '-st', '--start-time', dest='get_cmd', action='store_const', - const='start_date', - help='DEPRECATED: use -p or --print option') - get_parser.add_argument('--offset', default=0, type=int, - help='experiment list start index') + "-st", + "--start-time", + dest="get_cmd", + action="store_const", + const="start_date", + help="DEPRECATED: use -p or --print option", + ) + get_parser.add_argument( + "--offset", default=0, type=int, help="experiment list start index" + ) - get_parser.add_argument('--limit', default=0, type=int, - help='experiment list maximum length') + get_parser.add_argument( + "--limit", default=0, type=int, help="experiment list maximum length" + ) - get_parser.add_argument('--state', help='experiment list state filter') + get_parser.add_argument("--state", help="experiment list state filter") - get_group.add_argument('-e', '--experiments', dest='get_cmd', - action='store_const', - const='experiments', - help='get running experiments ids') + get_group.add_argument( + "-e", + "--experiments", + dest="get_cmd", + action="store_const", + const="experiments", + help="get running experiments ids", + ) get_parser.add_argument( - '--active', action='store_true', default=False, - help='experiments: include waiting/starting experiments') + "--active", + action="store_true", + default=False, + help="experiments: include waiting/starting experiments", + ) # ####### LOAD PARSER ############### - load_parser = subparsers.add_parser('load', epilog=help_msgs.LOAD_EPILOG, - help='load and submit user experiment', - formatter_class=RawTextHelpFormatter) - - load_parser.add_argument('-f', '--file', dest='path_file', - metavar='EXP_JSON', required=True, - help='experiment path file') + load_parser = subparsers.add_parser( + "load", + epilog=help_msgs.LOAD_EPILOG, + help="load and submit user experiment", + formatter_class=RawTextHelpFormatter, + ) - _load_list_help = ('file path for firmware/script/... if not in' - ' current directory.') + load_parser.add_argument( + "-f", + "--file", + dest="path_file", + metavar="EXP_JSON", + required=True, + help="experiment path file", + ) - load_parser.add_argument('-l', '--list', metavar='FILEPATH', - dest='files', default=[], - type=(lambda s: s.split(',')), action='append', - help=_load_list_help) + _load_list_help = "file path for firmware/script/... if not in current directory." + + load_parser.add_argument( + "-l", + "--list", + metavar="FILEPATH", + dest="files", + default=[], + type=(lambda s: s.split(",")), + action="append", + help=_load_list_help, + ) # ####### RELOAD PARSER ############### - reload_parser = subparsers.add_parser('reload', - epilog=help_msgs.RELOAD_EPILOG, - help='reload user experiment', - formatter_class=RawTextHelpFormatter) + reload_parser = subparsers.add_parser( + "reload", + epilog=help_msgs.RELOAD_EPILOG, + help="reload user experiment", + formatter_class=RawTextHelpFormatter, + ) common.add_expid_arg(reload_parser, required=True) - _parser_add_duration_and_reservation(reload_parser, - duration_required=False) + _parser_add_duration_and_reservation(reload_parser, duration_required=False) # ####### INFO PARSER ############### class DeprecateHelpFormatter(argparse.HelpFormatter): - """ Add drepecate help formatter """ + """Add drepecate help formatter""" + def add_usage(self, usage, actions, groups, prefix=None): - helpers.deprecate_warn_cmd('info', 'iotlab-status', 19) + helpers.deprecate_warn_cmd("info", "iotlab-status", 19) return super().add_usage(usage, actions, groups, prefix) - help_msg = 'DEPRECATED: use iotlab-status command instead' - info_parser = subparsers.add_parser('info', help=help_msg, - formatter_class=DeprecateHelpFormatter) - - info_parser.add_argument('--site', - action='append', dest='info_selection', - type=lambda x: ('site', x), - help='nodes list filter by site') - info_parser.add_argument('--archi', - action='append', dest='info_selection', - type=lambda x: ('archi', x), - help='nodes list filter by architecture') - info_parser.add_argument('--state', action='append', dest='info_selection', - type=lambda x: ('state', x), - help='nodes list filter by state') + help_msg = "DEPRECATED: use iotlab-status command instead" + info_parser = subparsers.add_parser( + "info", help=help_msg, formatter_class=DeprecateHelpFormatter + ) + + info_parser.add_argument( + "--site", + action="append", + dest="info_selection", + type=lambda x: ("site", x), + help="nodes list filter by site", + ) + info_parser.add_argument( + "--archi", + action="append", + dest="info_selection", + type=lambda x: ("archi", x), + help="nodes list filter by architecture", + ) + info_parser.add_argument( + "--state", + action="append", + dest="info_selection", + type=lambda x: ("state", x), + help="nodes list filter by state", + ) # subcommand info_group = info_parser.add_mutually_exclusive_group(required=True) - info_group.add_argument('-l', '--list', dest='list_id', - action='store_false', help='nodes list') - info_group.add_argument('-li', '--list-id', dest='list_id', - action='store_true', - help=('nodes id list by archi and state ' - '(EXP_LIST format : 1-34+72)')) + info_group.add_argument( + "-l", "--list", dest="list_id", action="store_false", help="nodes list" + ) + info_group.add_argument( + "-li", + "--list-id", + dest="list_id", + action="store_true", + help=("nodes id list by archi and state (EXP_LIST format : 1-34+72)"), + ) # ####### WAIT PARSER ############### parser_add_wait_subparser(subparsers, expid_required=False) @@ -197,74 +280,115 @@ def add_usage(self, usage, actions, groups, prefix=None): def parser_add_submit_subparser(subparsers): """Add 'submit' subparser and return it.""" submit_parser = subparsers.add_parser( - 'submit', help='submit user experiment', - epilog=help_msgs.SUBMIT_EPILOG, formatter_class=RawTextHelpFormatter) + "submit", + help="submit user experiment", + epilog=help_msgs.SUBMIT_EPILOG, + formatter_class=RawTextHelpFormatter, + ) - submit_parser.add_argument('-p', '--print', - dest='print_json', action='store_true', - help='print experiment submission') + submit_parser.add_argument( + "-p", + "--print", + dest="print_json", + action="store_true", + help="print experiment submission", + ) # General experiment configuration - conf_parser = submit_parser.add_argument_group('experiment configuration') + conf_parser = submit_parser.add_argument_group("experiment configuration") - conf_parser.add_argument('-n', '--name', help='experiment name') + conf_parser.add_argument("-n", "--name", help="experiment name") _parser_add_duration_and_reservation(conf_parser, duration_required=True) # Resources - res_parser = submit_parser.add_argument_group('resource configuration') - res_parser.add_argument('-l', '--list', action='append', - dest='nodes_list', required=True, - type=exp_resources_from_str, - help="experiment list") - res_parser.add_argument('-s', '--site-association', action='append', - type=site_association_from_str, - help='sites associations') + res_parser = submit_parser.add_argument_group("resource configuration") + res_parser.add_argument( + "-l", + "--list", + action="append", + dest="nodes_list", + required=True, + type=exp_resources_from_str, + help="experiment list", + ) + res_parser.add_argument( + "-s", + "--site-association", + action="append", + type=site_association_from_str, + help="sites associations", + ) # Help messages for resources - help_parser = submit_parser.add_argument_group('advanced help options') - common.HelpAction.add_help(help_parser, '--help-list', - 'show help on --list option', - help_msgs.SUBMIT_LIST_HELP) - common.HelpAction.add_help(help_parser, '--help-site-association', - 'show help on --site-association option', - help_msgs.SUBMIT_SITE_ASSOC_HELP) + help_parser = submit_parser.add_argument_group("advanced help options") + common.HelpAction.add_help( + help_parser, + "--help-list", + "show help on --list option", + help_msgs.SUBMIT_LIST_HELP, + ) + common.HelpAction.add_help( + help_parser, + "--help-site-association", + "show help on --site-association option", + help_msgs.SUBMIT_SITE_ASSOC_HELP, + ) def _parser_add_duration_and_reservation( # pylint:disable=invalid-name - subparser, duration_required): + subparser, duration_required +): """Add a 'duration' and a 'reservation' argument to subparser. :param subparser: subparser instance :param duration_required: select if duration is required or optional """ - subparser.add_argument('-d', '--duration', required=duration_required, - type=int, help='experiment duration in minutes') + subparser.add_argument( + "-d", + "--duration", + required=duration_required, + type=int, + help="experiment duration in minutes", + ) - subparser.add_argument('-r', '--reservation', type=int, - help=('experiment schedule starting : seconds ' - 'since 1970-01-01 00:00:00 UTC')) + subparser.add_argument( + "-r", + "--reservation", + type=int, + help=("experiment schedule starting : seconds since 1970-01-01 00:00:00 UTC"), + ) def parser_add_wait_subparser(subparsers, expid_required=False): """Add wait experiment subparser and return it.""" wait_parser = subparsers.add_parser( - 'wait', help='wait user experiment started', - epilog=help_msgs.WAIT_EPILOG, formatter_class=RawTextHelpFormatter) + "wait", + help="wait user experiment started", + epilog=help_msgs.WAIT_EPILOG, + formatter_class=RawTextHelpFormatter, + ) common.add_expid_arg(wait_parser, required=expid_required) wait_parser.add_argument( - '--state', default='Running', - help="wait states `State1,State2` or Finished, default 'Running'") + "--state", + default="Running", + help="wait states `State1,State2` or Finished, default 'Running'", + ) wait_parser.add_argument( - '--step', default=5, type=int, - help="Wait time in seconds between each check") + "--step", default=5, type=int, help="Wait time in seconds between each check" + ) wait_parser.add_argument( - '--timeout', default=experiment.WAIT_TIMEOUT_DEFAULT, type=float, - help="Max time to wait in seconds") + "--timeout", + default=experiment.WAIT_TIMEOUT_DEFAULT, + type=float, + help="Max time to wait in seconds", + ) wait_parser.add_argument( - '--cancel-on-timeout', action='store_true', - help="Cancel experiment if timeout is reached") + "--cancel-on-timeout", + action="store_true", + help="Cancel experiment if timeout is reached", + ) return wait_parser @@ -272,35 +396,53 @@ def parser_add_wait_subparser(subparsers, expid_required=False): def parser_add_script_subparser(subparsers): """Add suparser for 'script'.""" _script_parser = subparsers.add_parser( - 'script', help='run script on sites frontends', - epilog=help_msgs.SCRIPT_EPILOG, formatter_class=RawTextHelpFormatter) + "script", + help="run script on sites frontends", + epilog=help_msgs.SCRIPT_EPILOG, + formatter_class=RawTextHelpFormatter, + ) common.add_expid_arg(_script_parser) script_group = _script_parser.add_argument_group("Command") script_group = script_group.add_mutually_exclusive_group(required=True) - script_group.add_argument('--run', type=run_site_association_from_str, - metavar=RUN_SITE_ASSOCIATION_METAVAR, - dest='run_script_site', nargs='+', - help="sites association with 'script'") - script_group.add_argument('--kill', type=common.site_with_domain_checked, - metavar='site', dest='kill_sites', - nargs='*', help='sites list') - script_group.add_argument('--status', type=common.site_with_domain_checked, - metavar='site', dest='status_sites', - nargs='*', help='sites list') + script_group.add_argument( + "--run", + type=run_site_association_from_str, + metavar=RUN_SITE_ASSOCIATION_METAVAR, + dest="run_script_site", + nargs="+", + help="sites association with 'script'", + ) + script_group.add_argument( + "--kill", + type=common.site_with_domain_checked, + metavar="site", + dest="kill_sites", + nargs="*", + help="sites list", + ) + script_group.add_argument( + "--status", + type=common.site_with_domain_checked, + metavar="site", + dest="status_sites", + nargs="*", + help="sites list", + ) return _script_parser def exp_infos_from_str(exp_str): """Extract nodes and associations.""" try: - params = exp_str.split(',') + params = exp_str.split(",") nodes, params = _extract_firmware_nodes_list(params) associations = _extract_associations(params) except ValueError as err: raise argparse.ArgumentTypeError( - f'Invalid arguments in experiment list {exp_str!r}: {err}') + f"Invalid arguments in experiment list {exp_str!r}: {err}" + ) return nodes, associations @@ -315,10 +457,9 @@ def exp_resources_from_str(exp_str): + rocquencourt,a8,1-5,,battery,firmware=a8.elf """ nodes, associations = exp_infos_from_str(exp_str) - firmware_path = associations.pop('firmware', None) - profile_name = associations.pop('profile', None) - return experiment.exp_resources(nodes, firmware_path, profile_name, - **associations) + firmware_path = associations.pop("firmware", None) + profile_name = associations.pop("profile", None) + return experiment.exp_resources(nodes, firmware_path, profile_name, **associations) def site_association_from_str(site_assoc_str): @@ -335,18 +476,17 @@ def site_association_from_str(site_assoc_str): """ # Decode keywoard associations try: - sites, kwassocs = _args_kwargs(site_assoc_str.split(',')) + sites, kwassocs = _args_kwargs(site_assoc_str.split(",")) # Validate site and add domain sites = [common.site_with_domain_checked(site) for site in sites] return experiment.site_association(*sites, **kwassocs) except ValueError as err: - raise argparse.ArgumentTypeError(f'Invalid site_association: {err}') + raise argparse.ArgumentTypeError(f"Invalid site_association: {err}") -RUN_SITE_ASSOCIATIONS_STR = ('script=script_path' - '[,scriptconfig=scriptconfig_path]') -RUN_SITE_ASSOCIATION_METAVAR = f'site,site,{RUN_SITE_ASSOCIATIONS_STR}' +RUN_SITE_ASSOCIATIONS_STR = "script=script_path[,scriptconfig=scriptconfig_path]" +RUN_SITE_ASSOCIATION_METAVAR = f"site,site,{RUN_SITE_ASSOCIATIONS_STR}" def _run_associations_arg_check(script, scriptconfig=None): @@ -381,9 +521,9 @@ def _valid_param(param): * no space * a name before '=' """ - if ' ' in param: - raise ValueError('no space allowed') - if param.startswith('='): + if " " in param: + raise ValueError("no space allowed") + if param.startswith("="): raise ValueError(f"name required for kwarg '{param}'") @@ -438,9 +578,9 @@ def _args_kwargs(params): for param in params: _valid_param(param) - if '=' in param: + if "=" in param: # Parsing kwargs - key, value = param.split('=', 1) + key, value = param.split("=", 1) _add_key_value(kwargs, key, value) else: # Parsing args @@ -451,13 +591,13 @@ def _args_kwargs(params): def _check_args_then_kwargs(params): """Check that args are first, and then kwargs only.""" - is_kwargs = ['=' in param for param in params] + is_kwargs = ["=" in param for param in params] # Should be many False then many True if is_kwargs != sorted(is_kwargs): - raise ValueError('got argument after keyword argument') + raise ValueError("got argument after keyword argument") -def _add_key_value(kwargs, key, value=''): +def _add_key_value(kwargs, key, value=""): """Add `key`,`value` if value is not empty. Raise an error if key exists. @@ -469,13 +609,13 @@ def _add_key_value(kwargs, key, value=''): kwargs[key] = value -def _submit_args_to_dict(firmware='', profile=''): +def _submit_args_to_dict(firmware="", profile=""): """Return kwargs for this arguments. Remove empty values""" kwargs = {} if firmware: - kwargs['firmware'] = firmware + kwargs["firmware"] = firmware if profile: - kwargs['profile'] = profile + kwargs["profile"] = profile return kwargs @@ -493,7 +633,7 @@ def _merge_assocs_args_d_kwargs(args_dict, kwargs): return merged -SUBMIT_ASSOC_ARGS_KWARGS = '[firmware][,profile][,assoc=value][,assoc=...]' +SUBMIT_ASSOC_ARGS_KWARGS = "[firmware][,profile][,assoc=value][,assoc=...]" def _extract_associations(params): @@ -507,8 +647,7 @@ def _extract_associations(params): args_dict = _submit_args_to_dict(*args) except TypeError: raise ValueError( - "Invalid positional arguments, " - f"should be {SUBMIT_ASSOC_ARGS_KWARGS}" + f"Invalid positional arguments, should be {SUBMIT_ASSOC_ARGS_KWARGS}" ) associations = _merge_assocs_args_d_kwargs(args_dict, kwargs) @@ -516,7 +655,7 @@ def _extract_associations(params): def get_alias_properties(properties_str): - """ Extract nodes selection properties from given properties_str + """Extract nodes selection properties from given properties_str >>> get_alias_properties("site=grenoble+archi=wsn430:cc1101+mobile=True") ('grenoble', 'wsn430:cc1101', 'True') @@ -538,8 +677,7 @@ def get_alias_properties(properties_str): try: properties = _alias_properties_from_kwargs(**properties_dict) except TypeError: - raise ValueError('Properties should be "archi", "site"' - ' and optional "mobile".') + raise ValueError('Properties should be "archi", "site" and optional "mobile".') # Forward check if there are more properties site, archi, mobile = properties @@ -569,14 +707,14 @@ def _properties_str_to_dict(properties_str): Traceback (most recent call last): ValueError: Invalid empty value for property "a" """ - properties = [p.split('=', 1) for p in properties_str.split('+')] + properties = [p.split("=", 1) for p in properties_str.split("+")] prop_dict = {} for key, value in properties: if key in prop_dict: raise ValueError(f'Property "{key}" should appear only once') - if value == '': + if value == "": raise ValueError(f'Invalid empty value for property "{key}"') prop_dict[key] = value @@ -606,7 +744,7 @@ def mobile_from_mobile_str(mobile_str=None): Traceback (most recent call last): ValueError: Invalid 'mobile' property: %r. Should be in 'true|false|0|1' """ - mobile_str = mobile_str or 'False' + mobile_str = mobile_str or "False" for convert_fct in (_mobile_str_true_false, _mobile_str_as_bool): try: @@ -614,14 +752,13 @@ def mobile_from_mobile_str(mobile_str=None): except (KeyError, ValueError): pass - raise ValueError("Invalid 'mobile' property: %r." - " Should be in 'true|false|0|1'") + raise ValueError("Invalid 'mobile' property: %r. Should be in 'true|false|0|1'") def _mobile_str_true_false(mobile_str): """Try checking for 'true', 'false' in any case.""" mobile_str = mobile_str.title() # upper first letter - return {'True': True, 'False': False}[mobile_str] + return {"True": True, "False": False}[mobile_str] def _mobile_str_as_bool(mobile_str): @@ -660,14 +797,19 @@ def _extract_firmware_nodes_list(param_list): def submit_experiment_parser(opts): - """ Parse namespace 'opts' and execute requested 'submit' command """ + """Parse namespace 'opts' and execute requested 'submit' command""" user, passwd = auth.get_user_credentials(opts.username, opts.password) api = rest.Api(user, passwd) - return experiment.submit_experiment(api, opts.name, opts.duration, - opts.nodes_list, opts.reservation, - opts.print_json, - opts.site_association) + return experiment.submit_experiment( + api, + opts.name, + opts.duration, + opts.nodes_list, + opts.reservation, + opts.print_json, + opts.site_association, + ) def script_parser(opts): @@ -684,22 +826,22 @@ def script_parser(opts): def _script_command_options(opts): """Extract `command` and `options` from argparse 'opts'.""" if opts.run_script_site is not None: - command = 'run' + command = "run" options = opts.run_script_site elif opts.kill_sites is not None: - command = 'kill' + command = "kill" options = opts.kill_sites elif opts.status_sites is not None: - command = 'status' + command = "status" options = opts.status_sites else: # pragma: no cover - raise ValueError('Unknown request') + raise ValueError("Unknown request") return command, options def stop_experiment_parser(opts): - """ Parse namespace 'opts' object and execute requested 'stop' command """ + """Parse namespace 'opts' object and execute requested 'stop' command""" user, passwd = auth.get_user_credentials(opts.username, opts.password) api = rest.Api(user, passwd) exp_id = helpers.get_current_experiment(api, opts.experiment_id) @@ -708,58 +850,55 @@ def stop_experiment_parser(opts): def get_experiment_parser(opts): - """ Parse namespace 'opts' object and execute requested 'get' command """ + """Parse namespace 'opts' object and execute requested 'get' command""" user, passwd = auth.get_user_credentials(opts.username, opts.password) api = rest.Api(user, passwd) # pylint:disable=no-else-return - if opts.get_cmd == 'experiment_list': - return experiment.get_experiments_list(api, opts.state, opts.limit, - opts.offset) - elif opts.get_cmd in ('start_date', 'state'): + if opts.get_cmd == "experiment_list": + return experiment.get_experiments_list(api, opts.state, opts.limit, opts.offset) + elif opts.get_cmd in ("start_date", "state"): return _get_experiment_attr(api, opts) - elif opts.get_cmd == 'experiments': - return experiment.get_active_experiments(api, - running_only=not opts.active) + elif opts.get_cmd == "experiments": + return experiment.get_active_experiments(api, running_only=not opts.active) else: exp_id = helpers.get_current_experiment(api, opts.experiment_id) - return experiment.get_experiment(api, exp_id, - _deprecate_cmd(opts.get_cmd)) + return experiment.get_experiment(api, exp_id, _deprecate_cmd(opts.get_cmd)) def _deprecate_cmd(cmd): - if cmd == 'resources': - new_cmd = 'nodes' + if cmd == "resources": + new_cmd = "nodes" helpers.deprecate_warn_cmd(cmd, new_cmd, 8) return new_cmd - if cmd == 'resources_ids': - helpers.deprecate_warn_cmd('resources-id', 'nodes-ids', 8) - return 'nodes_ids' + if cmd == "resources_ids": + helpers.deprecate_warn_cmd("resources-id", "nodes-ids", 8) + return "nodes_ids" return cmd def _get_experiment_attr(api, opts): - """ Return start_time or state experiment attribute with old api format""" - assert opts.get_cmd in ('state', 'start_date',) - exp_id = helpers.get_current_experiment(api, opts.experiment_id, - running_only=False) - ret = experiment.get_experiment(api, exp_id, '') - if opts.get_cmd == 'state': - helpers.deprecate_warn_cmd('exp-state', 'print', 8) + """Return start_time or state experiment attribute with old api format""" + assert opts.get_cmd in ( + "state", + "start_date", + ) + exp_id = helpers.get_current_experiment(api, opts.experiment_id, running_only=False) + ret = experiment.get_experiment(api, exp_id, "") + if opts.get_cmd == "state": + helpers.deprecate_warn_cmd("exp-state", "print", 8) return {opts.get_cmd: ret[opts.get_cmd]} - helpers.deprecate_warn_cmd('start-time', 'print', 8) + helpers.deprecate_warn_cmd("start-time", "print", 8) # start_date option - utc_date = datetime.strptime(ret[opts.get_cmd], - '%Y-%m-%dT%H:%M:%SZ') + utc_date = datetime.strptime(ret[opts.get_cmd], "%Y-%m-%dT%H:%M:%SZ") timestamp = (utc_date - datetime(1970, 1, 1)).total_seconds() - local_date = time.ctime(timestamp) if timestamp else 'Unknown' - return {'start_time': int(timestamp), - 'local_date': local_date} + local_date = time.ctime(timestamp) if timestamp else "Unknown" + return {"start_time": int(timestamp), "local_date": local_date} def load_experiment_parser(opts): - """ Parse namespace 'opts' object and execute requested 'load' command """ + """Parse namespace 'opts' object and execute requested 'load' command""" user, passwd = auth.get_user_credentials(opts.username, opts.password) api = rest.Api(user, passwd) @@ -771,51 +910,49 @@ def reload_experiment_parser(opts): """Parse namespace 'opts' object and execute requested 'reload' command.""" user, passwd = auth.get_user_credentials(opts.username, opts.password) api = rest.Api(user, passwd) - return experiment.reload_experiment(api, opts.experiment_id, - opts.duration, opts.reservation) + return experiment.reload_experiment( + api, opts.experiment_id, opts.duration, opts.reservation + ) def info_experiment_parser(opts): - """ Parse namespace 'opts' object and execute requested 'info' command """ + """Parse namespace 'opts' object and execute requested 'info' command""" user, passwd = auth.get_user_credentials(opts.username, opts.password) api = rest.Api(user, passwd) - helpers.deprecate_warn_cmd('info', 'iotlab-status', 7) + helpers.deprecate_warn_cmd("info", "iotlab-status", 7) selection = dict(opts.info_selection or ()) return experiment.info_experiment(api, opts.list_id, **selection) def wait_experiment_parser(opts): - """ Parse namespace 'opts' object and execute requested 'wait' command """ + """Parse namespace 'opts' object and execute requested 'wait' command""" user, passwd = auth.get_user_credentials(opts.username, opts.password) api = rest.Api(user, passwd) - exp_id = helpers.get_current_experiment( - api, opts.experiment_id, running_only=False) + exp_id = helpers.get_current_experiment(api, opts.experiment_id, running_only=False) - sys.stderr.write( - f"Waiting that experiment {exp_id} gets in state {opts.state}\n" - ) + sys.stderr.write(f"Waiting that experiment {exp_id} gets in state {opts.state}\n") - return experiment.wait_experiment(api, exp_id, opts.state, - opts.step, opts.timeout, - opts.cancel_on_timeout) + return experiment.wait_experiment( + api, exp_id, opts.state, opts.step, opts.timeout, opts.cancel_on_timeout + ) def experiment_parse_and_run(opts): - """ Parse namespace 'opts' object and execute requested command + """Parse namespace 'opts' object and execute requested command Return result object """ command = { - 'submit': submit_experiment_parser, - 'script': script_parser, - 'stop': stop_experiment_parser, - 'get': get_experiment_parser, - 'load': load_experiment_parser, - 'reload': reload_experiment_parser, - 'info': info_experiment_parser, - 'wait': wait_experiment_parser, + "submit": submit_experiment_parser, + "script": script_parser, + "stop": stop_experiment_parser, + "get": get_experiment_parser, + "load": load_experiment_parser, + "reload": reload_experiment_parser, + "info": info_experiment_parser, + "wait": wait_experiment_parser, }[opts.command] return command(opts) diff --git a/iotlabcli/parser/help_msgs.py b/iotlabcli/parser/help_msgs.py index a61d0dd..3d48022 100644 --- a/iotlabcli/parser/help_msgs.py +++ b/iotlabcli/parser/help_msgs.py @@ -24,7 +24,7 @@ import os CUR_DIR = os.path.dirname(__file__) -HELP_DIR = os.path.join(CUR_DIR, 'help') +HELP_DIR = os.path.join(CUR_DIR, "help") def _read_help_file(name): @@ -67,8 +67,8 @@ def _read_help_file(name): + 5,archi=m3:at86rf231+site=strasbourg,stras.elf """ -SUBMIT_LIST_HELP = _read_help_file('submit_list_help.md') -SUBMIT_SITE_ASSOC_HELP = _read_help_file('submit_site_association_help.md') +SUBMIT_LIST_HELP = _read_help_file("submit_list_help.md") +SUBMIT_SITE_ASSOC_HELP = _read_help_file("submit_site_association_help.md") SCRIPT_EPILOG = """ Examples: diff --git a/iotlabcli/parser/main.py b/iotlabcli/parser/main.py index 6f72e6e..00f4473 100644 --- a/iotlabcli/parser/main.py +++ b/iotlabcli/parser/main.py @@ -57,16 +57,15 @@ def parse_subcommands(commands, args): - """ common function to parse `iotlab` or other with subcommands """ + """common function to parse `iotlab` or other with subcommands""" parser = ArgumentParser() - commands['help'] = lambda args: parser.print_help() - parser.add_argument('command', nargs='?', - choices=commands.keys(), default='help') + commands["help"] = lambda args: parser.print_help() + parser.add_argument("command", nargs="?", choices=commands.keys(), default="help") opts, _ = parser.parse_known_args(args[:1]) - sys.argv[0] = f'iotlab {opts.command}' + sys.argv[0] = f"iotlab {opts.command}" return commands[opts.command](args[1:]) @@ -74,9 +73,9 @@ def oml_plot(args): """'iotlab oml-plot' main function.""" commands = { - 'consum': oml_plot_tools.consum.main, - 'radio': oml_plot_tools.radio.main, - 'traj': oml_plot_tools.traj.main + "consum": oml_plot_tools.consum.main, + "radio": oml_plot_tools.radio.main, + "traj": oml_plot_tools.traj.main, } parse_subcommands(commands, args) @@ -86,19 +85,19 @@ def main(args=None): args = args or sys.argv[1:] commands = { - 'auth': iotlabcli.parser.auth.main, - 'experiment': iotlabcli.parser.experiment.main, - 'node': iotlabcli.parser.node.main, - 'profile': iotlabcli.parser.profile.main, - 'robot': iotlabcli.parser.robot.main, - 'status': iotlabcli.parser.status.main + "auth": iotlabcli.parser.auth.main, + "experiment": iotlabcli.parser.experiment.main, + "node": iotlabcli.parser.node.main, + "profile": iotlabcli.parser.profile.main, + "robot": iotlabcli.parser.robot.main, + "status": iotlabcli.parser.status.main, } if iotlabaggregator: - commands['serial'] = iotlabaggregator.serial.main - commands['sniffer'] = iotlabaggregator.sniffer.main + commands["serial"] = iotlabaggregator.serial.main + commands["sniffer"] = iotlabaggregator.sniffer.main if oml_plot_tools: - commands['plot'] = oml_plot + commands["plot"] = oml_plot if iotlabsshcli: - commands['ssh'] = iotlabsshcli.parser.open_linux_parser.main + commands["ssh"] = iotlabsshcli.parser.open_linux_parser.main return parse_subcommands(commands, args) diff --git a/iotlabcli/parser/node.py b/iotlabcli/parser/node.py index 645f468..3785b63 100644 --- a/iotlabcli/parser/node.py +++ b/iotlabcli/parser/node.py @@ -21,16 +21,13 @@ """Node parser""" -import sys import argparse +import sys from argparse import RawTextHelpFormatter -from iotlabcli import rest -from iotlabcli import helpers -from iotlabcli import auth import iotlabcli.node -from iotlabcli.parser import help_msgs -from iotlabcli.parser import common +from iotlabcli import auth, helpers, rest +from iotlabcli.parser import common, help_msgs NODE_PARSER = """ @@ -58,15 +55,17 @@ def parse_options(): - """ Handle iotlab-node command-line options with argparse """ + """Handle iotlab-node command-line options with argparse""" parent_parser = common.base_parser() # We create top level parser parser = argparse.ArgumentParser( description=NODE_PARSER, - parents=[parent_parser], formatter_class=RawTextHelpFormatter, - epilog=(help_msgs.PARSER_EPILOG.format(cli='node', option='--flash') + - NODE_EPILOG), + parents=[parent_parser], + formatter_class=RawTextHelpFormatter, + epilog=( + help_msgs.PARSER_EPILOG.format(cli="node", option="--flash") + NODE_EPILOG + ), ) common.add_expid_arg(parser) @@ -74,49 +73,97 @@ def parse_options(): # command # argument with parameter can't both set 'command' and set argument value # so save argument, and command will be left to 'with_arguments' - parser.set_defaults(command='with_argument') + parser.set_defaults(command="with_argument") cmd_group = parser.add_mutually_exclusive_group(required=True) cmd_group.add_argument( - '-sta', '--start', help='start command', const='start', - dest='command', action='store_const') + "-sta", + "--start", + help="start command", + const="start", + dest="command", + action="store_const", + ) cmd_group.add_argument( - '-sto', '--stop', help='stop command', const='stop', - dest='command', action='store_const') + "-sto", + "--stop", + help="stop command", + const="stop", + dest="command", + action="store_const", + ) cmd_group.add_argument( - '-r', '--reset', help='reset command', const='reset', - dest='command', action='store_const') - cmd_group.add_argument('-fl', '--flash', - dest='firmware_path', default=None, - help='flash firmware command with path file') + "-r", + "--reset", + help="reset command", + const="reset", + dest="command", + action="store_const", + ) + cmd_group.add_argument( + "-fl", + "--flash", + dest="firmware_path", + default=None, + help="flash firmware command with path file", + ) cmd_group.add_argument( - '--flash-idle', help='flash idle firmware', const='flash-idle', - dest='command', action='store_const') + "--flash-idle", + help="flash idle firmware", + const="flash-idle", + dest="command", + action="store_const", + ) cmd_group.add_argument( - '--debug-start', help='start debugger', const='debug-start', - dest='command', action='store_const') + "--debug-start", + help="start debugger", + const="debug-start", + dest="command", + action="store_const", + ) cmd_group.add_argument( - '--debug-stop', help='stop debugger', const='debug-stop', - dest='command', action='store_const') - cmd_group.add_argument('--profile', '--update-profile', - dest='profile_name', default=None, - help='change nodes current monitoring profile') - cmd_group.add_argument('--profile-load', - dest='profile_path', default=None, - help=('change nodes current monitoring profile' - ' with provided JSON')) - cmd_group.add_argument('--profile-reset', - dest='command', const='profile-reset', - action='store_const', - help='reset to default no monitoring profile') + "--debug-stop", + help="stop debugger", + const="debug-stop", + dest="command", + action="store_const", + ) cmd_group.add_argument( - '--update-idle', help='DEPRECATED: use --flash-idle', - const='update-idle', dest='command', action='store_const') - cmd_group.add_argument('-up', '--update', - dest='up_firmware_path', default=None, - help='DEPRECATED: use -fl or --flash option') + "--profile", + "--update-profile", + dest="profile_name", + default=None, + help="change nodes current monitoring profile", + ) + cmd_group.add_argument( + "--profile-load", + dest="profile_path", + default=None, + help=("change nodes current monitoring profile with provided JSON"), + ) + cmd_group.add_argument( + "--profile-reset", + dest="command", + const="profile-reset", + action="store_const", + help="reset to default no monitoring profile", + ) + cmd_group.add_argument( + "--update-idle", + help="DEPRECATED: use --flash-idle", + const="update-idle", + dest="command", + action="store_const", + ) + cmd_group.add_argument( + "-up", + "--update", + dest="up_firmware_path", + default=None, + help="DEPRECATED: use -fl or --flash option", + ) # nodes list or exclude list common.add_nodes_selection_list(parser) @@ -124,32 +171,31 @@ def parse_options(): def node_parse_and_run(opts): - """ Parse namespace 'opts' object and execute requested command """ + """Parse namespace 'opts' object and execute requested command""" user, passwd = auth.get_user_credentials(opts.username, opts.password) api = rest.Api(user, passwd) exp_id = helpers.get_current_experiment(api, opts.experiment_id) _deprecate_cmd(opts) - if opts.command == 'with_argument': + if opts.command == "with_argument": command, cmd_opt = _node_parse_command_and_opt(**vars(opts)) else: # opts.command has a real value command, cmd_opt = (opts.command, None) - nodes = common.list_nodes(api, exp_id, opts.nodes_list, - opts.exclude_nodes_list) + nodes = common.list_nodes(api, exp_id, opts.nodes_list, opts.exclude_nodes_list) return iotlabcli.node.node_command(api, command, exp_id, nodes, cmd_opt) def _deprecate_cmd(opts): - if opts.command == 'update-idle': - new_cmd = 'flash-idle' + if opts.command == "update-idle": + new_cmd = "flash-idle" helpers.deprecate_warn_cmd(opts.command, new_cmd, 7) opts.command = new_cmd if opts.up_firmware_path: - helpers.deprecate_warn_cmd('update', 'flash', 7) + helpers.deprecate_warn_cmd("update", "flash", 7) opts.firmware_path = opts.up_firmware_path @@ -188,9 +234,9 @@ def _node_parse_command_and_opt(**opts_dict): """ # Mapping between 'command' and argparse option name commands_arguments = { - 'flash': 'firmware_path', - 'profile': 'profile_name', - 'profile-load': 'profile_path', + "flash": "firmware_path", + "profile": "profile_name", + "profile-load": "profile_path", } for command, argname in commands_arguments.items(): @@ -198,11 +244,11 @@ def _node_parse_command_and_opt(**opts_dict): if cmd_opt is not None: return command, cmd_opt - raise ValueError('Unknown command') + raise ValueError("Unknown command") def main(args=None): - """ Main command-line execution loop." """ + """Main command-line execution loop." """ args = args or sys.argv[1:] parser = parse_options() common.main_cli(node_parse_and_run, parser, args) diff --git a/iotlabcli/parser/profile.py b/iotlabcli/parser/profile.py index d1f9666..19b41e5 100644 --- a/iotlabcli/parser/profile.py +++ b/iotlabcli/parser/profile.py @@ -19,20 +19,16 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Profile parser""" +"""Profile parser""" +import argparse import json import sys -import argparse from argparse import RawTextHelpFormatter -from iotlabcli import helpers -from iotlabcli import rest -from iotlabcli import auth -from iotlabcli.parser import help_msgs -from iotlabcli.parser import common -from iotlabcli.profile import ProfileWSN430, ProfileM3, ProfileA8 -from iotlabcli.profile import ProfileCustom +from iotlabcli import auth, helpers, rest +from iotlabcli.parser import common, help_msgs +from iotlabcli.profile import ProfileA8, ProfileCustom, ProfileM3, ProfileWSN430 PROFILE_PARSER = """ @@ -45,240 +41,311 @@ def parse_options(): - """ Handle iotlab-profile command-line opts with argparse """ + """Handle iotlab-profile command-line opts with argparse""" parent_parser = common.base_parser() # We create top level parser parser = argparse.ArgumentParser( description=PROFILE_PARSER, - parents=[parent_parser], epilog=help_msgs.PARSER_EPILOG.format( - cli='profile', option='add'), - formatter_class=RawTextHelpFormatter) + parents=[parent_parser], + epilog=help_msgs.PARSER_EPILOG.format(cli="profile", option="add"), + formatter_class=RawTextHelpFormatter, + ) - subparsers = parser.add_subparsers(dest='command') + subparsers = parser.add_subparsers(dest="command") subparsers.required = True # not required by default in Python3 add_wsn430_parser = subparsers.add_parser( - 'addwsn430', help='add wsn430 user profile', + "addwsn430", + help="add wsn430 user profile", epilog=help_msgs.ADD_EPILOG_WSN430, - formatter_class=RawTextHelpFormatter) + formatter_class=RawTextHelpFormatter, + ) # # m3 profile # add_m3_parser = subparsers.add_parser( - 'addm3', help='add m3 user profile', - epilog=help_msgs.ADD_EPILOG_M3_A8.format(cmd='addm3', archi='m3'), - formatter_class=RawTextHelpFormatter) - add_m3_a8_parser('M3', add_m3_parser) + "addm3", + help="add m3 user profile", + epilog=help_msgs.ADD_EPILOG_M3_A8.format(cmd="addm3", archi="m3"), + formatter_class=RawTextHelpFormatter, + ) + add_m3_a8_parser("M3", add_m3_parser) # # a8 profile # add_a8_parser = subparsers.add_parser( - 'adda8', help='add a8 user profile', - epilog=help_msgs.ADD_EPILOG_M3_A8.format(cmd='adda8', archi='a8'), - formatter_class=RawTextHelpFormatter) - add_m3_a8_parser('A8', add_a8_parser) + "adda8", + help="add a8 user profile", + epilog=help_msgs.ADD_EPILOG_M3_A8.format(cmd="adda8", archi="a8"), + formatter_class=RawTextHelpFormatter, + ) + add_m3_a8_parser("A8", add_a8_parser) add_custom_parser = subparsers.add_parser( - 'addcustom', help='add custom user profile', - epilog=help_msgs.ADD_EPILOG_M3_A8.format(cmd='addcustom', - archi='custom'), - formatter_class=RawTextHelpFormatter) - add_m3_a8_parser('CUSTOM', add_custom_parser) + "addcustom", + help="add custom user profile", + epilog=help_msgs.ADD_EPILOG_M3_A8.format(cmd="addcustom", archi="custom"), + formatter_class=RawTextHelpFormatter, + ) + add_m3_a8_parser("CUSTOM", add_custom_parser) - del_parser = subparsers.add_parser('del', help='delete user profile') - get_parser = subparsers.add_parser('get', help='get user\'s profile') - load_parser = subparsers.add_parser('load', help='load user profile') + del_parser = subparsers.add_parser("del", help="delete user profile") + get_parser = subparsers.add_parser("get", help="get user's profile") + load_parser = subparsers.add_parser("load", help="load user profile") # # WSN430 profile # - add_wsn430_parser.add_argument('-n', '--name', required=True, - help='profile name') + add_wsn430_parser.add_argument("-n", "--name", required=True, help="profile name") add_wsn430_parser.add_argument( - '-j', '--json', action='store_true', - help='print profile JSON representation without add it') + "-j", + "--json", + action="store_true", + help="print profile JSON representation without add it", + ) add_wsn430_parser.add_argument( - '-p', '--power', - dest='power_mode', default='dc', - help='power mode (dc by default)', - choices=ProfileWSN430.choices['power_mode']) + "-p", + "--power", + dest="power_mode", + default="dc", + help="power mode (dc by default)", + choices=ProfileWSN430.choices["power_mode"], + ) # WSN430 Consumption group_wsn430_consumption = add_wsn430_parser.add_argument_group( - 'Consumption measure') + "Consumption measure" + ) group_wsn430_consumption.add_argument( - '-cfreq', dest='cfreq', type=int, - choices=ProfileWSN430.choices['consumption']['frequency'], - help='frequency measure (ms)') + "-cfreq", + dest="cfreq", + type=int, + choices=ProfileWSN430.choices["consumption"]["frequency"], + help="frequency measure (ms)", + ) group_wsn430_consumption.add_argument( - '-power', action='store_true', - help='power measure') + "-power", action="store_true", help="power measure" + ) group_wsn430_consumption.add_argument( - '-voltage', action='store_true', - help='voltage measure') + "-voltage", action="store_true", help="voltage measure" + ) group_wsn430_consumption.add_argument( - '-current', action='store_true', - help='current measure') + "-current", action="store_true", help="current measure" + ) # WSN430 Radio - group_wsn430_radio = add_wsn430_parser.add_argument_group( - 'Radio measure') + group_wsn430_radio = add_wsn430_parser.add_argument_group("Radio measure") group_wsn430_radio.add_argument( - '-rfreq', dest='rfreq', type=int, - choices=ProfileWSN430.choices['radio']['frequency'], - help='frequency measure (ms)') + "-rfreq", + dest="rfreq", + type=int, + choices=ProfileWSN430.choices["radio"]["frequency"], + help="frequency measure (ms)", + ) # WSN430 Sensor - group_wsn430_sensor = add_wsn430_parser.add_argument_group( - 'Sensor measure') + group_wsn430_sensor = add_wsn430_parser.add_argument_group("Sensor measure") group_wsn430_sensor.add_argument( - '-sfreq', dest='sfreq', type=int, - choices=ProfileWSN430.choices['sensor']['frequency'], - help='frequency measure (ms)') + "-sfreq", + dest="sfreq", + type=int, + choices=ProfileWSN430.choices["sensor"]["frequency"], + help="frequency measure (ms)", + ) group_wsn430_sensor.add_argument( - '-temperature', action='store_true', - help='temperature measure') + "-temperature", action="store_true", help="temperature measure" + ) group_wsn430_sensor.add_argument( - '-luminosity', action='store_true', - help='luminosity measure') + "-luminosity", action="store_true", help="luminosity measure" + ) # Delete Profile - del_parser.add_argument('-n', '--name', required=True, help='profile name') + del_parser.add_argument("-n", "--name", required=True, help="profile name") # Get Profile get_group = get_parser.add_mutually_exclusive_group(required=True) - get_group.add_argument('-n', '--name', help='profile name') + get_group.add_argument("-n", "--name", help="profile name") get_group.add_argument( - '-l', '--list', action='store_true', - help='print profile\'s list JSON representation') + "-l", + "--list", + action="store_true", + help="print profile's list JSON representation", + ) - get_parser.add_argument('--archi', help='only list profiles for archi') + get_parser.add_argument("--archi", help="only list profiles for archi") # Load Profile load_parser.add_argument( - '-f', '--file', dest='path_file', required=True, - help='profile JSON representation path file') + "-f", + "--file", + dest="path_file", + required=True, + help="profile JSON representation path file", + ) return parser def add_m3_a8_parser(node_type, subparser): - """ Add options for m3 and a8 parsers as they are the same """ - node_class = { - 'M3': ProfileM3, 'A8': ProfileA8, 'CUSTOM': ProfileCustom - }[node_type] + """Add options for m3 and a8 parsers as they are the same""" + node_class = {"M3": ProfileM3, "A8": ProfileA8, "CUSTOM": ProfileCustom}[node_type] - subparser.add_argument('-n', '--name', dest='name', required=True, - help='profile name') + subparser.add_argument( + "-n", "--name", dest="name", required=True, help="profile name" + ) - subparser.add_argument('-p', '--power', dest='power_mode', default='dc', - help='power mode (dc by default)', - choices=node_class.choices['power_mode']) + subparser.add_argument( + "-p", + "--power", + dest="power_mode", + default="dc", + help="power mode (dc by default)", + choices=node_class.choices["power_mode"], + ) subparser.add_argument( - '-j', '--json', dest='json', - action='store_true', - help='print profile JSON representation without add it') + "-j", + "--json", + dest="json", + action="store_true", + help="print profile JSON representation without add it", + ) # Radio configuration - consumption = subparser.add_argument_group('Consumption measure') + consumption = subparser.add_argument_group("Consumption measure") - consumption.add_argument( - '-current', action='store_true', help='current measure') + consumption.add_argument("-current", action="store_true", help="current measure") - consumption.add_argument( - '-voltage', action='store_true', help='voltage measure') + consumption.add_argument("-voltage", action="store_true", help="voltage measure") - consumption.add_argument( - '-power', action='store_true', help='power measure') + consumption.add_argument("-power", action="store_true", help="power measure") consumption.add_argument( - '-period', dest='period', type=int, - choices=node_class.choices['consumption']['period'], - help='period measure (us)') + "-period", + dest="period", + type=int, + choices=node_class.choices["consumption"]["period"], + help="period measure (us)", + ) consumption.add_argument( - '-avg', dest='average', type=int, - choices=node_class.choices['consumption']['average'], - help='average measure') + "-avg", + dest="average", + type=int, + choices=node_class.choices["consumption"]["average"], + help="average measure", + ) # Radio configuration - radio = subparser.add_argument_group('Radio measure mode') + radio = subparser.add_argument_group("Radio measure mode") radio.add_argument( - '-rssi', dest='mode', action='store_const', const='rssi', - help='RSSI measure. Configure with `channels`, `num` and `rperiod`') + "-rssi", + dest="mode", + action="store_const", + const="rssi", + help="RSSI measure. Configure with `channels`, `num` and `rperiod`", + ) radio.add_argument( - '-sniffer', dest='mode', action='store_const', const='sniffer', - help='Radio Sniffer. Configure one channel with `channels`.') + "-sniffer", + dest="mode", + action="store_const", + const="sniffer", + help="Radio Sniffer. Configure one channel with `channels`.", + ) - radio_cfg = subparser.add_argument_group('Radio measure config') + radio_cfg = subparser.add_argument_group("Radio measure config") radio_cfg.add_argument( - '-channels', dest='channels', nargs='+', - type=int, choices=node_class.choices['radio']['channels'], - metavar='{11..26}', help='List of channels (11 to 26)') + "-channels", + dest="channels", + nargs="+", + type=int, + choices=node_class.choices["radio"]["channels"], + metavar="{11..26}", + help="List of channels (11 to 26)", + ) radio_cfg.add_argument( - '-num', dest='num_per_channel', type=int, metavar='{0..255}', - choices=node_class.choices['radio']['num_per_channel'], - help='Number of measure by channel, if multiple channels supplied') + "-num", + dest="num_per_channel", + type=int, + metavar="{0..255}", + choices=node_class.choices["radio"]["num_per_channel"], + help="Number of measure by channel, if multiple channels supplied", + ) radio_cfg.add_argument( - '-rperiod', dest='rperiod', type=int, - choices=node_class.choices['radio']['period'], - metavar='{1..65535}', help='period measure') + "-rperiod", + dest="rperiod", + type=int, + choices=node_class.choices["radio"]["period"], + metavar="{1..65535}", + help="period measure", + ) def _wsn430_profile(opts): - """ Create a wsn430 profile from namespace object """ + """Create a wsn430 profile from namespace object""" profile = ProfileWSN430(profilename=opts.name, power=opts.power_mode) - profile.set_consumption(frequency=opts.cfreq, power=opts.power, - voltage=opts.voltage, current=opts.current) + profile.set_consumption( + frequency=opts.cfreq, + power=opts.power, + voltage=opts.voltage, + current=opts.current, + ) profile.set_radio(frequency=opts.rfreq) - profile.set_sensors(frequency=opts.sfreq, temperature=opts.temperature, - luminosity=opts.luminosity) + profile.set_sensors( + frequency=opts.sfreq, temperature=opts.temperature, luminosity=opts.luminosity + ) return profile def _m3_a8_profile(opts, node_class): - """ Create a node_class profile from namespace object """ + """Create a node_class profile from namespace object""" profile = node_class(profilename=opts.name, power=opts.power_mode) - profile.set_consumption(period=opts.period, average=opts.average, - power=opts.power, voltage=opts.voltage, - current=opts.current) - profile.set_radio(mode=opts.mode, channels=opts.channels, - period=opts.rperiod, - num_per_channel=opts.num_per_channel) + profile.set_consumption( + period=opts.period, + average=opts.average, + power=opts.power, + voltage=opts.voltage, + current=opts.current, + ) + profile.set_radio( + mode=opts.mode, + channels=opts.channels, + period=opts.rperiod, + num_per_channel=opts.num_per_channel, + ) return profile def _m3_profile(opts): - """ Create a m3 profile from namespace object """ + """Create a m3 profile from namespace object""" return _m3_a8_profile(opts, ProfileM3) def _a8_profile(opts): - """ Create a a8 profile from namespace object """ + """Create a a8 profile from namespace object""" return _m3_a8_profile(opts, ProfileA8) def _custom_profile(opts): - """ Create a a8 profile from namespace object """ + """Create a a8 profile from namespace object""" return _m3_a8_profile(opts, ProfileCustom) def _add_profile(api, profile, json_out=False): - """ Add user profile. if json, dump json dict to stdout """ + """Add user profile. if json, dump json dict to stdout""" if json_out: return profile @@ -286,28 +353,28 @@ def _add_profile(api, profile, json_out=False): def add_profile_parser(api, opts): - """ Add user profile with JSON Encoder serialization object Profile. + """Add user profile with JSON Encoder serialization object Profile. :param api: API Rest api object :param opts: command-line parser opts :type opts: Namespace object with opts attribute """ profile_func_d = { - 'addwsn430': _wsn430_profile, - 'addm3': _m3_profile, - 'adda8': _a8_profile, - 'addcustom': _custom_profile + "addwsn430": _wsn430_profile, + "addm3": _m3_profile, + "adda8": _a8_profile, + "addcustom": _custom_profile, } try: profile = profile_func_d[opts.command](opts) return _add_profile(api, profile, opts.json) - except AssertionError as err: # pragma: no cover + except AssertionError as err: # pragma: no cover raise ValueError(str(err)) def load_profile_parser(api, opts): - """ Load and add user profile description + """Load and add user profile description :param api: API Rest api object :param opts: command-line parser opts @@ -318,7 +385,7 @@ def load_profile_parser(api, opts): def del_profile_parser(api, opts): - """ Delete user profile description + """Delete user profile description :param api: API Rest api object :param opts: command-line parser opts @@ -328,7 +395,7 @@ def del_profile_parser(api, opts): def get_profile_parser(api, opts): - """ Get user profile description + """Get user profile description _ print JSONObject profile description _ print JSONObject profile's list description @@ -347,25 +414,25 @@ def get_profile_parser(api, opts): def profile_parse_and_run(opts): - """ Parse namespace 'opts' object and execute requested command """ + """Parse namespace 'opts' object and execute requested command""" user, passwd = auth.get_user_credentials(opts.username, opts.password) api = rest.Api(user, passwd) fct_parser = { - 'addwsn430': add_profile_parser, - 'addm3': add_profile_parser, - 'adda8': add_profile_parser, - 'addcustom': add_profile_parser, - 'load': load_profile_parser, - 'get': get_profile_parser, - 'del': del_profile_parser, + "addwsn430": add_profile_parser, + "addm3": add_profile_parser, + "adda8": add_profile_parser, + "addcustom": add_profile_parser, + "load": load_profile_parser, + "get": get_profile_parser, + "del": del_profile_parser, }[opts.command] return fct_parser(api, opts) def main(args=None): - """ Main command-line execution loop." """ + """Main command-line execution loop." """ args = args or sys.argv[1:] parser = parse_options() common.main_cli(profile_parse_and_run, parser, args) diff --git a/iotlabcli/parser/robot.py b/iotlabcli/parser/robot.py index 19e865f..fb4324c 100644 --- a/iotlabcli/parser/robot.py +++ b/iotlabcli/parser/robot.py @@ -20,96 +20,99 @@ # knowledge of the CeCILL license and that you accept its terms. """Robot parser""" -import sys + import argparse +import sys -from iotlabcli import rest -from iotlabcli import auth -from iotlabcli import helpers -from iotlabcli.parser import common import iotlabcli.robot +from iotlabcli import auth, helpers, rest +from iotlabcli.parser import common ROBOT_PARSER = """iotlab-robot manages interaction with nodes \ on a turtlebot.""" def parse_options(): - """ Handle iotlab-robot command-line options with argparse """ + """Handle iotlab-robot command-line options with argparse""" parent_parser = common.base_parser() # We create top level parser parser = argparse.ArgumentParser(parents=[parent_parser]) - subparsers = parser.add_subparsers(dest='command') + subparsers = parser.add_subparsers(dest="command") subparsers.required = True # not required by default in Python3 # Robot Commands # 'status' command - status_parser = subparsers.add_parser('status', help='Get robot status') + status_parser = subparsers.add_parser("status", help="Get robot status") common.add_nodes_selection_list(status_parser) common.add_expid_arg(status_parser) # 'update' command - up_parser = subparsers.add_parser('update', help='Update robot mobility') - up_parser.add_argument('-n', '--name', dest='up_name', required=True, - help='Update robot mobility') + up_parser = subparsers.add_parser("update", help="Update robot mobility") + up_parser.add_argument( + "-n", "--name", dest="up_name", required=True, help="Update robot mobility" + ) common.add_nodes_selection_list(up_parser) common.add_expid_arg(up_parser) # Mobility commands # 'get' command - get_parser = subparsers.add_parser('get', - help='Get robot mobilities') + get_parser = subparsers.add_parser("get", help="Get robot mobilities") get_group = get_parser.add_mutually_exclusive_group(required=True) - get_group.add_argument('-l', '--list', dest='list', - action='store_true', - help='Get circuits list') - get_group.add_argument('-n', '--name', dest='get_name', - help='Get circuit') - get_parser.add_argument('--site', action='append', dest='get_selection', - type=lambda x: ('site', x), - help='filter list by site') - get_parser.add_argument('--type', action='append', dest='get_selection', - type=lambda x: ('type', x), - help='filter list by circuit type') + get_group.add_argument( + "-l", "--list", dest="list", action="store_true", help="Get circuits list" + ) + get_group.add_argument("-n", "--name", dest="get_name", help="Get circuit") + get_parser.add_argument( + "--site", + action="append", + dest="get_selection", + type=lambda x: ("site", x), + help="filter list by site", + ) + get_parser.add_argument( + "--type", + action="append", + dest="get_selection", + type=lambda x: ("type", x), + help="filter list by circuit type", + ) return parser def robot_parse_and_run(opts): # noqa # Too complex but straightforward - """ Parse namespace 'opts' object and execute requested command """ + """Parse namespace 'opts' object and execute requested command""" user, passwd = auth.get_user_credentials(opts.username, opts.password) api = rest.Api(user, passwd) command = opts.command - if command == 'status': + if command == "status": exp_id = helpers.get_current_experiment(api, opts.experiment_id) - nodes = common.list_nodes(api, exp_id, opts.nodes_list, - opts.exclude_nodes_list) - ret = iotlabcli.robot.robot_command(api, 'status', exp_id, nodes) - elif command == 'update': + nodes = common.list_nodes(api, exp_id, opts.nodes_list, opts.exclude_nodes_list) + ret = iotlabcli.robot.robot_command(api, "status", exp_id, nodes) + elif command == "update": exp_id = helpers.get_current_experiment(api, opts.experiment_id) - nodes = common.list_nodes(api, exp_id, opts.nodes_list, - opts.exclude_nodes_list) - ret = iotlabcli.robot.robot_update_mobility(api, exp_id, - opts.up_name, nodes) - elif command == 'get' and opts.list: + nodes = common.list_nodes(api, exp_id, opts.nodes_list, opts.exclude_nodes_list) + ret = iotlabcli.robot.robot_update_mobility(api, exp_id, opts.up_name, nodes) + elif command == "get" and opts.list: selection = dict(opts.get_selection or ()) - ret = iotlabcli.robot.circuit_command(api, 'list', **selection) - elif command == 'get' and opts.get_name is not None: - ret = iotlabcli.robot.circuit_command(api, 'get', opts.get_name) + ret = iotlabcli.robot.circuit_command(api, "list", **selection) + elif command == "get" and opts.get_name is not None: + ret = iotlabcli.robot.circuit_command(api, "get", opts.get_name) else: # pragma: no cover - raise ValueError('Unknown command') + raise ValueError("Unknown command") return ret def main(args=None): - """ Main command-line execution loop." """ + """Main command-line execution loop." """ args = args or sys.argv[1:] parser = parse_options() common.main_cli(robot_parse_and_run, parser, args) diff --git a/iotlabcli/parser/status.py b/iotlabcli/parser/status.py index 7c4feae..332d115 100644 --- a/iotlabcli/parser/status.py +++ b/iotlabcli/parser/status.py @@ -21,15 +21,13 @@ """Status parser""" -import sys import argparse +import sys from argparse import RawTextHelpFormatter -from iotlabcli import rest -from iotlabcli import auth import iotlabcli.status -from iotlabcli.parser import help_msgs -from iotlabcli.parser import common +from iotlabcli import auth, rest +from iotlabcli.parser import common, help_msgs STATUS_PARSER = """ @@ -54,55 +52,84 @@ def parse_options(): - """ Handle iotlab-status command-line options with argparse """ + """Handle iotlab-status command-line options with argparse""" parent_parser = common.base_parser() # We create top level parser - epilog_msg = help_msgs.PARSER_EPILOG.format(cli='status', - option='--sites') + epilog_msg = help_msgs.PARSER_EPILOG.format(cli="status", option="--sites") epilog_msg += STATUS_EPILOG parser = argparse.ArgumentParser( description=STATUS_PARSER, - parents=[parent_parser], formatter_class=RawTextHelpFormatter, + parents=[parent_parser], + formatter_class=RawTextHelpFormatter, epilog=epilog_msg, ) - parser.set_defaults(command='with_argument') + parser.set_defaults(command="with_argument") status_group = parser.add_mutually_exclusive_group(required=True) status_group.add_argument( - '-s', '--sites', help='get testbed sites list', const='sites', - dest='command', action='store_const') + "-s", + "--sites", + help="get testbed sites list", + const="sites", + dest="command", + action="store_const", + ) status_group.add_argument( - '-n', '--nodes', help='get testbed nodes list', const='nodes', - dest='command', action='store_const') + "-n", + "--nodes", + help="get testbed nodes list", + const="nodes", + dest="command", + action="store_const", + ) status_group.add_argument( - '-ni', '--nodes-ids', help='get testbed nodes ids list (1-3+5)', - const='nodes-ids', dest='command', action='store_const') + "-ni", + "--nodes-ids", + help="get testbed nodes ids list (1-3+5)", + const="nodes-ids", + dest="command", + action="store_const", + ) status_group.add_argument( - '-er', '--experiments-running', - help='get testbed running experiments list', const='experiments', - dest='command', action='store_const') - - parser.add_argument('--site', - action='append', dest='nodes_selection', - type=lambda x: ('site', x), - help='testbed nodes list filter by site') - parser.add_argument('--archi', - action='append', dest='nodes_selection', - type=lambda x: ('archi', x), - help='testbed nodes list filter by architecture') - parser.add_argument('--state', action='append', dest='nodes_selection', - type=lambda x: ('state', x), - help='testbed nodes list filter by state') + "-er", + "--experiments-running", + help="get testbed running experiments list", + const="experiments", + dest="command", + action="store_const", + ) + + parser.add_argument( + "--site", + action="append", + dest="nodes_selection", + type=lambda x: ("site", x), + help="testbed nodes list filter by site", + ) + parser.add_argument( + "--archi", + action="append", + dest="nodes_selection", + type=lambda x: ("archi", x), + help="testbed nodes list filter by architecture", + ) + parser.add_argument( + "--state", + action="append", + dest="nodes_selection", + type=lambda x: ("state", x), + help="testbed nodes list filter by state", + ) return parser def status_parse_and_run(opts): - """ Parse namespace 'opts' object and execute requested command """ + """Parse namespace 'opts' object and execute requested command""" user, passwd = auth.get_user_credentials(opts.username, opts.password) api = rest.Api(user, passwd) selection = dict(opts.nodes_selection or ()) @@ -110,7 +137,7 @@ def status_parse_and_run(opts): def main(args=None): - """ Main command-line execution loop." """ + """Main command-line execution loop." """ args = args or sys.argv[1:] parser = parse_options() common.main_cli(status_parse_and_run, parser, args) diff --git a/iotlabcli/profile.py b/iotlabcli/profile.py index 4e59b1f..18e47de 100644 --- a/iotlabcli/profile.py +++ b/iotlabcli/profile.py @@ -24,19 +24,25 @@ # pylint:disable=too-few-public-methods -class ProfileM3A8(): - """A generic Profile for M3 and A8 """ +class ProfileM3A8: + """A generic Profile for M3 and A8""" + choices = { - 'power_mode': ['dc', 'battery'], - 'consumption': {'period': [140, 204, 332, 588, 1100, 2116, 4156, 8244], - 'average': [1, 4, 16, 64, 128, 256, 512, 1024]}, - 'radio': {'channels': range(11, 27), 'num_per_channel': range(0, 256), - 'period': range(1, 2**16)} + "power_mode": ["dc", "battery"], + "consumption": { + "period": [140, 204, 332, 588, 1100, 2116, 4156, 8244], + "average": [1, 4, 16, 64, 128, 256, 512, 1024], + }, + "radio": { + "channels": range(11, 27), + "num_per_channel": range(0, 256), + "period": range(1, 2**16), + }, } arch = None def __init__(self, profilename, power): - assert power in self.choices['power_mode'] + assert power in self.choices["power_mode"] assert self.arch is not None, "Using Generic class" self.nodearch = self.arch self.profilename = profilename @@ -45,46 +51,44 @@ def __init__(self, profilename, power): self.consumption = None self.radio = None - # pylint: disable=too-many-arguments - def set_consumption(self, period, average, - power=False, voltage=False, current=False): - """ Configure consumption measures """ + def set_consumption( # pylint: disable=too-many-arguments,too-many-positional-arguments + self, period, average, power=False, voltage=False, current=False + ): + """Configure consumption measures""" if not power and not voltage and not current: return _err = "Required values period/average for consumption measure." assert period is not None and average is not None, _err - assert period in self.choices['consumption']['period'] - assert average in self.choices['consumption']['average'] + assert period in self.choices["consumption"]["period"] + assert average in self.choices["consumption"]["average"] self.consumption = { - 'period': period, - 'average': average, - 'power': power, - 'voltage': voltage, - 'current': current, + "period": period, + "average": average, + "power": power, + "voltage": voltage, + "current": current, } def set_radio(self, mode, channels, period=None, num_per_channel=None): - """ Configure radio measures """ + """Configure radio measures""" if not mode: return assert channels for channel in channels: - assert channel in self.choices['radio']['channels'] + assert channel in self.choices["radio"]["channels"] - assert mode in ['rssi', 'sniffer'] - self.radio = { - 'mode': mode - } + assert mode in ["rssi", "sniffer"] + self.radio = {"mode": mode} config_radio = { - 'rssi': self._cfg_radio_rssi, - 'sniffer': self._cfg_radio_sniffer + "rssi": self._cfg_radio_rssi, + "sniffer": self._cfg_radio_sniffer, } config_radio[mode](channels, period, num_per_channel) def _cfg_radio_rssi(self, channels, period, num_per_channel=None): - """ Check parameters for rssi measures and set config """ + """Check parameters for rssi measures and set config""" num_per_channel = num_per_channel or 0 _err = "Required 'channels/period' for radio rssi measure" @@ -94,16 +98,16 @@ def _cfg_radio_rssi(self, channels, period, num_per_channel=None): _err = "Required 'num_per_channel' as multiple channels provided" assert len(channels) == 1 or num_per_channel != 0, _err - assert period in self.choices['radio']['period'] - assert num_per_channel in self.choices['radio']['num_per_channel'] + assert period in self.choices["radio"]["period"] + assert num_per_channel in self.choices["radio"]["num_per_channel"] # Write usefull parameters - self.radio['channels'] = channels - self.radio['period'] = period - self.radio['num_per_channel'] = num_per_channel + self.radio["channels"] = channels + self.radio["period"] = period + self.radio["num_per_channel"] = num_per_channel def _cfg_radio_sniffer(self, channels, period=None, num_per_channel=None): - """ Check parameters for sniffer measures and set the configuration """ + """Check parameters for sniffer measures and set the configuration""" # 'Period' and multiple channels should be handled later when supported _err = "`period` and `num_per_channel` not allowed for sniffer" @@ -112,9 +116,9 @@ def _cfg_radio_sniffer(self, channels, period=None, num_per_channel=None): assert len(channels) == 1, "Only one channel is allowed" # Write config - self.radio['channels'] = channels - self.radio['period'] = None - self.radio['num_per_channel'] = None + self.radio["channels"] = channels + self.radio["period"] = None + self.radio["num_per_channel"] = None def __eq__(self, other): # pragma: no cover return self.__dict__ == other.__dict__ @@ -122,31 +126,35 @@ def __eq__(self, other): # pragma: no cover class ProfileM3(ProfileM3A8): """A Profile measure class for M3.""" - arch = 'm3' + + arch = "m3" class ProfileA8(ProfileM3A8): """A Profile measure class for A8.""" - arch = 'a8' + + arch = "a8" class ProfileCustom(ProfileM3A8): """A Profile measure class for Custom.""" - arch = 'custom' + arch = "custom" + + +class ProfileWSN430: + """A Profile measure class for WSN430""" -class ProfileWSN430(): - """A Profile measure class for WSN430 """ choices = { - 'power_mode': ['dc', 'battery'], - 'consumption': {'frequency': [5000, 1000, 500, 100, 70]}, - 'radio': {'frequency': [5000, 1000, 500]}, - 'sensor': {'frequency': [30000, 10000, 5000, 1000]}, + "power_mode": ["dc", "battery"], + "consumption": {"frequency": [5000, 1000, 500, 100, 70]}, + "radio": {"frequency": [5000, 1000, 500]}, + "sensor": {"frequency": [30000, 10000, 5000, 1000]}, } def __init__(self, profilename, power): - assert power in ProfileWSN430.choices['power_mode'] - self.nodearch = 'wsn430' + assert power in ProfileWSN430.choices["power_mode"] + self.nodearch = "wsn430" self.profilename = profilename self.power = power @@ -154,44 +162,43 @@ def __init__(self, profilename, power): self.radio = None self.sensor = None - def set_consumption(self, frequency, power=False, voltage=False, - current=False): - """ Configure consumption measures """ + def set_consumption(self, frequency, power=False, voltage=False, current=False): + """Configure consumption measures""" if not power and not voltage and not current: return _err = "Required 'frequency' for consumption measure" assert frequency is not None, _err - assert frequency in self.choices['consumption']['frequency'] + assert frequency in self.choices["consumption"]["frequency"] self.consumption = { - 'frequency': frequency, - 'power': power, - 'voltage': voltage, - 'current': current, + "frequency": frequency, + "power": power, + "voltage": voltage, + "current": current, } def set_radio(self, frequency): - """ Configure radio measures """ + """Configure radio measures""" if not frequency: return - assert frequency in self.choices['radio']['frequency'] + assert frequency in self.choices["radio"]["frequency"] self.radio = { - 'frequency': frequency, - 'rssi': True, + "frequency": frequency, + "rssi": True, } def set_sensors(self, frequency, temperature=False, luminosity=False): - """ Configure sensor measures """ + """Configure sensor measures""" if not temperature and not luminosity: return _err = "Required 'frequency' for sensor measure" assert frequency is not None, _err - assert frequency in self.choices['sensor']['frequency'] + assert frequency in self.choices["sensor"]["frequency"] self.sensor = { - 'frequency': frequency, - 'luminosity': luminosity, - 'temperature': temperature, + "frequency": frequency, + "luminosity": luminosity, + "temperature": temperature, } def __eq__(self, other): # pragma: no cover diff --git a/iotlabcli/rest.py b/iotlabcli/rest.py index a1de44e..467e8f3 100644 --- a/iotlabcli/rest.py +++ b/iotlabcli/rest.py @@ -19,7 +19,7 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Rest API class +"""Rest API class Methods are meant to be used internally. @@ -29,23 +29,26 @@ """ import sys + import requests from requests.auth import HTTPBasicAuth + from iotlabcli import helpers + # pylint: disable=import-error,no-name-in-module # pylint: disable=wrong-import-order try: # pragma: no cover - from urllib.parse import urljoin, urlencode from urllib.error import HTTPError + from urllib.parse import urlencode, urljoin except ImportError: # pragma: no cover # pylint: disable=import-error,no-name-in-module,ungrouped-imports - from urlparse import urljoin from urllib import urlencode + from urllib2 import HTTPError + from urlparse import urljoin try: # pragma: no cover - # With newer versions of requests, old python version may # raise an InsecurePlatformWarning # https://urllib3.readthedocs.org/en/latest/\ @@ -59,16 +62,18 @@ # pip install iotlabcli[secure] import urllib3.contrib.pyopenssl + urllib3.contrib.pyopenssl.inject_into_urllib3() except ImportError: pass # pylint: disable=maybe-no-member,no-member -class Api(): # pylint:disable=too-many-public-methods - """ IoT-Lab REST API """ +class Api: # pylint:disable=too-many-public-methods + """IoT-Lab REST API""" + _cache = {} - url = helpers.read_custom_api_url() or 'https://www.iot-lab.info/api/' + url = helpers.read_custom_api_url() or "https://www.iot-lab.info/api/" def __init__(self, username, password): """ @@ -79,48 +84,48 @@ def __init__(self, username, password): self.auth = HTTPBasicAuth(username, password) def get_sites_details(self): - """ Get testbed sites details """ - return self.method('sites/details') + """Get testbed sites details""" + return self.method("sites/details") def get_nodes(self, list_id=False, site=None, **selections): - """ Get testbed nodes description + """Get testbed nodes description :param list_id: return result in 'exp_list' format '3-12+35' :param site: restrict to site :param **selections: other selections than site """ if site: - selections['site'] = site + selections["site"] = site url = f"nodes{'/ids' if list_id else ''}" if selections: # the order of parameters in the encoded string # will match the order of tuples list - url += '?' + urlencode(sorted(list(selections.items()))) + url += "?" + urlencode(sorted(list(selections.items()))) return self.method(url) def submit_experiment(self, files): - """ Submit user experiment + """Submit user experiment :param files: experiment description and firmware(s) :type files: dictionnary :returns JSONObject """ - return self.method('experiments', 'post', files=files) + return self.method("experiments", "post", files=files) - def get_experiments(self, state='Running', limit=0, offset=0): - """ Get user's experiment + def get_experiments(self, state="Running", limit=0, offset=0): + """Get user's experiment :returns JSONObject """ - queryset = f'state={state}&limit={limit}&offset={offset}' - return self.method(f'experiments?{queryset}') + queryset = f"state={state}&limit={limit}&offset={offset}" + return self.method(f"experiments?{queryset}") def get_running_experiments(self): - """ Get testbed running experiments """ - return self.method('experiments/running') + """Get testbed running experiments""" + return self.method("experiments/running") - def get_experiment_info(self, expid, option=''): - """ Get user experiment description. + def get_experiment_info(self, expid, option=""): + """Get user experiment description. :param expid: experiment id submission (e.g. OAR scheduler) :param option: Restrict to some values: * '': experiment submission @@ -129,19 +134,19 @@ def get_experiment_info(self, expid, option=''): * 'data': experiment tar.gz with description and firmwares * 'deployment': deployment info """ - assert option in ('', 'nodes', 'nodes_ids', 'data', 'deployment') + assert option in ("", "nodes", "nodes_ids", "data", "deployment") - url = f'experiments/{expid}' + url = f"experiments/{expid}" if option: - url += f'/{option}' - return self.method(url, raw=(option == 'data')) + url += f"/{option}" + return self.method(url, raw=option == "data") def stop_experiment(self, expid): - """ Stop user experiment. + """Stop user experiment. :param id: experiment id submission (e.g. OAR scheduler) """ - return self.method(f'experiments/{expid}', 'delete') + return self.method(f"experiments/{expid}", "delete") def reload_experiment(self, expid, exp_json=None): """Reload user experiment. @@ -150,26 +155,26 @@ def reload_experiment(self, expid, exp_json=None): :param exp_json: experiment duration and reservation configuration :returns JSONObject """ - url = f'experiments/{expid}/reload' - return self.method(url, 'post', json=exp_json) + url = f"experiments/{expid}/reload" + return self.method(url, "post", json=exp_json) # Node commands def node_command(self, command, expid, nodes=(), option=None): - """ Lanch 'command' on user experiment list nodes + """Lanch 'command' on user experiment list nodes :param id: experiment id submission (e.g. OAR scheduler) :param nodes: list of nodes, if empty apply on all nodes :param opt: additional string to pass as option in the url :returns: dict """ - url = f'experiments/{expid}/nodes/{command}' + url = f"experiments/{expid}/nodes/{command}" if option: - url += f'/{option}' - return self.method(url, 'post', json=nodes) + url += f"/{option}" + return self.method(url, "post", json=nodes) def node_update(self, expid, files, binary=False): - """ Launch update command (flash firmware) on user + """Launch update command (flash firmware) on user experiment list nodes :param id: experiment id submission (e.g. OAR scheduler) @@ -177,10 +182,10 @@ def node_update(self, expid, files, binary=False): :type files: dict :returns: dict """ - url = f'experiments/{expid}/nodes/flash' + url = f"experiments/{expid}/nodes/flash" if binary: - url += '/binary' - return self.method(url, 'post', files=files) + url += "/binary" + return self.method(url, "post", files=files) def node_profile_load(self, expid, files): """Update profile with profile json on user @@ -191,8 +196,7 @@ def node_profile_load(self, expid, files): :type files: dict :returns: dict """ - return self.method(f'experiments/{expid}/nodes/monitoring', - 'post', files=files) + return self.method(f"experiments/{expid}/nodes/monitoring", "post", files=files) # script def script_command(self, expid, command, files=None, json=None): @@ -204,35 +208,38 @@ def script_command(self, expid, command, files=None, json=None): :param json: 'kill/status' only: sites list, may be empty for all sites """ # Only json or files and for the correct command (inverted checks) - assert json is not None or command in ('run',) - assert files is not None or command in ('kill', 'status',) + assert json is not None or command in ("run",) + assert files is not None or command in ( + "kill", + "status", + ) - url = f'experiments/{expid}/scripts/{command}' - return self.method(url, 'post', files=files, json=json) + url = f"experiments/{expid}/scripts/{command}" + return self.method(url, "post", files=files, json=json) # Profile methods def get_profiles(self, archi=None): - """ Get user's list profile description + """Get user's list profile description :returns JSONObject """ - url = 'monitoring' + url = "monitoring" if archi is not None: - url += f'?archi={archi}' + url += f"?archi={archi}" return self.method(url) def get_profile(self, name): - """ Get user profile description. + """Get user profile description. :param name: profile name :type name: string :returns JSONObject """ - return self.method(f'monitoring/{name}') + return self.method(f"monitoring/{name}") def add_profile(self, profile): - """ Add user profile + """Add user profile :param profile: profile description :type profile: JSONObject. @@ -240,22 +247,22 @@ def add_profile(self, profile): # dict has no __dict__ and load_profile gives a dict # requests wants a 'simple' type like dict profile = profile if isinstance(profile, dict) else profile.__dict__ - ret = self.method('monitoring', 'post', json=profile) + ret = self.method("monitoring", "post", json=profile) return ret def del_profile(self, name): - """ Delete user profile + """Delete user profile :param profile_name: name :type profile_name: string """ - ret = self.method(f'monitoring/{name}', 'delete') + ret = self.method(f"monitoring/{name}", "delete") return ret def check_credential(self): - """ Check that the credentials are valid """ + """Check that the credentials are valid""" try: - self.method('user') + self.method("user") return True except HTTPError as err: if err.code == 401: @@ -265,13 +272,13 @@ def check_credential(self): # ssh keys api def get_ssh_keys(self): - """ Get user's registered ssh keys """ - ret = self.method('user/keys') + """Get user's registered ssh keys""" + ret = self.method("user/keys") return ret def set_ssh_keys(self, ssh_keys_json): - """ Set user's ssh keys """ - self.method('user/keys', 'post', json=ssh_keys_json, raw=True) + """Set user's ssh keys""" + self.method("user/keys", "post", json=ssh_keys_json, raw=True) # robot @@ -281,9 +288,8 @@ def robot_command(self, command, expid, nodes=()): :param id: experiment id submission (e.g. OAR scheduler) :param nodes: list of nodes, if empty apply on all nodes """ - assert command in ('status',) - return self.method(f'experiments/{expid}/robots/{command}', - 'post', json=nodes) + assert command in ("status",) + return self.method(f"experiments/{expid}/robots/{command}", "post", json=nodes) def robot_update_mobility(self, expid, name, nodes=()): """Update mobility on user experiment robot list nodes. @@ -291,39 +297,45 @@ def robot_update_mobility(self, expid, name, nodes=()): :param id: experiment id submission (e.g. OAR scheduler) :param nodes: list of nodes, if empty apply on all nodes """ - url = f'experiments/{expid}/robots/mobility/{name}' - return self.method(url, 'post', json=nodes) + url = f"experiments/{expid}/robots/mobility/{name}" + return self.method(url, "post", json=nodes) @classmethod def get_robot_mapfile(cls, site, mapfile): - """ Download robot mapfile. + """Download robot mapfile. :params site: Map info for site :params mapfile: select type in ('mapconfig', 'mapimage', 'dockconfig') :returns: Image content or json loaded structure """ - assert mapfile in ('map/config', 'map/image', 'dock/config') - raw = mapfile in ('map/image',) + assert mapfile in ("map/config", "map/image", "dock/config") + raw = mapfile in ("map/image",) api = cls(None, None) - url = f'robots/{site}/{mapfile}' + url = f"robots/{site}/{mapfile}" return api.method(url, raw=raw) def get_circuits(self, **selections): """List circuits mobilities.""" - url = 'mobilities/circuits' + url = "mobilities/circuits" if selections: # the order of parameters in the encoded string # will match the order of tuples list - url += '?' + urlencode(sorted(list(selections.items()))) + url += "?" + urlencode(sorted(list(selections.items()))) return self.method(url) def get_circuit(self, name): """Get user mobilities.""" - return self.method(f'mobilities/circuits/{name}') - - def method(self, url, method='get', # pylint:disable=too-many-arguments - json=None, files=None, raw=False): + return self.method(f"mobilities/circuits/{name}") + + def method( # pylint:disable=too-many-arguments,too-many-positional-arguments + self, + url, + method="get", + json=None, + files=None, + raw=False, + ): """Call http `method` on iot-lab-url/'url'. :param url: url of API. @@ -332,13 +344,12 @@ def method(self, url, method='get', # pylint:disable=too-many-arguments :param files: send as 'post' multipart data :param raw: Should data be loaded as json or not """ - assert method in ('get', 'post', 'delete') - assert (method == 'post') or (files is None and json is None) + assert method in ("get", "post", "delete") + assert (method == "post") or (files is None and json is None) _url = urljoin(self.url, url) - req = self._request(_url, method, auth=self.auth, - json=json, files=files) + req = self._request(_url, method, auth=self.auth, json=json, files=files) if requests.codes.ok == req.status_code: return req.content if raw else req.json() if requests.codes.no_content == req.status_code: @@ -347,36 +358,36 @@ def method(self, url, method='get', # pylint:disable=too-many-arguments @staticmethod def _request(url, method, **kwargs): - """ Call http `method` on 'url' + """Call http `method` on 'url' :param url: url of API. :param method: request method - :param **kwargs: requests.request additional arguments """ + :param **kwargs: requests.request additional arguments""" try: - return requests.request(method, url, **kwargs) + return requests.request(method, url, timeout=None, **kwargs) except Exception: # show issue with old requests versions raise RuntimeError(sys.exc_info()) @staticmethod def _raise_http_error(url, req): - """ Raises HTTP error for 'url' and 'req' """ + """Raises HTTP error for 'url' and 'req'""" # Indent req.text to pretty print it later - indented_lines = ['\t' + line for line in req.text.splitlines(True)] - msg = '\n' + ''.join(indented_lines) + indented_lines = ["\t" + line for line in req.text.splitlines(True)] + msg = "\n" + "".join(indented_lines) raise HTTPError(url, req.status_code, msg, req.headers, None) @classmethod def get_sites(cls): - """ Get testbed sites description + """Get testbed sites description May be run unauthicated :returns JSONObject """ - return cls._get_with_cache('sites') + return cls._get_with_cache("sites") @classmethod def _get_with_cache(cls, url): - """ Get resource from either cache or rest + """Get resource from either cache or rest :returns JSONObject """ try: diff --git a/iotlabcli/robot.py b/iotlabcli/robot.py index fd50c7f..72365b3 100644 --- a/iotlabcli/robot.py +++ b/iotlabcli/robot.py @@ -19,13 +19,13 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Implement the 'robot' requests """ +"""Implement the 'robot' requests""" from iotlabcli.rest import Api def robot_command(api, command, exp_id, nodes_list=()): - """ Launch commands ('status',) on nodes_list + """Launch commands ('status',) on nodes_list :param api: API Rest api object :param command: command that should be run @@ -33,7 +33,7 @@ def robot_command(api, command, exp_id, nodes_list=()): :param nodes_list: List of nodes where to run command. Empty list runs on all nodes """ - assert command in ('status',) + assert command in ("status",) result = api.robot_command(command, exp_id, nodes_list) return result @@ -59,28 +59,31 @@ def circuit_command(api, command, name=None, **selection): :param **selections: selections by circuit site and type """ - assert command in ('list', 'get') - if 'type' in selection: - assert selection['type'] in ('predefined', 'userdefined',) - - if command == 'list': + assert command in ("list", "get") + if "type" in selection: + assert selection["type"] in ( + "predefined", + "userdefined", + ) + + if command == "list": result = api.get_circuits(**selection) - elif command == 'get': + elif command == "get": result = api.get_circuit(name) else: # pragma: no cover - raise ValueError(f'Unknown command {command!r}') + raise ValueError(f"Unknown command {command!r}") return result def robot_get_map(site): - """ Download all robot map files + """Download all robot map files - Download robot site config, map and docks list """ + Download robot site config, map and docks list""" map_cfg = {} - map_cfg['config'] = Api.get_robot_mapfile(site, 'map/config') - map_cfg['image'] = Api.get_robot_mapfile(site, 'map/image') - map_cfg['dock'] = Api.get_robot_mapfile(site, 'dock/config') + map_cfg["config"] = Api.get_robot_mapfile(site, "map/config") + map_cfg["image"] = Api.get_robot_mapfile(site, "map/image") + map_cfg["dock"] = Api.get_robot_mapfile(site, "dock/config") return map_cfg diff --git a/iotlabcli/status.py b/iotlabcli/status.py index 7dd3caf..56bca18 100644 --- a/iotlabcli/status.py +++ b/iotlabcli/status.py @@ -19,19 +19,18 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Implement the 'status' requests """ +"""Implement the 'status' requests""" def status_command(api, command, **selections): - """ Launch testbed status commands + """Launch testbed status commands :param api: API Rest api object :param command: command that should be run :param **selections: other selections (site, archi, state) """ - assert command in ('sites', 'nodes', 'nodes-ids', - 'experiments') - if command == 'experiments': + assert command in ("sites", "nodes", "nodes-ids", "experiments") + if command == "experiments": result = api.get_running_experiments() elif command == "sites": result = api.get_sites_details() diff --git a/iotlabcli/tests/__init__.py b/iotlabcli/tests/__init__.py index 18a41e0..fc9bdca 100644 --- a/iotlabcli/tests/__init__.py +++ b/iotlabcli/tests/__init__.py @@ -20,6 +20,7 @@ # knowledge of the CeCILL license and that you accept its terms. """Tests for iotlabcli package.""" + import os.path diff --git a/iotlabcli/tests/associations_test.py b/iotlabcli/tests/associations_test.py index ef1381f..79e84f6 100644 --- a/iotlabcli/tests/associations_test.py +++ b/iotlabcli/tests/associations_test.py @@ -24,8 +24,7 @@ import json import unittest -from iotlabcli import associations -from iotlabcli import helpers +from iotlabcli import associations, helpers # pylint:disable=protected-access # pylint:disable=invalid-name @@ -45,119 +44,103 @@ def _assert_json_equal(self, assocs, expected): def test_associations_map(self): """Test AssociationsMap.""" - assocs = associations.AssociationsMap('firmware', 'nodes', - helpers.node_url_sort_key) + assocs = associations.AssociationsMap( + "firmware", "nodes", helpers.node_url_sort_key + ) # Add 'firmware.elf' - assoc = assocs.extendvalues('firmware.elf', ['m3-2', 'm3-1', 'm3-10']) + assoc = assocs.extendvalues("firmware.elf", ["m3-2", "m3-1", "m3-10"]) expected = [ - { - 'firmwarename': 'firmware.elf', - 'nodes': ['m3-1', 'm3-2', 'm3-10'] - } + {"firmwarename": "firmware.elf", "nodes": ["m3-1", "m3-2", "m3-10"]} ] self._assert_json_equal(assocs, expected) # Add nodes - assocs['firmware.elf'] += ['m3-6'] # also works with '+=' + assocs["firmware.elf"] += ["m3-6"] # also works with '+=' expected = [ - { - 'firmwarename': 'firmware.elf', - 'nodes': ['m3-1', 'm3-2', 'm3-6', 'm3-10'] - } + {"firmwarename": "firmware.elf", "nodes": ["m3-1", "m3-2", "m3-6", "m3-10"]} ] self._assert_json_equal(assocs, expected) # Add 'a_tutorial.elf' - assoc = assocs.setdefault('a_tutorial.elf', []) - assoc.extend(['m3-5', 'm3-4']) # Use extend, data needs to be sorted + assoc = assocs.setdefault("a_tutorial.elf", []) + assoc.extend(["m3-5", "m3-4"]) # Use extend, data needs to be sorted expected = [ + {"firmwarename": "a_tutorial.elf", "nodes": ["m3-4", "m3-5"]}, { - 'firmwarename': 'a_tutorial.elf', - 'nodes': ['m3-4', 'm3-5'] + "firmwarename": "firmware.elf", + "nodes": ["m3-1", "m3-2", "m3-6", "m3-10"], }, - { - 'firmwarename': 'firmware.elf', - 'nodes': ['m3-1', 'm3-2', 'm3-6', 'm3-10'] - } ] self._assert_json_equal(assocs, expected) # remove an entry - del assocs['firmware.elf'] - expected = [ - { - 'firmwarename': 'a_tutorial.elf', - 'nodes': ['m3-4', 'm3-5'] - } - ] + del assocs["firmware.elf"] + expected = [{"firmwarename": "a_tutorial.elf", "nodes": ["m3-4", "m3-5"]}] self._assert_json_equal(assocs, expected) + # __len__ on an _Association instance (firmwarename + nodes = 2 attrs) + assoc_obj = next(iter(assocs)) + self.assertEqual(len(assoc_obj), 2) + def test_associationsmap_from_list(self): """Test loading a dict from a list.""" - assocs = associations.AssociationsMap('firmware', 'nodes', - helpers.node_url_sort_key) - assocs.extendvalues('test.elf', ['m3-1', 'm3-2']) - assocs.extendvalues('fw.elf', ['m3-20', 'm3-5']) + assocs = associations.AssociationsMap( + "firmware", "nodes", helpers.node_url_sort_key + ) + assocs.extendvalues("test.elf", ["m3-1", "m3-2"]) + assocs.extendvalues("fw.elf", ["m3-20", "m3-5"]) assocs_list = assocs.list() loaded_assocs = associations.AssociationsMap.from_list( - assocs_list, 'firmware', 'nodes', helpers.node_url_sort_key) + assocs_list, "firmware", "nodes", helpers.node_url_sort_key + ) self.assertEqual(assocs_list, loaded_assocs.list()) self.assertEqual(loaded_assocs, assocs) # Check keys - self.assertEqual(sorted(assocs.keys()), ['fw.elf', 'test.elf']) + self.assertEqual(sorted(assocs.keys()), ["fw.elf", "test.elf"]) # Test loading None - ret = associations.AssociationsMap.from_list(None, 'script', 'sites') + ret = associations.AssociationsMap.from_list(None, "script", "sites") self.assertTrue(ret is None) def test_association_dict_factory(self): """Test associationsmapdict_from_dict.""" assocsdict = { - 'firmware': [ + "firmware": [ + {"firmwarename": "a_tutorial.elf", "nodes": ["m3-4", "m3-6"]}, { - 'firmwarename': 'a_tutorial.elf', - 'nodes': ['m3-4', 'm3-6'] - }, - { - 'firmwarename': 'firmware.elf', - 'nodes': ['m3-1', 'm3-2', 'm3-5', 'm3-10'] + "firmwarename": "firmware.elf", + "nodes": ["m3-1", "m3-2", "m3-5", "m3-10"], }, ], - 'profile': [ - { - 'profilename': 'consumption', - 'nodes': ['m3-4'] - }, - { - 'profilename': 'radio', - 'nodes': ['m3-1', 'm3-2'] - }, + "profile": [ + {"profilename": "consumption", "nodes": ["m3-4"]}, + {"profilename": "radio", "nodes": ["m3-1", "m3-2"]}, ], } - ret = associations.associationsmapdict_from_dict(assocsdict, 'nodes') - self.assertEqual(ret['profile']['consumption'], ['m3-4']) - self.assertEqual(ret['firmware']['a_tutorial.elf'], ['m3-4', 'm3-6']) + ret = associations.associationsmapdict_from_dict(assocsdict, "nodes") + self.assertEqual(ret["profile"]["consumption"], ["m3-4"]) + self.assertEqual(ret["firmware"]["a_tutorial.elf"], ["m3-4", "m3-6"]) # None case - ret = associations.associationsmapdict_from_dict(None, 'nodes') + ret = associations.associationsmapdict_from_dict(None, "nodes") self.assertTrue(ret is None) class TestAssociation(unittest.TestCase): """Test Association corner cases.""" + def test_repr(self): """Test Association 'repr'.""" - assocclass = associations._Association.for_key_value('firmware', - 'nodes') - assoc = assocclass('test.elf', ['m3-1', 'm3-2', 'm3-3']) + assocclass = associations._Association.for_key_value("firmware", "nodes") + assoc = assocclass("test.elf", ["m3-1", "m3-2", "m3-3"]) - reprret = 'FirmwareNodesAssociation(%r, %r)' - ret = reprret % ('test.elf', ['m3-1', 'm3-2', 'm3-3']) + reprret = "FirmwareNodesAssociation(%r, %r)" + ret = reprret % ("test.elf", ["m3-1", "m3-2", "m3-3"]) self.assertEqual(repr(assoc), ret) # only repr defined @@ -165,13 +148,13 @@ def test_repr(self): def test_abstract_class(self): """Test error when instanciating abstract class.""" - self.assertRaises(NotImplementedError, associations._Association, - 'test.elf', ['m3-1', 'm3-2']) + self.assertRaises( + NotImplementedError, associations._Association, "test.elf", ["m3-1", "m3-2"] + ) def test_accessing_removed_method(self): """Test could not access dict methods on object.""" - assocclass = associations._Association.for_key_value('firmware', - 'nodes') - assoc = assocclass('test.elf', ['m3-1', 'm3-2', 'm3-3']) + assocclass = associations._Association.for_key_value("firmware", "nodes") + assoc = assocclass("test.elf", ["m3-1", "m3-2", "m3-3"]) with self.assertRaises(AttributeError): assoc.update() diff --git a/iotlabcli/tests/auth_parser_test.py b/iotlabcli/tests/auth_parser_test.py index ca31028..507cc7a 100644 --- a/iotlabcli/tests/auth_parser_test.py +++ b/iotlabcli/tests/auth_parser_test.py @@ -19,7 +19,7 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.parser.auth module """ +"""Test the iotlabcli.parser.auth module""" import sys import unittest @@ -32,85 +32,83 @@ # pylint: disable=missing-docstring,too-many-public-methods -@patch('sys.stderr', sys.stdout) -@patch('iotlabcli.auth.ssh_keys') -@patch('iotlabcli.auth.add_ssh_key') -@patch('iotlabcli.auth.write_password_file') +@patch("sys.stderr", sys.stdout) +@patch("iotlabcli.auth.ssh_keys") +@patch("iotlabcli.auth.add_ssh_key") +@patch("iotlabcli.auth.write_password_file") class TestMainAuthParser(unittest.TestCase): - @patch('getpass.getpass') + @patch("getpass.getpass") def test_main(self, getpass_m, store_m, ssh_key_m, ssh_keys_m): - """ Test parser.auth.main function """ - store_m.return_value = 'Written' - getpass_m.return_value = 'password2' + """Test parser.auth.main function""" + store_m.return_value = "Written" + getpass_m.return_value = "password2" - with patch('iotlabcli.auth.Api') as api_class: + with patch("iotlabcli.auth.Api") as api_class: api = api_class.return_value api.check_credential.return_value = True - auth_parser.main(['-u', 'super_user', '-p', 'password']) - store_m.assert_called_with('super_user', 'password') + auth_parser.main(["-u", "super_user", "-p", "password"]) + store_m.assert_called_with("super_user", "password") self.assertFalse(getpass_m.called) getpass_m.reset_mock() store_m.reset_mock() api.check_credential.return_value = False - self.assertRaises(SystemExit, auth_parser.main, - ['-u', 'super_user']) + self.assertRaises(SystemExit, auth_parser.main, ["-u", "super_user"]) self.assertTrue(getpass_m.called) self.assertEqual(0, store_m.call_count) # -u/--user is required - self.assertRaises(SystemExit, auth_parser.main, ['-p', 'test']) + self.assertRaises(SystemExit, auth_parser.main, ["-p", "test"]) # SSH key file - auth_parser.main(['--add-ssh-key']) + auth_parser.main(["--add-ssh-key"]) ssh_key_m.assert_called_with(auth.IDENTITY_FILE) - auth_parser.main(['--add-ssh-key', '--identity-file', 'test']) - ssh_key_m.assert_called_with('test') + auth_parser.main(["--add-ssh-key", "--identity-file", "test"]) + ssh_key_m.assert_called_with("test") # Add ssh key with -u/-p options but key is already set ssh_key_m.reset_mock() api.check_credential.return_value = True - store_m.return_value = 'Written' - ssh_key_m.side_effect = ValueError('key message') - auth_parser.main(['-u', 'super_user', '-p', 'password', - '--add-ssh-key']) - store_m.assert_called_with('super_user', 'password') + store_m.return_value = "Written" + ssh_key_m.side_effect = ValueError("key message") + auth_parser.main(["-u", "super_user", "-p", "password", "--add-ssh-key"]) + store_m.assert_called_with("super_user", "password") ssh_key_m.assert_called_with(auth.IDENTITY_FILE) # List SSH keys - auth_parser.main(['--list-ssh-key']) + auth_parser.main(["--list-ssh-key"]) ssh_keys_m.assert_called_once() # List ssh key with -u/-p options ssh_key_m.reset_mock() ssh_keys_m.call_count = 0 api.check_credential.return_value = True - store_m.return_value = 'Written' - auth_parser.main(['-u', 'super_user', '-p', 'password', - '--list-ssh-keys']) - store_m.assert_called_with('super_user', 'password') + store_m.return_value = "Written" + auth_parser.main(["-u", "super_user", "-p", "password", "--list-ssh-keys"]) + store_m.assert_called_with("super_user", "password") ssh_keys_m.assert_called_once() def test_main_exceptions(self, store_m, ssh_key_m, ssh_keys_m): - """ Test parser.auth.main error cases """ + """Test parser.auth.main error cases""" # Replacing 'side_effect' # pylint:disable=redefined-variable-type # pylint:disable=unused-argument - with patch('iotlabcli.auth.Api') as api_class: + with patch("iotlabcli.auth.Api") as api_class: api = api_class.return_value api.check_credential.return_value = True - store_m.side_effect = IOError('message') - self.assertRaises(SystemExit, auth_parser.main, - ['-u', 'error', '-p', 'password']) + store_m.side_effect = IOError("message") + self.assertRaises( + SystemExit, auth_parser.main, ["-u", "error", "-p", "password"] + ) store_m.side_effect = KeyboardInterrupt() - self.assertRaises(SystemExit, auth_parser.main, - ['-u', 'ctrl_c', '-p', 'password']) + self.assertRaises( + SystemExit, auth_parser.main, ["-u", "ctrl_c", "-p", "password"] + ) - ssh_key_m.side_effect = ValueError('message') - self.assertRaises(SystemExit, auth_parser.main, - ['--add-ssh-key']) + ssh_key_m.side_effect = ValueError("message") + self.assertRaises(SystemExit, auth_parser.main, ["--add-ssh-key"]) diff --git a/iotlabcli/tests/auth_test.py b/iotlabcli/tests/auth_test.py index 98862b6..c4fb3d4 100644 --- a/iotlabcli/tests/auth_test.py +++ b/iotlabcli/tests/auth_test.py @@ -19,7 +19,7 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.auth module """ +"""Test the iotlabcli.auth module""" # pylint:disable=too-many-public-methods @@ -28,21 +28,22 @@ from iotlabcli import auth -from .c23 import patch, mock_open +from .c23 import mock_open, patch -TEST_RC_FILE = 'test_iotlabrc_file' +TEST_RC_FILE = "test_iotlabrc_file" class TestAuthModule(unittest.TestCase): - """ Test the experiment.auth module """ + """Test the experiment.auth module""" + def setUp(self): try: os.remove(TEST_RC_FILE) except OSError: pass - patch('iotlabcli.auth.RC_FILE', TEST_RC_FILE).start() + patch("iotlabcli.auth.RC_FILE", TEST_RC_FILE).start() self.passwords = [] - m_getpass = patch('getpass.getpass').start() + m_getpass = patch("getpass.getpass").start() m_getpass.side_effect = self.getpass def tearDown(self): @@ -53,76 +54,75 @@ def tearDown(self): patch.stopall() def getpass(self): - """ Getpass mock """ + """Getpass mock""" return self.passwords.pop() def test_write_then_read(self): - """ Test writing auth then reading it back """ + """Test writing auth then reading it back""" # pylint: disable=protected-access - auth.write_password_file('username', 'password') - self.assertEqual(('username', 'password'), - auth.get_user_credentials()) + auth.write_password_file("username", "password") + self.assertEqual(("username", "password"), auth.get_user_credentials()) def test_read_with_no_file(self): - """ Test reading password with no file """ + """Test reading password with no file""" # pylint: disable=protected-access self.assertEqual((None, None), auth._read_password_file()) - @patch('iotlabcli.auth._read_password_file') + @patch("iotlabcli.auth._read_password_file") def test_get_user_credentials(self, m_read): - """ Test auth.get_user_credentials """ - m_read.return_value = ('super_user', 'super_passwd') + """Test auth.get_user_credentials""" + m_read.return_value = ("super_user", "super_passwd") # passwords given - self.assertEqual(('user', 'passwd'), - auth.get_user_credentials('user', 'passwd')) - self.passwords = ['password_prompt'] - self.assertEqual(('user', 'password_prompt'), - auth.get_user_credentials('user')) + self.assertEqual( + ("user", "passwd"), auth.get_user_credentials("user", "passwd") + ) + self.passwords = ["password_prompt"] + self.assertEqual(("user", "password_prompt"), auth.get_user_credentials("user")) - self.assertEqual(('super_user', 'super_passwd'), - auth.get_user_credentials()) + self.assertEqual(("super_user", "super_passwd"), auth.get_user_credentials()) def test_error__read_password_file(self): - """ Test Error while reading password file """ + """Test Error while reading password file""" # pylint: disable=protected-access - open_name = 'iotlabcli.auth.open' - m_open = mock_open(read_data='invalid_format:paswd:third_field') - open(TEST_RC_FILE, 'wb').close() # pylint:disable=consider-using-with + open_name = "iotlabcli.auth.open" + m_open = mock_open(read_data="invalid_format:paswd:third_field") + open(TEST_RC_FILE, "wb").close() # pylint:disable=consider-using-with with patch(open_name, m_open, create=True): self.assertRaises(ValueError, auth._read_password_file) - @patch('iotlabcli.auth.Api') + @patch("iotlabcli.auth.Api") def test_check_user_credentials(self, m_api_class): - """ Check the check_user_credentials function """ + """Check the check_user_credentials function""" api = m_api_class.return_value api.check_credential.return_value = True - self.assertEqual(True, auth.check_user_credentials('user', 'password')) + self.assertEqual(True, auth.check_user_credentials("user", "password")) -TEST_SSH_IDENTITY_FILE = 'test_ssh_key_file' -TEST_SSH_PUB_FILE = TEST_SSH_IDENTITY_FILE + '.pub' -TEST_SSHKEYS = {'sshkeys': ['test']} -TEST_KEY = 'testkey' +TEST_SSH_IDENTITY_FILE = "test_ssh_key_file" +TEST_SSH_PUB_FILE = TEST_SSH_IDENTITY_FILE + ".pub" +TEST_SSHKEYS = {"sshkeys": ["test"]} +TEST_KEY = "testkey" class TestSSHKeyFeature(unittest.TestCase): - """ Test the ssh key feature from auth module """ + """Test the ssh key feature from auth module""" + def setUp(self): try: - os.remove(TEST_SSH_PUB_FILE + '.pub') + os.remove(TEST_SSH_PUB_FILE + ".pub") except OSError: pass - with open(TEST_SSH_PUB_FILE, 'w') as key_file: + with open(TEST_SSH_PUB_FILE, "w") as key_file: key_file.write(TEST_KEY) - patch('iotlabcli.auth.IDENTITY_FILE', TEST_SSH_IDENTITY_FILE).start() + patch("iotlabcli.auth.IDENTITY_FILE", TEST_SSH_IDENTITY_FILE).start() def tearDown(self): os.remove(TEST_SSH_PUB_FILE) patch.stopall() - @patch('iotlabcli.auth.Api') + @patch("iotlabcli.auth.Api") def test_add_ssh_key(self, m_api_class): - """ Check the add_ssh_key function """ + """Check the add_ssh_key function""" api = m_api_class.return_value api.get_ssh_keys.return_value = TEST_SSHKEYS @@ -130,7 +130,7 @@ def test_add_ssh_key(self, m_api_class): auth.add_ssh_key() api.get_ssh_keys.assert_called_once() test_keys = TEST_SSHKEYS - test_keys['sshkeys'].append(TEST_SSHKEYS) + test_keys["sshkeys"].append(TEST_SSHKEYS) api.set_ssh_keys.assert_called_with(test_keys) api.get_ssh_keys.call_count = 0 @@ -138,9 +138,9 @@ def test_add_ssh_key(self, m_api_class): self.assertRaises(ValueError, auth.add_ssh_key, TEST_SSH_IDENTITY_FILE) api.get_ssh_keys.assert_called_once() - @patch('iotlabcli.auth.Api') + @patch("iotlabcli.auth.Api") def test_ssh_keys(self, m_api_class): # pylint:disable=no-self-use - """ Check the ssh_keys function """ + """Check the ssh_keys function""" api = m_api_class.return_value api.get_ssh_keys.return_value = TEST_SSHKEYS auth.ssh_keys() diff --git a/iotlabcli/tests/c23.py b/iotlabcli/tests/c23.py index 7c61bc2..8f82256 100644 --- a/iotlabcli/tests/c23.py +++ b/iotlabcli/tests/c23.py @@ -29,18 +29,19 @@ # flake8: noqa from sys import version_info + if version_info[0] == 2: # pragma: no cover # python2 - from urllib2 import HTTPError import mock from cStringIO import StringIO + from urllib2 import HTTPError elif version_info[0] == 3: # pragma: no cover # python3 - from urllib.error import HTTPError - from unittest import mock from io import StringIO + from unittest import mock + from urllib.error import HTTPError else: # pragma: no cover - raise ValueError(f'Unknown python version {version_info!r}') + raise ValueError(f"Unknown python version {version_info!r}") # pylint:disable=wrong-import-position -from mock import patch, Mock, mock_open # noqa +from mock import Mock, mock_open, patch # noqa diff --git a/iotlabcli/tests/common_parser_test.py b/iotlabcli/tests/common_parser_test.py index 2dd729c..17cf13a 100644 --- a/iotlabcli/tests/common_parser_test.py +++ b/iotlabcli/tests/common_parser_test.py @@ -19,38 +19,38 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.parser.common module """ +"""Test the iotlabcli.parser.common module""" # pylint: disable=too-many-public-methods -import unittest -import sys import argparse +import sys +import unittest from iotlabcli.parser import common from iotlabcli.parser.common import print_result from iotlabcli.tests.my_mock import api_mock, api_mock_stop -from .c23 import HTTPError, patch, Mock, StringIO +from .c23 import HTTPError, Mock, StringIO, patch -BUILTIN = 'builtins' if sys.version_info[0] == 3 else '__builtin__' +BUILTIN = "builtins" if sys.version_info[0] == 3 else "__builtin__" class TestCommonParser(unittest.TestCase): - """ Test the iotlab.parser.common module """ + """Test the iotlab.parser.common module""" - @patch('iotlabcli.rest.Api.method') + @patch("iotlabcli.rest.Api.method") def test_sites_list(self, _method_get_sites): - """ Run get_sites method """ + """Run get_sites method""" _method_get_sites.return_value = { - "items": [{'site': 'grenoble'}, {'site': 'strasbourg'}] + "items": [{"site": "grenoble"}, {"site": "strasbourg"}] } - self.assertEqual(['grenoble', 'strasbourg'], common.sites_list()) - self.assertEqual(['grenoble', 'strasbourg'], common.sites_list()) + self.assertEqual(["grenoble", "strasbourg"], common.sites_list()) + self.assertEqual(["grenoble", "strasbourg"], common.sites_list()) self.assertEqual(1, _method_get_sites.call_count) def test_main_cli(self): - """ Run the main-cli function """ + """Run the main-cli function""" # Redefinition of function.side_effect type from XXX # pylint: disable=redefined-variable-type @@ -63,12 +63,12 @@ def test_main_cli(self): function.side_effect = IOError() self.assertRaises(SystemExit, common.main_cli, function, parser) - with patch('sys.stderr', sys.stdout): + with patch("sys.stderr", sys.stdout): # HTTPError, both cases - err = HTTPError(None, 401, 'msg', None, None) + err = HTTPError(None, 401, "msg", None, None) function.side_effect = err self.assertRaises(SystemExit, common.main_cli, function, parser) - err = HTTPError(None, 200, 'msg', None, None) + err = HTTPError(None, 200, "msg", None, None) function.side_effect = err self.assertRaises(SystemExit, common.main_cli, function, parser) @@ -80,19 +80,19 @@ def test_main_cli(self): self.assertRaises(SystemExit, common.main_cli, function, parser) def test_print_result_sigpipe(self): - """ Test BrokenPipe silent handling + """Test BrokenPipe silent handling When executing 'cli_cmd | grep -m1' code may raise a BrokenPipe error - message. We just want it to be silent """ + message. We just want it to be silent""" - result = {'ret': 0} - with patch(f'{BUILTIN}.print') as mock_print: + result = {"ret": 0} + with patch(f"{BUILTIN}.print") as mock_print: # Silent BrokenPipe - mock_print.side_effect = IOError(32, 'Broken pipe') + mock_print.side_effect = IOError(32, "Broken pipe") common.print_result(result) # Should raise other errors - mock_print.side_effect = IOError(28, 'No space left on device') + mock_print.side_effect = IOError(28, "No space left on device") self.assertRaises(IOError, common.print_result, result) def test_print_result(self): @@ -101,66 +101,73 @@ def test_print_result(self): @staticmethod def test_main_cli_jmespath_fmt(): - """ Run main_cli with --jmespath and --format options + """Run main_cli with --jmespath and --format options Test getting deployed nodes from 'iotlab-experiment get -p'""" - function = Mock(return_value={ - "deploymentresults": { - "0": [ + function = Mock( + return_value={ + "deploymentresults": { + "0": [ + "a8-5.grenoble.iot-lab.info", + "a8-6.grenoble.iot-lab.info", + "a8-7.grenoble.iot-lab.info", + "a8-8.grenoble.iot-lab.info", + "a8-9.grenoble.iot-lab.info", + ] + }, + "duration": 60, + "firmwareassociations": None, + "name": None, + "nodes": [ "a8-5.grenoble.iot-lab.info", "a8-6.grenoble.iot-lab.info", "a8-7.grenoble.iot-lab.info", "a8-8.grenoble.iot-lab.info", - "a8-9.grenoble.iot-lab.info" - ] - }, - "duration": 60, - "firmwareassociations": None, - "name": None, - "nodes": [ - "a8-5.grenoble.iot-lab.info", - "a8-6.grenoble.iot-lab.info", - "a8-7.grenoble.iot-lab.info", - "a8-8.grenoble.iot-lab.info", - "a8-9.grenoble.iot-lab.info" - ], - "profileassociations": None, - "profiles": None, - "reservation": None, - "state": "Running", - "type": "physical" - }) - - nodes_list_ret = ('a8-5.grenoble.iot-lab.info' - ' a8-6.grenoble.iot-lab.info' - ' a8-7.grenoble.iot-lab.info' - ' a8-8.grenoble.iot-lab.info' - ' a8-9.grenoble.iot-lab.info') + "a8-9.grenoble.iot-lab.info", + ], + "profileassociations": None, + "profiles": None, + "reservation": None, + "state": "Running", + "type": "physical", + } + ) + + nodes_list_ret = ( + "a8-5.grenoble.iot-lab.info" + " a8-6.grenoble.iot-lab.info" + " a8-7.grenoble.iot-lab.info" + " a8-8.grenoble.iot-lab.info" + " a8-9.grenoble.iot-lab.info" + ) # like iotlab-experiment get --print parser = common.base_parser() - args = ['--jmespath', 'deploymentresults."0"', '--format', '" ".join'] + args = ["--jmespath", 'deploymentresults."0"', "--format", '" ".join'] # No need to add 'exp-cli get -p' function is mocked - with patch(f'{BUILTIN}.print') as mock_print: + with patch(f"{BUILTIN}.print") as mock_print: common.main_cli(function, parser, args) mock_print.assert_called_with(nodes_list_ret) class TestNodeSelectionParser(unittest.TestCase): - """ Test the common '-l' '-e' options node selection parser """ + """Test the common '-l' '-e' options node selection parser""" + def tearDown(self): api_mock_stop() - @patch('iotlabcli.parser.common._get_experiment_nodes_list') + @patch("iotlabcli.parser.common._get_experiment_nodes_list") def test_list_nodes(self, g_nodes_list): - """ Run the different list_nodes cases """ + """Run the different list_nodes cases""" api = api_mock() g_nodes_list.return_value = [ - "m3-1.grenoble.iot-lab.info", "m3-2.grenoble.iot-lab.info", + "m3-1.grenoble.iot-lab.info", + "m3-2.grenoble.iot-lab.info", "m3-3.grenoble.iot-lab.info", - "m3-1.strasbourg.iot-lab.info", "m3-2.strasbourg.iot-lab.info", - "m3-3.strasbourg.iot-lab.info" + "m3-1.strasbourg.iot-lab.info", + "m3-2.strasbourg.iot-lab.info", + "m3-3.strasbourg.iot-lab.info", ] nodes_ll = [ @@ -175,19 +182,25 @@ def test_list_nodes(self, g_nodes_list): # Normal case, no external requests, only list of all provided nodes res = common.list_nodes(api, 123, nodes_ll=nodes_ll) - self.assertEqual(res, ["m3-1.grenoble.iot-lab.info", - "m3-2.grenoble.iot-lab.info", - "m3-1.strasbourg.iot-lab.info", - "m3-2.strasbourg.iot-lab.info"]) + self.assertEqual( + res, + [ + "m3-1.grenoble.iot-lab.info", + "m3-2.grenoble.iot-lab.info", + "m3-1.strasbourg.iot-lab.info", + "m3-2.strasbourg.iot-lab.info", + ], + ) self.assertFalse(g_nodes_list.called) res = common.list_nodes(api, 123, excl_nodes_ll=nodes_ll) - self.assertEqual(res, ["m3-3.grenoble.iot-lab.info", - "m3-3.strasbourg.iot-lab.info"]) + self.assertEqual( + res, ["m3-3.grenoble.iot-lab.info", "m3-3.strasbourg.iot-lab.info"] + ) self.assertTrue(g_nodes_list.called) def test__get_experiment_nodes_list(self): - """ Run get_experiment_nodes_list """ + """Run get_experiment_nodes_list""" api = api_mock( ret={ "items": [ @@ -198,51 +211,61 @@ def test__get_experiment_nodes_list(self): } ) # pylint: disable=protected-access - self.assertEqual(common._get_experiment_nodes_list(api, 3), - ["m3-1.grenoble.iot-lab.info", - "m3-2.grenoble.iot-lab.info", - "m3-3.grenoble.iot-lab.info"]) + self.assertEqual( + common._get_experiment_nodes_list(api, 3), + [ + "m3-1.grenoble.iot-lab.info", + "m3-2.grenoble.iot-lab.info", + "m3-3.grenoble.iot-lab.info", + ], + ) - @patch('iotlabcli.parser.common.check_site_with_server') + @patch("iotlabcli.parser.common.check_site_with_server") def test_nodes_list_from_str(self, _): - """ Run error case from test_nodes_list_from_str invalid string """ + """Run error case from test_nodes_list_from_str invalid string""" - self.assertRaises(argparse.ArgumentTypeError, - common.nodes_list_from_str, 'grenoble,m3_no_numbers') + self.assertRaises( + argparse.ArgumentTypeError, + common.nodes_list_from_str, + "grenoble,m3_no_numbers", + ) class TestSiteChecking(unittest.TestCase): """Test site checking functions.""" def setUp(self): - nodes_list = patch('iotlabcli.parser.common.sites_list').start() - nodes_list.return_value = ['grenoble', 'strasbourg', 'lille'] + nodes_list = patch("iotlabcli.parser.common.sites_list").start() + nodes_list.return_value = ["grenoble", "strasbourg", "lille"] def tearDown(self): patch.stopall() def test_check_site_with_server(self): """Test check_site_with_server.""" - common.check_site_with_server('grenoble') + common.check_site_with_server("grenoble") # Invalid site - self.assertRaises(argparse.ArgumentTypeError, - common.check_site_with_server, 'unknown') + self.assertRaises( + argparse.ArgumentTypeError, common.check_site_with_server, "unknown" + ) # sites list can be provided - common.check_site_with_server('strasourg', ['gre', 'strasourg', 'lil']) + common.check_site_with_server("strasourg", ["gre", "strasourg", "lil"]) def test_site_with_domain_checked(self): """Test site_with_domain_checked.""" - self.assertEqual(common.site_with_domain_checked('grenoble'), - 'grenoble.iot-lab.info') + self.assertEqual( + common.site_with_domain_checked("grenoble"), "grenoble.iot-lab.info" + ) - self.assertRaises(argparse.ArgumentTypeError, - common.site_with_domain_checked, 'unknown') + self.assertRaises( + argparse.ArgumentTypeError, common.site_with_domain_checked, "unknown" + ) # Override 'domain' - ret = common.site_with_domain_checked('grenoble', domain='localhost') - self.assertEqual(ret, 'grenoble.localhost') + ret = common.site_with_domain_checked("grenoble", domain="localhost") + self.assertEqual(ret, "grenoble.localhost") class TestHelpAction(unittest.TestCase): @@ -251,18 +274,17 @@ class TestHelpAction(unittest.TestCase): def test_help_action(self): """Test HelpAction method to add help.""" parser = argparse.ArgumentParser(add_help=True) - help_msg = 'Super Help Message for test.\n' - common.HelpAction.add_help(parser, '--help-test', 'help to test', - help_msg) + help_msg = "Super Help Message for test.\n" + common.HelpAction.add_help(parser, "--help-test", "help to test", help_msg) - with patch('sys.stdout', StringIO()) as stdout: + with patch("sys.stdout", StringIO()) as stdout: # Test Help - self.assertRaises(SystemExit, parser.parse_args, ['--help']) + self.assertRaises(SystemExit, parser.parse_args, ["--help"]) output = stdout.getvalue() - self.assertTrue(output.startswith('usage:')) - self.assertTrue(output.endswith(' --help-test help to test\n')) + self.assertTrue(output.startswith("usage:")) + self.assertTrue(output.endswith(" --help-test help to test\n")) - with patch('sys.stdout', StringIO()) as stdout: + with patch("sys.stdout", StringIO()) as stdout: # Test custom help - self.assertRaises(SystemExit, parser.parse_args, ['--help-test']) + self.assertRaises(SystemExit, parser.parse_args, ["--help-test"]) self.assertEqual(stdout.getvalue(), help_msg) diff --git a/iotlabcli/tests/experiment_parser_test.py b/iotlabcli/tests/experiment_parser_test.py index c3fa8d5..966eb83 100644 --- a/iotlabcli/tests/experiment_parser_test.py +++ b/iotlabcli/tests/experiment_parser_test.py @@ -19,19 +19,19 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.experiment_parser module """ +"""Test the iotlabcli.experiment_parser module""" # pylint: disable=too-many-public-methods # pylint: disable=invalid-name -import unittest import argparse +import unittest -from iotlabcli.tests import resource_file -from iotlabcli.tests.my_mock import MainMock import iotlabcli.parser.experiment as experiment_parser from iotlabcli import experiment +from iotlabcli.tests import resource_file +from iotlabcli.tests.my_mock import MainMock -from .c23 import patch, StringIO +from .c23 import StringIO, patch class TestMainInfoParser(MainMock): @@ -41,416 +41,536 @@ def setUp(self): MainMock.setUp(self) experiment.AliasNodes._alias = 0 # pylint:disable=protected-access - @patch('iotlabcli.experiment.info_experiment') + @patch("iotlabcli.experiment.info_experiment") def test_main_info_parser(self, info_exp): - """ Run experiment_parser.main.info """ + """Run experiment_parser.main.info""" info_exp.return_value = {} - experiment_parser.main(['info', '--list']) + experiment_parser.main(["info", "--list"]) info_exp.assert_called_with(self.api, False) - experiment_parser.main(['info', '--list-id', '--site', 'grenoble']) - info_exp.assert_called_with(self.api, True, site='grenoble') + experiment_parser.main(["info", "--list-id", "--site", "grenoble"]) + info_exp.assert_called_with(self.api, True, site="grenoble") # test deprecated info parser print usage - self.assertRaises(SystemExit, experiment_parser.main, ['info', '-h']) + self.assertRaises(SystemExit, experiment_parser.main, ["info", "-h"]) # Use other selections - experiment_parser.main(['info', '--list', '--archi', 'm3', - '--state', 'Alive']) - info_exp.assert_called_with(self.api, False, archi='m3', state='Alive') + experiment_parser.main(["info", "--list", "--archi", "m3", "--state", "Alive"]) + info_exp.assert_called_with(self.api, False, archi="m3", state="Alive") - experiment_parser.main(['info', '--list-id', '--site', 'lille', - '--archi', 'm3']) - info_exp.assert_called_with(self.api, True, site='lille', archi='m3') + experiment_parser.main( + ["info", "--list-id", "--site", "lille", "--archi", "m3"] + ) + info_exp.assert_called_with(self.api, True, site="lille", archi="m3") - @patch('iotlabcli.experiment.stop_experiment') + @patch("iotlabcli.experiment.stop_experiment") def test_main_stop_parser(self, stop_exp): - """ Run experiment_parser.main.stop """ + """Run experiment_parser.main.stop""" stop_exp.return_value = {} - experiment_parser.main(['stop']) + experiment_parser.main(["stop"]) stop_exp.assert_called_with(self.api, 123) - experiment_parser.main(['stop', '-i', '345']) + experiment_parser.main(["stop", "-i", "345"]) stop_exp.assert_called_with(self.api, 345) - @patch('iotlabcli.experiment.get_experiments_list') - @patch('iotlabcli.experiment.get_experiment') + @patch("iotlabcli.experiment.get_experiments_list") + @patch("iotlabcli.experiment.get_experiment") def test_main_get_parser(self, get_exp, get_exp_list): - """ Run experiment_parser.main.get """ + """Run experiment_parser.main.get""" get_exp.return_value = {} get_exp_list.return_value = {} - get_exp.return_value = {'start_date': "2018-01-19T14:54:15Z"} - experiment_parser.main(['get', '--start-time']) - get_exp.assert_called_with(self.api, 234, '') + get_exp.return_value = {"start_date": "2018-01-19T14:54:15Z"} + experiment_parser.main(["get", "--start-time"]) + get_exp.assert_called_with(self.api, 234, "") - get_exp.return_value = {'state': "Running"} - experiment_parser.main(['get', '--exp-state']) - get_exp.assert_called_with(self.api, 234, '') + get_exp.return_value = {"state": "Running"} + experiment_parser.main(["get", "--exp-state"]) + get_exp.assert_called_with(self.api, 234, "") # No value is treated inside get_exp.return_value = {} - experiment_parser.main(['get', '--id', '18', '--print']) - get_exp.assert_called_with(self.api, 18, '') + experiment_parser.main(["get", "--id", "18", "--print"]) + get_exp.assert_called_with(self.api, 18, "") - experiment_parser.main(['get', '--nodes']) - get_exp.assert_called_with(self.api, 123, 'nodes') + experiment_parser.main(["get", "--nodes"]) + get_exp.assert_called_with(self.api, 123, "nodes") - experiment_parser.main(['get', '--nodes-id']) - get_exp.assert_called_with(self.api, 123, 'nodes_ids') + experiment_parser.main(["get", "--nodes-id"]) + get_exp.assert_called_with(self.api, 123, "nodes_ids") # add deprecated option - experiment_parser.main(['get', '--resources']) - get_exp.assert_called_with(self.api, 123, 'nodes') + experiment_parser.main(["get", "--resources"]) + get_exp.assert_called_with(self.api, 123, "nodes") - experiment_parser.main(['get', '--resources-id']) - get_exp.assert_called_with(self.api, 123, 'nodes_ids') + experiment_parser.main(["get", "--resources-id"]) + get_exp.assert_called_with(self.api, 123, "nodes_ids") - experiment_parser.main(['get', '--archive']) - get_exp.assert_called_with(self.api, 123, 'data') + experiment_parser.main(["get", "--archive"]) + get_exp.assert_called_with(self.api, 123, "data") experiment_parser.main( - ['get', '--list', '--state=Running', '--limit=10', '--offset=50']) - get_exp_list.assert_called_with(self.api, 'Running', 10, 50) + ["get", "--list", "--state=Running", "--limit=10", "--offset=50"] + ) + get_exp_list.assert_called_with(self.api, "Running", 10, 50) - experiment_parser.main(['get', '--list']) + experiment_parser.main(["get", "--list"]) get_exp_list.assert_called_with(self.api, None, 0, 0) def test_parser_error(self): - """ Test some parser errors directly """ + """Test some parser errors directly""" parser = experiment_parser.parse_options() # Python3 didn't raised error without subcommand self.assertRaises(SystemExit, parser.parse_args, []) - @patch('iotlabcli.experiment.get_experiment') + @patch("iotlabcli.experiment.get_experiment") def test_experiment_get_start_time(self, get_exp): - """ Test the get-start-time parser """ - cmd = ['-u', 'test', '-p', 'password', 'get', '--start-time'] + """Test the get-start-time parser""" + cmd = ["-u", "test", "-p", "password", "get", "--start-time"] args = experiment_parser.parse_options().parse_args(cmd) - get_exp.return_value = {'start_date': "2018-01-19T14:54:15Z"} + get_exp.return_value = {"start_date": "2018-01-19T14:54:15Z"} ret = experiment_parser.get_experiment_parser(args) - get_exp.assert_called_with(self.api, 234, '') + get_exp.assert_called_with(self.api, 234, "") # don't expect anything on local time - self.assertTrue('2018' in ret['local_date']) + self.assertTrue("2018" in ret["local_date"]) # No start_time - get_exp.return_value = {'start_date': "1970-01-01T00:00:00Z"} + get_exp.return_value = {"start_date": "1970-01-01T00:00:00Z"} ret = experiment_parser.get_experiment_parser(args) - get_exp.assert_called_with(self.api, 234, '') - self.assertEqual('Unknown', ret['local_date']) + get_exp.assert_called_with(self.api, 234, "") + self.assertEqual("Unknown", ret["local_date"]) - @patch('iotlabcli.experiment.get_active_experiments') + @patch("iotlabcli.experiment.get_active_experiments") def test_get_experiments(self, get_active_experiments): """Run experiment_parser.main.get 'experiments'""" get_active_experiments.return_value = { - "Running": [11667], "Waiting": [11668], + "Running": [11667], + "Waiting": [11668], } - experiment_parser.main(['get', '-e']) + experiment_parser.main(["get", "-e"]) self.assertEqual(1, get_active_experiments.call_count) get_active_experiments.assert_called_with(self.api, running_only=True) get_active_experiments.reset_mock() - experiment_parser.main(['get', '--experiments', '--active']) + experiment_parser.main(["get", "--experiments", "--active"]) self.assertEqual(1, get_active_experiments.call_count) get_active_experiments.assert_called_with(self.api, running_only=False) get_active_experiments.reset_mock() - @patch('iotlabcli.experiment.submit_experiment') + @patch("iotlabcli.experiment.submit_experiment") def test_main_submit_parser(self, submit_exp): - """ Run experiment_parser.main.submit """ + """Run experiment_parser.main.submit""" submit_exp.return_value = {} # Physical tests - experiment_parser.main(['submit', '--name', 'exp_name', - '--duration', '20', '--reservation', '314159', - '--list', 'grenoble,m3,1-5']) + experiment_parser.main( + [ + "submit", + "--name", + "exp_name", + "--duration", + "20", + "--reservation", + "314159", + "--list", + "grenoble,m3,1-5", + ] + ) resources = [ experiment.exp_resources( - [f'm3-{i}.grenoble.iot-lab.info' for i in range(1, 6)], - None, - None + [f"m3-{i}.grenoble.iot-lab.info" for i in range(1, 6)], None, None ) ] - submit_exp.assert_called_with(self.api, 'exp_name', 20, resources, - 314159, False, None) + submit_exp.assert_called_with( + self.api, "exp_name", 20, resources, 314159, False, None + ) # print with simple options - nodes = [experiment.exp_resources(['m3-1.grenoble.iot-lab.info'])] - experiment_parser.main( - ['submit', '-p', '-d', '20', '-l', 'grenoble,m3,1']) - submit_exp.assert_called_with(self.api, None, 20, nodes, - None, True, None) + nodes = [experiment.exp_resources(["m3-1.grenoble.iot-lab.info"])] + experiment_parser.main(["submit", "-p", "-d", "20", "-l", "grenoble,m3,1"]) + submit_exp.assert_called_with(self.api, None, 20, nodes, None, True, None) # Alias tests - experiment_parser.main([ - 'submit', '-d', '20', - '-l', '1,archi=m3:at86rf231+site=grenoble,firmware.elf,profile1', - '-l', '2,archi=m3:at86rf231+site=grenoble,firmware.elf,profile1', - '-l', '3,archi=m3:at86rf231+site=grenoble,firmware_2.elf,profile2', - ]) + experiment_parser.main( + [ + "submit", + "-d", + "20", + "-l", + "1,archi=m3:at86rf231+site=grenoble,firmware.elf,profile1", + "-l", + "2,archi=m3:at86rf231+site=grenoble,firmware.elf,profile1", + "-l", + "3,archi=m3:at86rf231+site=grenoble,firmware_2.elf,profile2", + ] + ) experiment.AliasNodes._alias = 0 # pylint:disable=protected-access resources = [ experiment.exp_resources( - experiment.AliasNodes(1, 'grenoble', 'm3:at86rf231', False), - 'firmware.elf', 'profile1'), + experiment.AliasNodes(1, "grenoble", "m3:at86rf231", False), + "firmware.elf", + "profile1", + ), experiment.exp_resources( - experiment.AliasNodes(2, 'grenoble', 'm3:at86rf231', False), - 'firmware.elf', 'profile1'), + experiment.AliasNodes(2, "grenoble", "m3:at86rf231", False), + "firmware.elf", + "profile1", + ), experiment.exp_resources( - experiment.AliasNodes(3, 'grenoble', 'm3:at86rf231', False), - 'firmware_2.elf', 'profile2'), + experiment.AliasNodes(3, "grenoble", "m3:at86rf231", False), + "firmware_2.elf", + "profile2", + ), ] - submit_exp.assert_called_with(self.api, None, 20, resources, - None, False, None) + submit_exp.assert_called_with(self.api, None, 20, resources, None, False, None) - @patch('iotlabcli.experiment.submit_experiment') + @patch("iotlabcli.experiment.submit_experiment") def test_main_submit_parser_assocs(self, submit_exp): """Run experiment_parser.main.submit mobility.""" submit_exp.return_value = {} # Physical tests experiment_parser.main( - ['submit', '--name', 'exp_name', '--duration', '20', - '--list', - ('grenoble,m3,1,' - 'mobility=controlled,kernel=linux,firmware=m3.elf')]) + [ + "submit", + "--name", + "exp_name", + "--duration", + "20", + "--list", + ("grenoble,m3,1,mobility=controlled,kernel=linux,firmware=m3.elf"), + ] + ) - assocs = {'mobility': 'controlled', 'kernel': 'linux'} + assocs = {"mobility": "controlled", "kernel": "linux"} resources = [ - experiment.exp_resources(['m3-1.grenoble.iot-lab.info'], - 'm3.elf', None, **assocs) + experiment.exp_resources( + ["m3-1.grenoble.iot-lab.info"], "m3.elf", None, **assocs + ) ] - submit_exp.assert_called_with(self.api, 'exp_name', 20, resources, - None, False, None) + submit_exp.assert_called_with( + self.api, "exp_name", 20, resources, None, False, None + ) - @patch('iotlabcli.experiment.submit_experiment') + @patch("iotlabcli.experiment.submit_experiment") def test_main_submit_parser_site_assocs(self, submit_exp): """Run experiment_parser.main.submit site associations.""" - script_sh = resource_file('script.sh') - script_2_sh = resource_file('script_2.sh') - scriptconfig = resource_file('scriptconfig') + script_sh = resource_file("script.sh") + script_2_sh = resource_file("script_2.sh") + scriptconfig = resource_file("scriptconfig") submit_exp.return_value = {} # Groupped assocs - experiment_parser.main([ - 'submit', '--name', 'exp_name', '--duration', '20', - '--list', 'grenoble,m3,1', - '--list', 'strasbourg,m3,1', - '--site-association', f'grenoble,strasbourg,script={script_sh}', - ]) + experiment_parser.main( + [ + "submit", + "--name", + "exp_name", + "--duration", + "20", + "--list", + "grenoble,m3,1", + "--list", + "strasbourg,m3,1", + "--site-association", + f"grenoble,strasbourg,script={script_sh}", + ] + ) sites_assocs = [ - experiment.site_association('grenoble.iot-lab.info', - 'strasbourg.iot-lab.info', - script=script_sh), + experiment.site_association( + "grenoble.iot-lab.info", "strasbourg.iot-lab.info", script=script_sh + ), ] resources = [ - experiment.exp_resources(['m3-1.grenoble.iot-lab.info']), - experiment.exp_resources(['m3-1.strasbourg.iot-lab.info']), + experiment.exp_resources(["m3-1.grenoble.iot-lab.info"]), + experiment.exp_resources(["m3-1.strasbourg.iot-lab.info"]), ] - submit_exp.assert_called_with(self.api, 'exp_name', 20, resources, - None, False, sites_assocs) + submit_exp.assert_called_with( + self.api, "exp_name", 20, resources, None, False, sites_assocs + ) # Different assocs - experiment_parser.main([ - 'submit', '--name', 'exp_name', '--duration', '20', - '--list', 'grenoble,m3,1', - '--list', 'strasbourg,m3,1', - '--site-association', f'grenoble,script={script_sh},ipv6=2001::', - '--site-association', - f'strasbourg,script={script_2_sh},scriptconfig={scriptconfig}', - ]) + experiment_parser.main( + [ + "submit", + "--name", + "exp_name", + "--duration", + "20", + "--list", + "grenoble,m3,1", + "--list", + "strasbourg,m3,1", + "--site-association", + f"grenoble,script={script_sh},ipv6=2001::", + "--site-association", + f"strasbourg,script={script_2_sh},scriptconfig={scriptconfig}", + ] + ) sites_assocs = [ - experiment.site_association('grenoble.iot-lab.info', - script=script_sh, ipv6='2001::'), - experiment.site_association('strasbourg.iot-lab.info', - script=script_2_sh, - scriptconfig=scriptconfig), + experiment.site_association( + "grenoble.iot-lab.info", script=script_sh, ipv6="2001::" + ), + experiment.site_association( + "strasbourg.iot-lab.info", script=script_2_sh, scriptconfig=scriptconfig + ), ] resources = [ - experiment.exp_resources(['m3-1.grenoble.iot-lab.info']), - experiment.exp_resources(['m3-1.strasbourg.iot-lab.info']), + experiment.exp_resources(["m3-1.grenoble.iot-lab.info"]), + experiment.exp_resources(["m3-1.strasbourg.iot-lab.info"]), ] - submit_exp.assert_called_with(self.api, 'exp_name', 20, resources, - None, False, sites_assocs) + submit_exp.assert_called_with( + self.api, "exp_name", 20, resources, None, False, sites_assocs + ) def test_main_submit_parser_error(self): - """ Run experiment_parser.main.submit with error""" + """Run experiment_parser.main.submit with error""" # Physical tests self.assertRaises( - SystemExit, experiment_parser.main, - ['submit', '--duration', '20', - '-l', 'grenoble,m3,1-5,firmware, profile,toomanyvalues']) + SystemExit, + experiment_parser.main, + [ + "submit", + "--duration", + "20", + "-l", + "grenoble,m3,1-5,firmware, profile,toomanyvalues", + ], + ) # Physical tests self.assertRaises( - SystemExit, experiment_parser.main, - ['submit', '--duration', '20', '-l', 'grenoble,m3,100-1']) + SystemExit, + experiment_parser.main, + ["submit", "--duration", "20", "-l", "grenoble,m3,100-1"], + ) def test_main_submit_parser_site_assocs_error(self): - """ Run experiment_parser.main.submit with site assocs error""" + """Run experiment_parser.main.submit with site assocs error""" self.assertRaises( - SystemExit, experiment_parser.main, - ['submit', '--duration', '20', '-l', 'grenoble,m3,1', - '--site-association', 'invalid,script=test']) + SystemExit, + experiment_parser.main, + [ + "submit", + "--duration", + "20", + "-l", + "grenoble,m3,1", + "--site-association", + "invalid,script=test", + ], + ) self.assertRaises( - SystemExit, experiment_parser.main, - ['submit', '--duration', '20', '-l', 'grenoble,m3,1', - '--site-association', 'grenoble']) + SystemExit, + experiment_parser.main, + [ + "submit", + "--duration", + "20", + "-l", + "grenoble,m3,1", + "--site-association", + "grenoble", + ], + ) def test_main_submit_helps(self): """Run experiment_parser.main helps messages.""" - with patch('sys.stdout', StringIO()) as stdout: - self.assertRaises(SystemExit, experiment_parser.main, - ['submit', '--help-list']) + with patch("sys.stdout", StringIO()) as stdout: + self.assertRaises( + SystemExit, experiment_parser.main, ["submit", "--help-list"] + ) output = stdout.getvalue() - self.assertTrue(output.startswith('Resources list\n')) - with patch('sys.stdout', StringIO()) as stdout: - self.assertRaises(SystemExit, experiment_parser.main, - ['submit', '--help-site-association']) + self.assertTrue(output.startswith("Resources list\n")) + with patch("sys.stdout", StringIO()) as stdout: + self.assertRaises( + SystemExit, + experiment_parser.main, + ["submit", "--help-site-association"], + ) output = stdout.getvalue() - self.assertTrue(output.startswith('Site associations\n')) + self.assertTrue(output.startswith("Site associations\n")) - @patch('iotlabcli.experiment.wait_experiment') + @patch("iotlabcli.experiment.wait_experiment") def test_main_wait_parser(self, wait_exp): - """ Run experiment_parser.main.info """ + """Run experiment_parser.main.info""" wait_exp.return_value = {} - experiment_parser.main(['wait']) - wait_exp.assert_called_with(self.api, 234, 'Running', 5, - experiment.WAIT_TIMEOUT_DEFAULT, False) - experiment_parser.main(['wait', '--id', '42', - '--state', 'Launching,Running', '--step', '1', - '--timeout', '60']) - wait_exp.assert_called_with(self.api, 42, 'Launching,Running', 1, 60, - False) - experiment_parser.main(['wait', '--id', '42', - '--state', 'Launching,Running', '--step', '1', - '--timeout', '60', '--cancel-on-timeout']) - wait_exp.assert_called_with(self.api, 42, 'Launching,Running', 1, 60, - True) - - @patch('iotlabcli.experiment.load_experiment') + experiment_parser.main(["wait"]) + wait_exp.assert_called_with( + self.api, 234, "Running", 5, experiment.WAIT_TIMEOUT_DEFAULT, False + ) + experiment_parser.main( + [ + "wait", + "--id", + "42", + "--state", + "Launching,Running", + "--step", + "1", + "--timeout", + "60", + ] + ) + wait_exp.assert_called_with(self.api, 42, "Launching,Running", 1, 60, False) + experiment_parser.main( + [ + "wait", + "--id", + "42", + "--state", + "Launching,Running", + "--step", + "1", + "--timeout", + "60", + "--cancel-on-timeout", + ] + ) + wait_exp.assert_called_with(self.api, 42, "Launching,Running", 1, 60, True) + + @patch("iotlabcli.experiment.load_experiment") def test_main_load_parser(self, load_exp): - """ Run experiment_parser.main.load """ + """Run experiment_parser.main.load""" load_exp.return_value = {} - experiment_parser.main(['load', '-f', '../test_exp.json', - '-l', '~/firmware.elf', - '-l', './firmware_2.elf']) - load_exp.assert_called_with(self.api, '../test_exp.json', - ['~/firmware.elf', './firmware_2.elf']) + experiment_parser.main( + [ + "load", + "-f", + "../test_exp.json", + "-l", + "~/firmware.elf", + "-l", + "./firmware_2.elf", + ] + ) + load_exp.assert_called_with( + self.api, "../test_exp.json", ["~/firmware.elf", "./firmware_2.elf"] + ) # Deprecated, not documented anymore but keep it working - experiment_parser.main(['load', '-f', '../test_exp.json', - '-l', '~/firmware.elf,./firmware_2.elf']) - load_exp.assert_called_with(self.api, '../test_exp.json', - ['~/firmware.elf', './firmware_2.elf']) + experiment_parser.main( + ["load", "-f", "../test_exp.json", "-l", "~/firmware.elf,./firmware_2.elf"] + ) + load_exp.assert_called_with( + self.api, "../test_exp.json", ["~/firmware.elf", "./firmware_2.elf"] + ) - @patch('iotlabcli.experiment.reload_experiment') + @patch("iotlabcli.experiment.reload_experiment") def test_main_reload_parser(self, reload_exp): - """ Run experiment_parser.main.info """ + """Run experiment_parser.main.info""" reload_exp.return_value = {} - experiment_parser.main(['reload', '-i', '123']) + experiment_parser.main(["reload", "-i", "123"]) reload_exp.assert_called_with(self.api, 123, None, None) - experiment_parser.main(['reload', - '--id', '123', - '--duration', '120', - '--reservation', '314159']) + experiment_parser.main( + ["reload", "--id", "123", "--duration", "120", "--reservation", "314159"] + ) reload_exp.assert_called_with(self.api, 123, 120, 314159) # Exp id is required - self.assertRaises(SystemExit, experiment_parser.main, ['reload']) + self.assertRaises(SystemExit, experiment_parser.main, ["reload"]) - @patch('iotlabcli.experiment.script_experiment') + @patch("iotlabcli.experiment.script_experiment") def test_main_script_parser(self, script): - """ Run experiment_parser.main.run """ + """Run experiment_parser.main.run""" # Run script - script_sh = resource_file('script.sh') - scriptconfig = resource_file('scriptconfig') + script_sh = resource_file("script.sh") + scriptconfig = resource_file("scriptconfig") script.return_value = {} - experiment_parser.main(['script', '--run', - f'grenoble,script={script_sh}']) + experiment_parser.main(["script", "--run", f"grenoble,script={script_sh}"]) script.assert_called_with( - self.api, 123, 'run', - experiment.site_association('grenoble.iot-lab.info', - script=script_sh) + self.api, + 123, + "run", + experiment.site_association("grenoble.iot-lab.info", script=script_sh), ) # Multiple sites - experiment_parser.main(['script', '--run', - f'grenoble,strasbourg,script={script_sh}']) + experiment_parser.main( + ["script", "--run", f"grenoble,strasbourg,script={script_sh}"] + ) script.assert_called_with( - self.api, 123, 'run', - experiment.site_association('grenoble.iot-lab.info', - 'strasbourg.iot-lab.info', - script=script_sh) + self.api, + 123, + "run", + experiment.site_association( + "grenoble.iot-lab.info", "strasbourg.iot-lab.info", script=script_sh + ), ) # Multiple sites associations script.return_value = {} - experiment_parser.main([ - 'script', '--run', - f'grenoble,script={script_sh},scriptconfig={scriptconfig}', - f'strasbourg,script={script_sh}' - ]) + experiment_parser.main( + [ + "script", + "--run", + f"grenoble,script={script_sh},scriptconfig={scriptconfig}", + f"strasbourg,script={script_sh}", + ] + ) script.assert_called_with( - self.api, 123, 'run', - experiment.site_association('grenoble.iot-lab.info', - script=script_sh, - scriptconfig=scriptconfig), - experiment.site_association('strasbourg.iot-lab.info', - script=script_sh) + self.api, + 123, + "run", + experiment.site_association( + "grenoble.iot-lab.info", script=script_sh, scriptconfig=scriptconfig + ), + experiment.site_association("strasbourg.iot-lab.info", script=script_sh), ) # Error no arguments - self.assertRaises(SystemExit, experiment_parser.main, - ['script', '--run']) + self.assertRaises(SystemExit, experiment_parser.main, ["script", "--run"]) # Unknown assoc - self.assertRaises(SystemExit, experiment_parser.main, - ['script', '--run', - f'grenoble,script={script_sh},assoc=new']) + self.assertRaises( + SystemExit, + experiment_parser.main, + ["script", "--run", f"grenoble,script={script_sh},assoc=new"], + ) # Error no script - self.assertRaises(SystemExit, experiment_parser.main, - ['script', '--run', 'grenoble,assoc=test']) - self.assertRaises(SystemExit, experiment_parser.main, - ['script', '--run', 'assoc=test']) + self.assertRaises( + SystemExit, + experiment_parser.main, + ["script", "--run", "grenoble,assoc=test"], + ) + self.assertRaises( + SystemExit, experiment_parser.main, ["script", "--run", "assoc=test"] + ) # kill script - experiment_parser.main(['script', '--kill']) - script.assert_called_with(self.api, 123, 'kill') + experiment_parser.main(["script", "--kill"]) + script.assert_called_with(self.api, 123, "kill") - experiment_parser.main(['script', '--kill', 'grenoble']) - script.assert_called_with(self.api, 123, 'kill', - 'grenoble.iot-lab.info') + experiment_parser.main(["script", "--kill", "grenoble"]) + script.assert_called_with(self.api, 123, "kill", "grenoble.iot-lab.info") - experiment_parser.main(['script', '--kill', 'grenoble', 'strasbourg']) - script.assert_called_with(self.api, 123, 'kill', - 'grenoble.iot-lab.info', - 'strasbourg.iot-lab.info') + experiment_parser.main(["script", "--kill", "grenoble", "strasbourg"]) + script.assert_called_with( + self.api, 123, "kill", "grenoble.iot-lab.info", "strasbourg.iot-lab.info" + ) # Status script - experiment_parser.main(['script', '--status']) - script.assert_called_with(self.api, 123, 'status') + experiment_parser.main(["script", "--status"]) + script.assert_called_with(self.api, 123, "status") - experiment_parser.main(['script', '--status', 'grenoble']) - script.assert_called_with(self.api, 123, 'status', - 'grenoble.iot-lab.info') + experiment_parser.main(["script", "--status", "grenoble"]) + script.assert_called_with(self.api, 123, "status", "grenoble.iot-lab.info") - experiment_parser.main(['script', '--status', - 'grenoble', 'strasbourg']) - script.assert_called_with(self.api, 123, 'status', - 'grenoble.iot-lab.info', - 'strasbourg.iot-lab.info') + experiment_parser.main(["script", "--status", "grenoble", "strasbourg"]) + script.assert_called_with( + self.api, 123, "status", "grenoble.iot-lab.info", "strasbourg.iot-lab.info" + ) # pylint:disable=protected-access @@ -464,49 +584,50 @@ def _assert_assoc(self, params, expected): def _assert_fail_assoc(self, params): """Check given params fails.""" - self.assertRaises(ValueError, - experiment_parser._extract_associations, params) + self.assertRaises(ValueError, experiment_parser._extract_associations, params) def test_legacy_assocs(self): """Valid legacy mode associations.""" # Nothing given self._assert_assoc([], {}) - self._assert_assoc(['', ''], {}) + self._assert_assoc(["", ""], {}) # Only one - self._assert_assoc(['tutorial_m3.elf'], - {'firmware': 'tutorial_m3.elf'}) - self._assert_assoc(['', 'battery'], - {'profile': 'battery'}) + self._assert_assoc(["tutorial_m3.elf"], {"firmware": "tutorial_m3.elf"}) + self._assert_assoc(["", "battery"], {"profile": "battery"}) # Firmware and profile - self._assert_assoc(['tuto_m3.elf', 'battery'], - {'firmware': 'tuto_m3.elf', 'profile': 'battery'}) + self._assert_assoc( + ["tuto_m3.elf", "battery"], + {"firmware": "tuto_m3.elf", "profile": "battery"}, + ) def test_valid_new_assocs(self): """Valid new mode associations.""" # Nothing given - self._assert_assoc(['', '', 'firmware='], {}) + self._assert_assoc(["", "", "firmware="], {}) # With new value - self._assert_assoc(['tuto_m3.elf', 'battery', 'mobility=JHall'], - {'firmware': 'tuto_m3.elf', 'profile': 'battery', - 'mobility': 'JHall'}) - self._assert_assoc(['', 'battery', 'mobility=JHall'], - {'profile': 'battery', 'mobility': 'JHall'}) - self._assert_assoc(['', 'firmware=m3.elf'], - {'firmware': 'm3.elf'}) + self._assert_assoc( + ["tuto_m3.elf", "battery", "mobility=JHall"], + {"firmware": "tuto_m3.elf", "profile": "battery", "mobility": "JHall"}, + ) + self._assert_assoc( + ["", "battery", "mobility=JHall"], + {"profile": "battery", "mobility": "JHall"}, + ) + self._assert_assoc(["", "firmware=m3.elf"], {"firmware": "m3.elf"}) def test_invalid_legacy_assocs(self): """Invalid legacy mode associations.""" - self._assert_fail_assoc(['tuto.elf', 'battery', 'extra_unknown']) + self._assert_fail_assoc(["tuto.elf", "battery", "extra_unknown"]) def test_invalid_new_assocs(self): """Invalid new mode associations.""" - self._assert_fail_assoc(['mobility=JHall', 'm3.elf']) - self._assert_fail_assoc(['m3.elf', 'mobility=JHall', 'batt']) - self._assert_fail_assoc(['m3.elf', 'firmware=tuto.elf']) - self._assert_fail_assoc(['val aue']) + self._assert_fail_assoc(["mobility=JHall", "m3.elf"]) + self._assert_fail_assoc(["m3.elf", "mobility=JHall", "batt"]) + self._assert_fail_assoc(["m3.elf", "firmware=tuto.elf"]) + self._assert_fail_assoc(["val aue"]) class TestSiteAssociationParser(unittest.TestCase): @@ -516,43 +637,54 @@ def _test_site_assocs_from_str(self, assoc_str, *sites, **kwassocs): """Test if sites association is the expected one.""" self.assertEqual( experiment_parser.site_association_from_str(assoc_str), - experiment.site_association(*sites, **kwassocs)) + experiment.site_association(*sites, **kwassocs), + ) def test_site_assoctiations_from_str(self): """Test site_association_from_str.""" # Multi site self._test_site_assocs_from_str( - 'grenoble,strasbourg,script=iotlabcli/tests/script.sh', - *('grenoble.iot-lab.info', 'strasbourg.iot-lab.info'), - **{'script': 'iotlabcli/tests/script.sh'}) + "grenoble,strasbourg,script=iotlabcli/tests/script.sh", + *("grenoble.iot-lab.info", "strasbourg.iot-lab.info"), + **{"script": "iotlabcli/tests/script.sh"}, + ) # Multi associations self._test_site_assocs_from_str( - 'grenoble,script=iotlabcli/tests/script.sh,ipv6=2001::', - *('grenoble.iot-lab.info',), - **{'script': 'iotlabcli/tests/script.sh', 'ipv6': '2001::'}) + "grenoble,script=iotlabcli/tests/script.sh,ipv6=2001::", + *("grenoble.iot-lab.info",), + **{"script": "iotlabcli/tests/script.sh", "ipv6": "2001::"}, + ) def test_site_assoctiation_from_str_invalid(self): """Test invalid site_association_from_str.""" # Invalid association - self.assertRaises(argparse.ArgumentTypeError, - experiment_parser.site_association_from_str, - 'grenoble') + self.assertRaises( + argparse.ArgumentTypeError, + experiment_parser.site_association_from_str, + "grenoble", + ) # Invalid site - self.assertRaises(argparse.ArgumentTypeError, - experiment_parser.site_association_from_str, - 'invalid,script=test') + self.assertRaises( + argparse.ArgumentTypeError, + experiment_parser.site_association_from_str, + "invalid,script=test", + ) # No site - self.assertRaises(argparse.ArgumentTypeError, - experiment_parser.site_association_from_str, - 'script=test') + self.assertRaises( + argparse.ArgumentTypeError, + experiment_parser.site_association_from_str, + "script=test", + ) # Invalid args/kwargs with site after - self.assertRaises(argparse.ArgumentTypeError, - experiment_parser.site_association_from_str, - 'script=test,grenoble') + self.assertRaises( + argparse.ArgumentTypeError, + experiment_parser.site_association_from_str, + "script=test,grenoble", + ) class TestRunSiteAssociationParser(unittest.TestCase): @@ -562,41 +694,51 @@ def _test_run_site_assocs_from_str(self, assoc_str, *sites, **kwassocs): """Test if sites association is the expected one.""" self.assertEqual( experiment_parser.run_site_association_from_str(assoc_str), - experiment.site_association(*sites, **kwassocs)) + experiment.site_association(*sites, **kwassocs), + ) def test_run_site_assoctiations_from_str(self): """Test run_site_association_from_str.""" # Multi site self._test_run_site_assocs_from_str( - 'grenoble,strasbourg,script=iotlabcli/tests/script.sh', - *('grenoble.iot-lab.info', 'strasbourg.iot-lab.info'), - **{'script': 'iotlabcli/tests/script.sh'}) + "grenoble,strasbourg,script=iotlabcli/tests/script.sh", + *("grenoble.iot-lab.info", "strasbourg.iot-lab.info"), + **{"script": "iotlabcli/tests/script.sh"}, + ) # script and siteconfig in any order self._test_run_site_assocs_from_str( - ('grenoble' - ',scriptconfig=iotlabcli/tests/scriptconfig' - ',script=iotlabcli/tests/script.sh'), - *('grenoble.iot-lab.info',), - **{'script': 'iotlabcli/tests/script.sh', - 'scriptconfig': 'iotlabcli/tests/scriptconfig'}) + ( + "grenoble" + ",scriptconfig=iotlabcli/tests/scriptconfig" + ",script=iotlabcli/tests/script.sh" + ), + *("grenoble.iot-lab.info",), + **{ + "script": "iotlabcli/tests/script.sh", + "scriptconfig": "iotlabcli/tests/scriptconfig", + }, + ) def test_run_site_assoctiation_from_str_invalid(self): """Test invalid run_site_association_from_str.""" # Invalid association - self.assertRaises(argparse.ArgumentTypeError, - experiment_parser.run_site_association_from_str, - ('grenoble,script=iotlabcli/tests/script.sh' - ',ipv6=aaaa::1/64')) + self.assertRaises( + argparse.ArgumentTypeError, + experiment_parser.run_site_association_from_str, + ("grenoble,script=iotlabcli/tests/script.sh,ipv6=aaaa::1/64"), + ) # No 'association' - self.assertRaises(argparse.ArgumentTypeError, - experiment_parser.run_site_association_from_str, - 'grenoble') + self.assertRaises( + argparse.ArgumentTypeError, + experiment_parser.run_site_association_from_str, + "grenoble", + ) # 'scriptconfig' and no 'script' self.assertRaises( argparse.ArgumentTypeError, experiment_parser.run_site_association_from_str, - 'grenoble,scriptconfig=iotlabcli/tests/scriptconfig', + "grenoble,scriptconfig=iotlabcli/tests/scriptconfig", ) diff --git a/iotlabcli/tests/experiment_test.py b/iotlabcli/tests/experiment_test.py index 39fd160..f8819df 100644 --- a/iotlabcli/tests/experiment_test.py +++ b/iotlabcli/tests/experiment_test.py @@ -19,7 +19,7 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.experiment module """ +"""Test the iotlabcli.experiment module""" # pylint:disable=too-many-public-methods # pylint:disable=protected-access @@ -34,56 +34,44 @@ import json import unittest -from iotlabcli import experiment -from iotlabcli import rest -from iotlabcli import helpers -from iotlabcli import tests -from iotlabcli.tests.my_mock import CommandMock, API_RET, RequestRet +from iotlabcli import experiment, helpers, rest, tests +from iotlabcli.tests.my_mock import API_RET, CommandMock, RequestRet -from .c23 import mock, patch, mock_open +from .c23 import mock, mock_open, patch SCRIPTS = { - 'script.sh': (b'#! /bin/sh\n' - b'echo "script.sh"\n'), - 'script_2.sh': (b'#! /bin/sh\n' - b'echo "script_2.sh"\n'), + "script.sh": (b'#! /bin/sh\necho "script.sh"\n'), + "script_2.sh": (b'#! /bin/sh\necho "script_2.sh"\n'), } SCRIPTCONFIG = { - 'scriptconfig': b'PASSWORD=thesun\n', + "scriptconfig": b"PASSWORD=thesun\n", } class TestExperiment(unittest.TestCase): - """ Test iotlabcli.experiment module """ + """Test iotlabcli.experiment module""" def test_create_experiment(self): - """ Try creating an 'Experiment' object """ + """Try creating an 'Experiment' object""" - firmware_2 = 'firmware_2.elf' - nodes_list_2 = [ - f'm3-{num}.grenoble.iot-lab.info' for num in (0, 2, 4, 6, 8) - ] + firmware_2 = "firmware_2.elf" + nodes_list_2 = [f"m3-{num}.grenoble.iot-lab.info" for num in (0, 2, 4, 6, 8)] - firmware_3 = 'firmware_3.elf' - nodes_list_3 = [ - f'm3-{num}.grenoble.iot-lab.info' for num in (1, 3, 9, 27) - ] - exp_d_2 = experiment.exp_resources(nodes_list_2, firmware_2, 'prof2') - exp_d_3 = experiment.exp_resources(nodes_list_3, firmware_3, 'prof3') + firmware_3 = "firmware_3.elf" + nodes_list_3 = [f"m3-{num}.grenoble.iot-lab.info" for num in (1, 3, 9, 27)] + exp_d_2 = experiment.exp_resources(nodes_list_2, firmware_2, "prof2") + exp_d_3 = experiment.exp_resources(nodes_list_3, firmware_3, "prof3") # pylint:disable=protected-access - exp = experiment._Experiment('ExpName', 30, None) + exp = experiment._Experiment("ExpName", 30, None) exp.add_exp_resources(exp_d_2) exp.add_exp_resources(exp_d_3) - self.assertEqual(exp.type, 'physical') + self.assertEqual(exp.type, "physical") self.assertEqual( exp.nodes, - [ - f'm3-{num}.grenoble.iot-lab.info' - for num in (0, 1, 2, 3, 4, 6, 8, 9, 27) - ] + [f"m3-{num}.grenoble.iot-lab.info" for num in (0, 1, 2, 3, 4, 6, 8, 9, 27)], ) self.assertTrue(exp.firmwareassociations is not None) self.assertTrue(exp.profileassociations is not None) @@ -92,145 +80,172 @@ def test_create_experiment(self): class TestExperimentSubmit(CommandMock): - """ Test iotlabcli.experiment.submit_experiment """ + """Test iotlabcli.experiment.submit_experiment""" def test_experiment_submit_physical(self): - """ Run experiment_submit physical """ + """Run experiment_submit physical""" # Physical tests resources = [ experiment.exp_resources( - [f'm3-{i}.grenoble.iot-lab.info' for i in range(1, 6)] + [f"m3-{i}.grenoble.iot-lab.info" for i in range(1, 6)] ) ] - experiment.submit_experiment(self.api, 'exp_name', 20, resources, - start_time=314159) + experiment.submit_experiment( + self.api, "exp_name", 20, resources, start_time=314159 + ) call_dict = self.api.submit_experiment.call_args[0][0] expected = { - 'name': 'exp_name', - 'duration': 20, - 'type': 'physical', - 'nodes': [ - f'm3-{num}.grenoble.iot-lab.info' for num in range(1, 6) - ], - 'reservation': 314159, - 'profileassociations': None, - 'firmwareassociations': None, - 'associations': None, - 'siteassociations': None, - 'profiles': None, - 'mobilities': None, + "name": "exp_name", + "duration": 20, + "type": "physical", + "nodes": [f"m3-{num}.grenoble.iot-lab.info" for num in range(1, 6)], + "reservation": 314159, + "profileassociations": None, + "firmwareassociations": None, + "associations": None, + "siteassociations": None, + "profiles": None, + "mobilities": None, } - self.assertEqual(expected, json.loads(call_dict['new_exp.json'])) + self.assertEqual(expected, json.loads(call_dict["new_exp.json"])) # Try 'print', should return exp_dict - ret = experiment.submit_experiment(self.api, 'exp_name', 20, - resources, start_time=314159, - print_json=True) + ret = experiment.submit_experiment( + self.api, "exp_name", 20, resources, start_time=314159, print_json=True + ) self.assertEqual(ret.__dict__, expected) def test_experiment_submit_duplicated_basenames(self): - """ Run experiment_submit physical """ + """Run experiment_submit physical""" # Physical tests resources = [ experiment.exp_resources( - ['m3-1.grenoble.iot-lab.info'], - tests.resource_file('firmware.elf')), + ["m3-1.grenoble.iot-lab.info"], tests.resource_file("firmware.elf") + ), experiment.exp_resources( - ['m3-2.grenoble.iot-lab.info'], - tests.resource_file('other/firmware.elf')), + ["m3-2.grenoble.iot-lab.info"], + tests.resource_file("other/firmware.elf"), + ), ] - experiment.submit_experiment(self.api, 'exp_name', 20, resources, - start_time=314159) + experiment.submit_experiment( + self.api, "exp_name", 20, resources, start_time=314159 + ) call_dict = self.api.submit_experiment.call_args[0][0] expected = { - 'name': 'exp_name', - 'duration': 20, - 'type': 'physical', - 'nodes': [ - 'm3-1.grenoble.iot-lab.info', - 'm3-2.grenoble.iot-lab.info', + "name": "exp_name", + "duration": 20, + "type": "physical", + "nodes": [ + "m3-1.grenoble.iot-lab.info", + "m3-2.grenoble.iot-lab.info", ], - 'reservation': 314159, - 'profileassociations': None, - 'firmwareassociations': [ - {'firmwarename': - 'e77d9b8dcb84d1fcd21187b03eac74f1_firmware.elf', - 'nodes': ['m3-2.grenoble.iot-lab.info']}, - {'firmwarename': 'firmware.elf', - 'nodes': ['m3-1.grenoble.iot-lab.info']}, + "reservation": 314159, + "profileassociations": None, + "firmwareassociations": [ + { + "firmwarename": "e77d9b8dcb84d1fcd21187b03eac74f1_firmware.elf", + "nodes": ["m3-2.grenoble.iot-lab.info"], + }, + { + "firmwarename": "firmware.elf", + "nodes": ["m3-1.grenoble.iot-lab.info"], + }, ], - 'associations': None, - 'siteassociations': None, - 'profiles': None, - 'mobilities': None, + "associations": None, + "siteassociations": None, + "profiles": None, + "mobilities": None, } - self.assertEqual(expected, json.loads(call_dict['new_exp.json'])) + self.assertEqual(expected, json.loads(call_dict["new_exp.json"])) def test_experiment_submit_alias(self): - """ Run experiment_submit alias """ + """Run experiment_submit alias""" # Alias tests resources = [ experiment.exp_resources( - experiment.AliasNodes(1, 'grenoble', 'm3:at86rf231', False), - tests.resource_file('firmware.elf'), 'profile1'), + experiment.AliasNodes(1, "grenoble", "m3:at86rf231", False), + tests.resource_file("firmware.elf"), + "profile1", + ), experiment.exp_resources( - experiment.AliasNodes(2, 'grenoble', 'm3:at86rf231', False), - tests.resource_file('firmware.elf'), 'profile1'), + experiment.AliasNodes(2, "grenoble", "m3:at86rf231", False), + tests.resource_file("firmware.elf"), + "profile1", + ), experiment.exp_resources( - experiment.AliasNodes(4, 'grenoble', 'm3:at86rf231', False), - tests.resource_file('firmware_2.elf'), 'profile2'), + experiment.AliasNodes(4, "grenoble", "m3:at86rf231", False), + tests.resource_file("firmware_2.elf"), + "profile2", + ), ] experiment.submit_experiment(self.api, None, 20, resources) files_dict = self.api.submit_experiment.call_args[0][0] - exp_desc = json.loads(files_dict['new_exp.json']) + exp_desc = json.loads(files_dict["new_exp.json"]) expected = { - 'name': None, - 'duration': 20, - 'type': 'alias', - 'nodes': [ - {"alias": '1', "nbnodes": 1, "properties": { - "archi": "m3:at86rf231", "site": "grenoble", - "mobile": False - }}, - {"alias": '2', "nbnodes": 2, "properties": { - "archi": "m3:at86rf231", "site": "grenoble", - "mobile": False - }}, - {"alias": '3', "nbnodes": 4, "properties": { - "archi": "m3:at86rf231", "site": "grenoble", - "mobile": False - }}, + "name": None, + "duration": 20, + "type": "alias", + "nodes": [ + { + "alias": "1", + "nbnodes": 1, + "properties": { + "archi": "m3:at86rf231", + "site": "grenoble", + "mobile": False, + }, + }, + { + "alias": "2", + "nbnodes": 2, + "properties": { + "archi": "m3:at86rf231", + "site": "grenoble", + "mobile": False, + }, + }, + { + "alias": "3", + "nbnodes": 4, + "properties": { + "archi": "m3:at86rf231", + "site": "grenoble", + "mobile": False, + }, + }, ], - 'reservation': None, - 'profileassociations': [ - {'profilename': 'profile1', 'nodes': ['1', '2']}, - {'profilename': 'profile2', 'nodes': ['3']}, + "reservation": None, + "profileassociations": [ + {"profilename": "profile1", "nodes": ["1", "2"]}, + {"profilename": "profile2", "nodes": ["3"]}, ], - 'firmwareassociations': [ - {'firmwarename': 'firmware.elf', 'nodes': ['1', '2']}, - {'firmwarename': 'firmware_2.elf', 'nodes': ['3']} + "firmwareassociations": [ + {"firmwarename": "firmware.elf", "nodes": ["1", "2"]}, + {"firmwarename": "firmware_2.elf", "nodes": ["3"]}, ], - 'associations': None, - 'siteassociations': None, - 'profiles': None, - 'mobilities': None, + "associations": None, + "siteassociations": None, + "profiles": None, + "mobilities": None, } self.assertEqual(expected, exp_desc) - self.assertTrue('firmware.elf' in files_dict) + self.assertTrue("firmware.elf" in files_dict) def test_exp_alias_new_types(self): """Test submiting alias experiments with 'new' nodes types.""" resources = [ experiment.exp_resources( - experiment.AliasNodes(1, 'berlin', 'des:wifi-cc1100')), + experiment.AliasNodes(1, "berlin", "des:wifi-cc1100") + ), experiment.exp_resources( - experiment.AliasNodes(1, 'grenoble', 'custom:leonardo:')), + experiment.AliasNodes(1, "grenoble", "custom:leonardo:") + ), ] experiment.submit_experiment(self.api, None, 20, resources) self.assertEqual(1, self.api.submit_experiment.call_count) @@ -238,153 +253,156 @@ def test_exp_alias_new_types(self): def test_exp_physical_new_types(self): """Test submiting physical experiments with 'new' nodes types.""" resources = [ - experiment.exp_resources(['custom-1.grenoble.iot-lab.info']), - experiment.exp_resources(['berlin-1.berlin.iot-lab.info']), + experiment.exp_resources(["custom-1.grenoble.iot-lab.info"]), + experiment.exp_resources(["berlin-1.berlin.iot-lab.info"]), ] experiment.submit_experiment(self.api, None, 20, resources) self.assertEqual(1, self.api.submit_experiment.call_count) def test_exp_submit_associations(self): """Test experiment submission with associations.""" - nodes = ['m3-1.grenoble.iot-lab.info'] - assocs = {'mobility': 'controlled', 'kernel': 'linux'} + nodes = ["m3-1.grenoble.iot-lab.info"] + assocs = {"mobility": "controlled", "kernel": "linux"} resources = [ - experiment.exp_resources(nodes, - tests.resource_file('firmware.elf'), - None, **assocs), + experiment.exp_resources( + nodes, tests.resource_file("firmware.elf"), None, **assocs + ), ] experiment.submit_experiment(self.api, None, 20, resources) call_dict = self.api.submit_experiment.call_args[0][0] expected = { - 'name': None, - 'duration': 20, - 'type': 'physical', - 'nodes': nodes, - 'reservation': None, - 'profileassociations': None, - 'firmwareassociations': [ - {'firmwarename': 'firmware.elf', 'nodes': nodes}], - 'associations': { - 'mobility': [ - {'mobilityname': 'controlled', 'nodes': nodes}], - 'kernel': [ - {'kernelname': 'linux', 'nodes': nodes}], + "name": None, + "duration": 20, + "type": "physical", + "nodes": nodes, + "reservation": None, + "profileassociations": None, + "firmwareassociations": [{"firmwarename": "firmware.elf", "nodes": nodes}], + "associations": { + "mobility": [{"mobilityname": "controlled", "nodes": nodes}], + "kernel": [{"kernelname": "linux", "nodes": nodes}], }, - 'siteassociations': None, - 'profiles': None, - 'mobilities': None, + "siteassociations": None, + "profiles": None, + "mobilities": None, } - self.assertEqual(expected, json.loads(call_dict['new_exp.json'])) + self.assertEqual(expected, json.loads(call_dict["new_exp.json"])) def test_exp_submit_types_detect(self): - """ Try experiment submit types detection""" + """Try experiment submit types detection""" # Physical tests and Alias Nodes resources = [] - resources.append(experiment.exp_resources( - [f'm3-{i}.grenoble.iot-lab.info' for i in range(1, 6)]) + resources.append( + experiment.exp_resources( + [f"m3-{i}.grenoble.iot-lab.info" for i in range(1, 6)] + ) + ) + resources.append( + experiment.exp_resources( + experiment.AliasNodes(1, "grenoble", "m3:at86rf231", False), + tests.resource_file("firmware.elf"), + "profile1", + ) ) - resources.append(experiment.exp_resources( - experiment.AliasNodes(1, 'grenoble', 'm3:at86rf231', False), - tests.resource_file('firmware.elf'), 'profile1')) - self.assertRaises(ValueError, experiment.submit_experiment, - self.api, 'exp_name', 20, resources) + self.assertRaises( + ValueError, + experiment.submit_experiment, + self.api, + "exp_name", + 20, + resources, + ) def test_exp_submit_multiple_nodes(self): - """ Experiment submit with nodes specified multiple times """ + """Experiment submit with nodes specified multiple times""" - nodes = ['m3-1.grenoble.iot-lab.info'] + nodes = ["m3-1.grenoble.iot-lab.info"] resources = [] resources.append(experiment.exp_resources(nodes)) resources.append(experiment.exp_resources(nodes)) - self.assertRaises(ValueError, experiment.submit_experiment, - self.api, 'exp_name', 20, resources) + self.assertRaises( + ValueError, + experiment.submit_experiment, + self.api, + "exp_name", + 20, + resources, + ) def test_exp_submit_site_association(self): """Test experiment submission with site associations.""" - nodes = ['m3-1.grenoble.iot-lab.info', 'a8-1.strasbourg.iot-lab.info'] + nodes = ["m3-1.grenoble.iot-lab.info", "a8-1.strasbourg.iot-lab.info"] resources = [experiment.exp_resources(nodes)] site_assocs = [ experiment.site_association( - 'grenoble', - script=tests.resource_file('script.sh'), - ipv6='aaaa::/64', + "grenoble", + script=tests.resource_file("script.sh"), + ipv6="aaaa::/64", ), experiment.site_association( - 'strasbourg', - script=tests.resource_file('script_2.sh'), - scriptconfig=tests.resource_file('scriptconfig'), + "strasbourg", + script=tests.resource_file("script_2.sh"), + scriptconfig=tests.resource_file("scriptconfig"), ), ] - experiment.submit_experiment(self.api, None, 20, resources, - sites_assocs=site_assocs) + experiment.submit_experiment( + self.api, None, 20, resources, sites_assocs=site_assocs + ) files_dict = self.api.submit_experiment.call_args[0][0] expected = { - 'name': None, - 'duration': 20, - 'type': 'physical', - 'nodes': nodes, - 'reservation': None, - 'profileassociations': None, - 'firmwareassociations': None, - 'associations': None, - 'siteassociations': { - 'script': [ - { - 'scriptname': 'script.sh', - 'sites': ['grenoble'] - }, - { - 'scriptname': 'script_2.sh', - 'sites': ['strasbourg'] - }, + "name": None, + "duration": 20, + "type": "physical", + "nodes": nodes, + "reservation": None, + "profileassociations": None, + "firmwareassociations": None, + "associations": None, + "siteassociations": { + "script": [ + {"scriptname": "script.sh", "sites": ["grenoble"]}, + {"scriptname": "script_2.sh", "sites": ["strasbourg"]}, ], - 'scriptconfig': [ - { - 'scriptconfigname': 'scriptconfig', - 'sites': ['strasbourg'] - }, + "scriptconfig": [ + {"scriptconfigname": "scriptconfig", "sites": ["strasbourg"]}, ], - 'ipv6': [ - { - 'ipv6name': 'aaaa::/64', - 'sites': ['grenoble'] - }, + "ipv6": [ + {"ipv6name": "aaaa::/64", "sites": ["grenoble"]}, ], }, - 'profiles': None, - 'mobilities': None, + "profiles": None, + "mobilities": None, } - self.assertEqual(expected, json.loads(files_dict['new_exp.json'])) - self.assertEqual(files_dict['script.sh'], SCRIPTS['script.sh']) - self.assertEqual(files_dict['script_2.sh'], SCRIPTS['script_2.sh']) - self.assertEqual(files_dict['scriptconfig'], - SCRIPTCONFIG['scriptconfig']) - - def _read_file_for_load(self, file_path, *_): # flake8: noqa - """ read_file mock """ + self.assertEqual(expected, json.loads(files_dict["new_exp.json"])) + self.assertEqual(files_dict["script.sh"], SCRIPTS["script.sh"]) + self.assertEqual(files_dict["script_2.sh"], SCRIPTS["script_2.sh"]) + self.assertEqual(files_dict["scriptconfig"], SCRIPTCONFIG["scriptconfig"]) + + def _read_file_for_load(self, file_path, *_): # noqa: C901 + """read_file mock""" expected = self.expected res = None if file_path == experiment.EXP_FILENAME: res = json.dumps(expected) - elif file_path.endswith('.elf'): - res = 'elf32arm' - elif file_path.endswith('.sh'): - res = '#!/bin/sh' - elif file_path.endswith('config'): - res = 'KEY=value' + elif file_path.endswith(".elf"): + res = "elf32arm" + elif file_path.endswith(".sh"): + res = "#!/bin/sh" + elif file_path.endswith("config"): + res = "KEY=value" if res is None: raise ValueError(file_path) return res - @patch('iotlabcli.helpers.read_file') + @patch("iotlabcli.helpers.read_file") def test_experiment_load_min(self, read_file_mock): - """ Try experiment_load """ - node_fmt = 'm3-%u.grenoble.iot-lab.info' + """Try experiment_load""" + node_fmt = "m3-%u.grenoble.iot-lab.info" self.expected = { "duration": 20, "nodes": [node_fmt % num for num in range(1, 6)], @@ -392,18 +410,16 @@ def test_experiment_load_min(self, read_file_mock): } read_file_mock.side_effect = self._read_file_for_load - experiment.load_experiment( - self.api, experiment.EXP_FILENAME) + experiment.load_experiment(self.api, experiment.EXP_FILENAME) # read_file_calls _files = {_call[0][0] for _call in read_file_mock.call_args_list} - self.assertEqual(_files, - set([experiment.EXP_FILENAME])) + self.assertEqual(_files, set([experiment.EXP_FILENAME])) - @patch('iotlabcli.helpers.read_file') + @patch("iotlabcli.helpers.read_file") def test_experiment_load_full(self, read_file_mock): - """ Try experiment_load """ - node_fmt = 'm3-%u.grenoble.iot-lab.info' + """Try experiment_load""" + node_fmt = "m3-%u.grenoble.iot-lab.info" self.expected = { "name": None, "duration": 20, @@ -416,7 +432,7 @@ def test_experiment_load_full(self, read_file_mock): { "firmwarename": "firmware_2.elf", "nodes": [node_fmt % num for num in range(3, 5)], - } + }, ], "type": "physical", "profileassociations": None, @@ -424,96 +440,104 @@ def test_experiment_load_full(self, read_file_mock): } read_file_mock.side_effect = self._read_file_for_load - experiment.load_experiment( - self.api, experiment.EXP_FILENAME, ['firmware.elf']) + experiment.load_experiment(self.api, experiment.EXP_FILENAME, ["firmware.elf"]) # read_file_calls _files = {_call[0][0] for _call in read_file_mock.call_args_list} - self.assertEqual(_files, - set((experiment.EXP_FILENAME, - 'firmware.elf', 'firmware_2.elf'))) + self.assertEqual( + _files, set((experiment.EXP_FILENAME, "firmware.elf", "firmware_2.elf")) + ) self.assertRaises( ValueError, experiment.load_experiment, - self.api, experiment.EXP_FILENAME, - ['firmware.elf', 'firmware_2.elf', 'firmware_3.elf']) + self.api, + experiment.EXP_FILENAME, + ["firmware.elf", "firmware_2.elf", "firmware_3.elf"], + ) - self.assertRaises(ValueError, - self._read_file_for_load, 'invalid/file/path') + self.assertRaises(ValueError, self._read_file_for_load, "invalid/file/path") - @patch('iotlabcli.helpers.read_file') + @patch("iotlabcli.helpers.read_file") def test_experiment_load_with_script(self, read_file_mock): """Try experiment_load with script.""" self.expected = { "name": None, "duration": 20, - "nodes": ['m3-1.grenoble.iot-lab.info'], - "firmwareassociations": [{ - "firmwarename": "firmware.elf", - "nodes": ['m3-1.grenoble.iot-lab.info'], - }], + "nodes": ["m3-1.grenoble.iot-lab.info"], + "firmwareassociations": [ + { + "firmwarename": "firmware.elf", + "nodes": ["m3-1.grenoble.iot-lab.info"], + } + ], "type": "physical", "profileassociations": None, "reservation": None, "siteassociations": { - 'script': [{ - "scriptname": "script.sh", - "sites": ['grenoble'], - }], - 'scriptconfig': [{ - "scriptconfigname": "scriptconfig", - "sites": ['grenoble'], - }], + "script": [ + { + "scriptname": "script.sh", + "sites": ["grenoble"], + } + ], + "scriptconfig": [ + { + "scriptconfigname": "scriptconfig", + "sites": ["grenoble"], + } + ], }, - 'profiles': None, - 'mobilities': None, + "profiles": None, + "mobilities": None, } read_file_mock.side_effect = self._read_file_for_load - experiment.load_experiment( - self.api, experiment.EXP_FILENAME, ['script.sh']) + experiment.load_experiment(self.api, experiment.EXP_FILENAME, ["script.sh"]) # read_file_calls _files = {_call[0][0] for _call in read_file_mock.call_args_list} - self.assertEqual(_files, - set((experiment.EXP_FILENAME, - 'firmware.elf', 'script.sh', 'scriptconfig'))) + self.assertEqual( + _files, + set((experiment.EXP_FILENAME, "firmware.elf", "script.sh", "scriptconfig")), + ) class TestSiteAssociation(unittest.TestCase): """Test iotlabcli.experiment.site_association.""" + def test_site_assoctiation(self): """Test working site associations.""" # One site / assoc - assocs = experiment.site_association('grenoble', script='script.sh') - self.assertEqual(assocs, (('grenoble',), {'script': 'script.sh'})) + assocs = experiment.site_association("grenoble", script="script.sh") + self.assertEqual(assocs, (("grenoble",), {"script": "script.sh"})) # Multiple sites / asocs assocs = experiment.site_association( - 'grenoble', 'strasbourg', script='script.sh', ipv6='2001::') - self.assertEqual(assocs, (('grenoble', 'strasbourg'), - {'script': 'script.sh', 'ipv6': '2001::'})) - self.assertEqual(assocs.sites, ('grenoble', 'strasbourg')) - self.assertEqual(assocs.associations, - {'script': 'script.sh', 'ipv6': '2001::'}) + "grenoble", "strasbourg", script="script.sh", ipv6="2001::" + ) + self.assertEqual( + assocs, + (("grenoble", "strasbourg"), {"script": "script.sh", "ipv6": "2001::"}), + ) + self.assertEqual(assocs.sites, ("grenoble", "strasbourg")) + self.assertEqual(assocs.associations, {"script": "script.sh", "ipv6": "2001::"}) def test_site_assoctiations_error(self): """Test error in site associations.""" # No assoc - self.assertRaises(ValueError, - experiment.site_association, - 'grenoble') + self.assertRaises(ValueError, experiment.site_association, "grenoble") # Multiple times site - self.assertRaises(ValueError, - experiment.site_association, - 'grenoble', 'grenoble', - script='script.sh') + self.assertRaises( + ValueError, + experiment.site_association, + "grenoble", + "grenoble", + script="script.sh", + ) # No site - self.assertRaises(ValueError, - experiment.site_association, - script='script.sh') + self.assertRaises(ValueError, experiment.site_association, script="script.sh") class TestExperimentScript(CommandMock): @@ -523,97 +547,106 @@ def test_experiment_script_run(self): """Test running experiment script run.""" experiment.script_experiment( - self.api, 123, 'run', + self.api, + 123, + "run", experiment.site_association( - 'grenoble', script=tests.resource_file('script.sh'), - scriptconfig=tests.resource_file('scriptconfig')), + "grenoble", + script=tests.resource_file("script.sh"), + scriptconfig=tests.resource_file("scriptconfig"), + ), experiment.site_association( - 'strasbourg', script=tests.resource_file('script_2.sh')), + "strasbourg", script=tests.resource_file("script_2.sh") + ), ) - scripts_json = helpers.json_dumps({ - 'script': [ - {'scriptname': 'script.sh', 'sites': ['grenoble']}, - {'scriptname': 'script_2.sh', 'sites': ['strasbourg']}, - ], - 'scriptconfig': [ - {'scriptconfigname': 'scriptconfig', 'sites': ['grenoble']}, - ], - }) + scripts_json = helpers.json_dumps( + { + "script": [ + {"scriptname": "script.sh", "sites": ["grenoble"]}, + {"scriptname": "script_2.sh", "sites": ["strasbourg"]}, + ], + "scriptconfig": [ + {"scriptconfigname": "scriptconfig", "sites": ["grenoble"]}, + ], + } + ) expected_files = { - 'script.json': scripts_json, - 'script.sh': SCRIPTS['script.sh'], - 'script_2.sh': SCRIPTS['script_2.sh'], - 'scriptconfig': SCRIPTCONFIG['scriptconfig'], + "script.json": scripts_json, + "script.sh": SCRIPTS["script.sh"], + "script_2.sh": SCRIPTS["script_2.sh"], + "scriptconfig": SCRIPTCONFIG["scriptconfig"], } - self.api.script_command.assert_called_with(123, 'run', - files=expected_files) + self.api.script_command.assert_called_with(123, "run", files=expected_files) def test_script_run_error(self): """Test running experiment run with argument errors.""" # No sites/scripts - self.assertRaises(ValueError, - experiment.script_experiment, - self.api, 123, 'run') + self.assertRaises( + ValueError, experiment.script_experiment, self.api, 123, "run" + ) # site multiple times self.assertRaises( ValueError, - experiment.script_experiment, self.api, 123, 'run', + experiment.script_experiment, + self.api, + 123, + "run", experiment.site_association( - 'grenoble', script=tests.resource_file('script.sh')), + "grenoble", script=tests.resource_file("script.sh") + ), experiment.site_association( - 'grenoble', script=tests.resource_file('script_2.sh')) + "grenoble", script=tests.resource_file("script_2.sh") + ), ) def test_script_kill_status(self): """Test exp.script kill and status commands.""" # no sites - experiment.script_experiment(self.api, 123, 'kill') - self.api.script_command.assert_called_with(123, 'kill', json=[]) + experiment.script_experiment(self.api, 123, "kill") + self.api.script_command.assert_called_with(123, "kill", json=[]) - experiment.script_experiment(self.api, 123, 'status') - self.api.script_command.assert_called_with(123, 'status', json=[]) + experiment.script_experiment(self.api, 123, "status") + self.api.script_command.assert_called_with(123, "status", json=[]) # one site - experiment.script_experiment(self.api, 123, 'kill', 'grenoble') - self.api.script_command.assert_called_with(123, 'kill', - json=['grenoble']) + experiment.script_experiment(self.api, 123, "kill", "grenoble") + self.api.script_command.assert_called_with(123, "kill", json=["grenoble"]) - experiment.script_experiment(self.api, 123, 'status', 'grenoble') - self.api.script_command.assert_called_with(123, 'status', - json=['grenoble']) + experiment.script_experiment(self.api, 123, "status", "grenoble") + self.api.script_command.assert_called_with(123, "status", json=["grenoble"]) # multiple sites - experiment.script_experiment(self.api, 123, 'kill', - 'grenoble', 'strasbourg') + experiment.script_experiment(self.api, 123, "kill", "grenoble", "strasbourg") self.api.script_command.assert_called_with( - 123, 'kill', json=['grenoble', 'strasbourg']) + 123, "kill", json=["grenoble", "strasbourg"] + ) - experiment.script_experiment(self.api, 123, 'status', - 'grenoble', 'strasbourg') + experiment.script_experiment(self.api, 123, "status", "grenoble", "strasbourg") self.api.script_command.assert_called_with( - 123, 'status', json=['grenoble', 'strasbourg']) + 123, "status", json=["grenoble", "strasbourg"] + ) def test_script_invalid_cmd(self): """Test running experiment script with invalid command.""" - self.assertRaises(ValueError, - experiment.script_experiment, - self.api, 123, 'unknown_command') + self.assertRaises( + ValueError, experiment.script_experiment, self.api, 123, "unknown_command" + ) class TestExperimentStop(CommandMock): - """ Test iotlabcli.experiment.stop_experiment """ + """Test iotlabcli.experiment.stop_experiment""" def test_experiment_stop(self): - """ Test running stop experiment """ + """Test running stop experiment""" experiment.stop_experiment(self.api, 123) self.api.stop_experiment.assert_called_with(123) class TestExperimentReload(CommandMock): - """Test iotlabcli.experiment.reload_experiment """ + """Test iotlabcli.experiment.reload_experiment""" def test_experiment_reload(self): """Test reloading an experiment.""" @@ -622,31 +655,32 @@ def test_experiment_reload(self): self.api.reload_experiment.assert_called_with(123, exp_files) experiment.reload_experiment(self.api, 123, 120, 3124159) - exp_files = {'duration': '120', 'reservation': '3124159'} + exp_files = {"duration": "120", "reservation": "3124159"} self.api.reload_experiment.assert_called_with(123, exp_files) class TestExperimentGet(CommandMock): - """ Test iotlabcli.experiment.get_experiment """ + """Test iotlabcli.experiment.get_experiment""" def test_get_experiments_list(self): - """ Test experiment.get_experiments_list """ - experiment.get_experiments_list(self.api, 'Running', 100, 100) - self.api.get_experiments.assert_called_with('Running', 100, 100) + """Test experiment.get_experiments_list""" + experiment.get_experiments_list(self.api, "Running", 100, 100) + self.api.get_experiments.assert_called_with("Running", 100, 100) def test_get_experiment(self): - """ Test experiment.get_experiment """ + """Test experiment.get_experiment""" ret = experiment.get_experiment(self.api, 123) self.assertEqual(ret, API_RET) - ret = experiment.get_experiment(self.api, 123, option='nodes') + ret = experiment.get_experiment(self.api, 123, option="nodes") self.assertEqual(ret, API_RET) -@patch('iotlabcli.helpers.exps_by_states_dict') +@patch("iotlabcli.helpers.exps_by_states_dict") class TestGetActiveExperiment(unittest.TestCase): """Test iotlabcli.experiment.get_active_experiments.""" + def test_get_active_experiments(self, exps_by_states_dict): """Test iotlabcli.experiment.get_active_experiments.""" api = mock.Mock() @@ -655,107 +689,123 @@ def test_get_active_experiments(self, exps_by_states_dict): exps_by_states_dict.return_value = {} ret = experiment.get_active_experiments(api) self.assertEqual(ret, {}) - exps_by_states_dict.assert_called_with(api, ['Running']) + exps_by_states_dict.assert_called_with(api, ["Running"]) # Running only one experiment - exps_by_states_dict.return_value = {'Running': [12345]} + exps_by_states_dict.return_value = {"Running": [12345]} ret = experiment.get_active_experiments(api, running_only=True) - self.assertEqual(ret, {'Running': [12345]}) - exps_by_states_dict.assert_called_with(api, ['Running']) + self.assertEqual(ret, {"Running": [12345]}) + exps_by_states_dict.assert_called_with(api, ["Running"]) # Active experiments - exps_by_states_dict.return_value = {'Waiting': [10134, 10135], - 'Running': [10130]} + exps_by_states_dict.return_value = { + "Waiting": [10134, 10135], + "Running": [10130], + } ret = experiment.get_active_experiments(api, running_only=False) - self.assertEqual(ret, {'Waiting': [10134, 10135], - 'Running': [10130]}) + self.assertEqual(ret, {"Waiting": [10134, 10135], "Running": [10130]}) exps_by_states_dict.assert_called_with( - api, ['Running', 'Launching', 'toLaunch', 'Waiting']) + api, ["Running", "Launching", "toLaunch", "Waiting"] + ) -@patch('iotlabcli.experiment.stop_experiment') -@patch('iotlabcli.experiment.get_experiment') +@patch("iotlabcli.experiment.stop_experiment") +@patch("iotlabcli.experiment.get_experiment") class TestExperimentWait(CommandMock): - """ Test iotlabcli.experiment.wait_experiment """ + """Test iotlabcli.experiment.wait_experiment""" + wait_ret = [] def _get_exp(self, *args, **kwargs): # pylint: disable=unused-argument - """ Get experiment state - Return values from config list, or 'waiting' """ + """Get experiment state + Return values from config list, or 'waiting'""" try: state = self.wait_ret.pop(0) except IndexError: - state = 'Waiting' - return {'state': state} + state = "Waiting" + return {"state": state} def test_wait_experiment(self, get_exp, stop_exp): - """ Test the wait_experiment function """ - self.wait_ret = ['Waiting', 'Waiting', 'toLaunch', 'Launching', - 'Running'] + """Test the wait_experiment function""" + self.wait_ret = ["Waiting", "Waiting", "toLaunch", "Launching", "Running"] get_exp.side_effect = self._get_exp # simple ret = experiment.wait_experiment(self.api, 123, step=0) - self.assertEqual('Running', ret) + self.assertEqual("Running", ret) # Error before Running - self.wait_ret = ['Waiting', 'toLaunch', 'Launching', 'Error'] - self.assertRaises(RuntimeError, experiment.wait_experiment, - self.api, 123, step=0) + self.wait_ret = ["Waiting", "toLaunch", "Launching", "Error"] + self.assertRaises( + RuntimeError, experiment.wait_experiment, self.api, 123, step=0 + ) # Timeout self.wait_ret = [] - self.assertRaises(RuntimeError, experiment.wait_experiment, - self.api, 123, step=0.1, timeout=0.5) + self.assertRaises( + RuntimeError, + experiment.wait_experiment, + self.api, + 123, + step=0.1, + timeout=0.5, + ) # Timeout + cancel on timeout self.wait_ret = [] - self.assertRaises(RuntimeError, experiment.wait_experiment, - self.api, 123, step=0.1, timeout=0.5, - cancel_on_timeout=True) + self.assertRaises( + RuntimeError, + experiment.wait_experiment, + self.api, + 123, + step=0.1, + timeout=0.5, + cancel_on_timeout=True, + ) stop_exp.assert_called_with(self.api, 123) class TestExperimentGetWriteExpArchive(unittest.TestCase): - """ Test iotlabcli.experiment.get archive """ + """Test iotlabcli.experiment.get archive""" - @patch('iotlabcli.experiment._write_experiment_archive') + @patch("iotlabcli.experiment._write_experiment_archive") def test_get_experiment(self, w_exp_archive): - """ Test experiment.get_experiment """ - arch_content = '\x42\x69' + """Test experiment.get_experiment""" + arch_content = "\x42\x69" ret_val = RequestRet(content=arch_content, status_code=200) - patch('requests.request', return_value=ret_val).start() - api = rest.Api('user', 'password') + patch("requests.request", return_value=ret_val).start() + api = rest.Api("user", "password") - ret = experiment.get_experiment(api, 123, option='data') - self.assertEqual(ret, 'Written') + ret = experiment.get_experiment(api, 123, option="data") + self.assertEqual(ret, "Written") # encode, not real but easier for tests - w_exp_archive.assert_called_with(123, arch_content.encode('utf-8')) + w_exp_archive.assert_called_with(123, arch_content.encode("utf-8")) class TestExperimentInfo(CommandMock): - """ Test iotlabcli.experiment.info_experiment """ + """Test iotlabcli.experiment.info_experiment""" def test_info_experiment(self): """Test experiment.get_nodes.""" experiment.info_experiment(self.api) self.api.get_nodes.assert_called_with(False, None) - experiment.info_experiment(self.api, list_id=True, site='grenoble') - self.api.get_nodes.assert_called_with(True, 'grenoble') + experiment.info_experiment(self.api, list_id=True, site="grenoble") + self.api.get_nodes.assert_called_with(True, "grenoble") - experiment.info_experiment(self.api, site='grenoble', archi='m3') - self.api.get_nodes.assert_called_with(False, 'grenoble', archi='m3') + experiment.info_experiment(self.api, site="grenoble", archi="m3") + self.api.get_nodes.assert_called_with(False, "grenoble", archi="m3") class TestWriteExperimentArchive(unittest.TestCase): - """ Test iotlabcli.experiment._write_experiment_archive """ + """Test iotlabcli.experiment._write_experiment_archive""" + @staticmethod def test_write_experiment_archive(): - """ Test experiment._write_experiment_archive """ - open_name = 'iotlabcli.experiment.open' - exp_data = 'binary_content' + """Test experiment._write_experiment_archive""" + open_name = "iotlabcli.experiment.open" + exp_data = "binary_content" m_open = mock_open() with patch(open_name, m_open, create=True): experiment._write_experiment_archive(123, exp_data) diff --git a/iotlabcli/tests/helpers_test.py b/iotlabcli/tests/helpers_test.py index 92c82ec..5c5bff0 100644 --- a/iotlabcli/tests/helpers_test.py +++ b/iotlabcli/tests/helpers_test.py @@ -19,11 +19,11 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.helpers module """ +"""Test the iotlabcli.helpers module""" # pylint:disable=too-many-public-methods,redefined-outer-name -import unittest import sys +import unittest import warnings import pytest @@ -35,58 +35,64 @@ class TestHelpers(unittest.TestCase): - """ Test the iotlabcli.helpers module """ + """Test the iotlabcli.helpers module""" def test_exp_by_states(self): - """ Run the 'exp_by_states' function """ - api = my_mock.api_mock({"items": [{'state': 'Waiting', 'id': 10134}, - {'state': 'Waiting', 'id': 10135}, - {'state': 'Running', 'id': 10130}]}) + """Run the 'exp_by_states' function""" + api = my_mock.api_mock( + { + "items": [ + {"state": "Waiting", "id": 10134}, + {"state": "Waiting", "id": 10135}, + {"state": "Running", "id": 10130}, + ] + } + ) states_d = helpers.exps_by_states_dict(api, helpers.ACTIVE_STATES) - self.assertEqual( - {'Waiting': [10134, 10135], 'Running': [10130]}, states_d) + self.assertEqual({"Waiting": [10134, 10135], "Running": [10130]}, states_d) my_mock.api_mock_stop() def test_get_current_experiment(self): - """ Test get_current_experiment """ + """Test get_current_experiment""" api = None - with patch('iotlabcli.helpers.exps_by_states_dict') as exps_m: - exps_m.return_value = {'Running': [234]} + with patch("iotlabcli.helpers.exps_by_states_dict") as exps_m: + exps_m.return_value = {"Running": [234]} self.assertEqual(123, helpers.get_current_experiment(api, 123)) self.assertEqual(234, helpers.get_current_experiment(api, None)) # also return 'active' experiments - exps_m.return_value = {'Waiting': [234]} - self.assertEqual(234, helpers.get_current_experiment( - api, None, running_only=False)) + exps_m.return_value = {"Waiting": [234]} + self.assertEqual( + 234, helpers.get_current_experiment(api, None, running_only=False) + ) - @patch('sys.stderr', sys.stdout) - @patch('iotlabcli.helpers.read_file') + @patch("sys.stderr", sys.stdout) + @patch("iotlabcli.helpers.read_file") def test_read_custom_api_url(self, read_file_mock): - """ Test API URL reading """ + """Test API URL reading""" # No config - with patch('os.getenv', return_value=None): + with patch("os.getenv", return_value=None): read_file_mock.side_effect = IOError() self.assertTrue(helpers.read_custom_api_url() is None) # Only File - with patch('os.getenv', return_value=None): + with patch("os.getenv", return_value=None): read_file_mock.side_effect = None - read_file_mock.return_value = 'API_URL_CUSTOM' - self.assertEqual('API_URL_CUSTOM', helpers.read_custom_api_url()) + read_file_mock.return_value = "API_URL_CUSTOM" + self.assertEqual("API_URL_CUSTOM", helpers.read_custom_api_url()) # Only env variable - with patch('os.getenv', return_value='API_URL_2'): + with patch("os.getenv", return_value="API_URL_2"): read_file_mock.side_effect = IOError() - self.assertEqual('API_URL_2', helpers.read_custom_api_url()) + self.assertEqual("API_URL_2", helpers.read_custom_api_url()) # File priority over env variable - with patch('os.getenv', return_value='API_URL_2'): + with patch("os.getenv", return_value="API_URL_2"): read_file_mock.side_effect = None - read_file_mock.return_value = 'API_URL_CUSTOM' - self.assertEqual('API_URL_CUSTOM', helpers.read_custom_api_url()) + read_file_mock.return_value = "API_URL_CUSTOM" + self.assertEqual("API_URL_CUSTOM", helpers.read_custom_api_url()) def test_deprecate_command(self): """Test command deprecation.""" @@ -95,14 +101,16 @@ def test_deprecate_command(self): self.assertEqual(len(warn), 1) self.assertTrue(issubclass(warn[-1].category, DeprecationWarning)) - self.assertTrue(helpers.DEPRECATION_MESSAGE.format(old_cmd="old", - new_cmd="new") - in str(warn[-1].message)) + self.assertTrue( + helpers.DEPRECATION_MESSAGE.format(old_cmd="old", new_cmd="new") + in str(warn[-1].message) + ) # Test FilesDict class -@pytest.fixture(scope='function') + +@pytest.fixture(scope="function") def file_dict(): """fixture to provide an empty FilesDict""" return helpers.FilesDict() @@ -116,52 +124,52 @@ def test_none_input(file_dict): def test_can_overwrite(file_dict): """we can overwrite a value inside the FileDict if same value""" - file_dict['test'] = 'value' - file_dict['test'] = 'value' + file_dict["test"] = "value" + file_dict["test"] = "value" - assert file_dict['test'] == 'value' + assert file_dict["test"] == "value" def test_dict_eq(file_dict): """a FileDict should be equatable to a dict""" - file_dict['test'] = 'value' - file_dict['test2'] = 'value' - file_dict['test3'] = 'value3' + file_dict["test"] = "value" + file_dict["test2"] = "value" + file_dict["test3"] = "value3" - assert file_dict == {'test': 'value', 'test3': 'value3', 'test2': 'value'} + assert file_dict == {"test": "value", "test3": "value3", "test2": "value"} def test_no_overwrite(file_dict): """Test FilesDict values overriding.""" - file_dict['a'] = 1 + file_dict["a"] = 1 # Can re-add same value - file_dict['a'] = 1 + file_dict["a"] = 1 # Cannot add a different valu - file_dict['b'] = 2 + file_dict["b"] = 2 with pytest.raises(ValueError): - file_dict['b'] = 3 + file_dict["b"] = 3 # Check dict - assert file_dict == {'a': 1, 'b': 2} + assert file_dict == {"a": 1, "b": 2} -@patch('iotlabcli.helpers.read_file') +@patch("iotlabcli.helpers.read_file") def test_same_basename_files(read_file, file_dict): """Test FilesDict add_file methods when given two files with same basename.""" files = { - 'a/1.elf': b'ELF32_1', - '~/iot-lab/b/1.elf': b'ELF32_2', - '/tmp/c/1.elf': b'ELF32_3', + "a/1.elf": b"ELF32_1", + "~/iot-lab/b/1.elf": b"ELF32_2", + "/tmp/c/1.elf": b"ELF32_3", } keys = [ - '1.elf', - '56d50b0f4f3216dd962e8911ec91c062_1.elf', - 'c7b7412e59348fb71ca8ec6619284f3f_1.elf' + "1.elf", + "56d50b0f4f3216dd962e8911ec91c062_1.elf", + "c7b7412e59348fb71ca8ec6619284f3f_1.elf", ] def _read_file(name, *_): @@ -172,50 +180,50 @@ def _read_file(name, *_): read_file.side_effect = _read_file # Add some files - input_files_dict = {'firmware': 'a/1.elf'} - inserted = file_dict.add_files_from_dict(('firmware',), input_files_dict) - assert inserted['firmware'] == keys[0] - assert file_dict == {keys[0]: b'ELF32_1'} + input_files_dict = {"firmware": "a/1.elf"} + inserted = file_dict.add_files_from_dict(("firmware",), input_files_dict) + assert inserted["firmware"] == keys[0] + assert file_dict == {keys[0]: b"ELF32_1"} # Add some other files - input_files_dict = {'firmware': '~/iot-lab/b/1.elf'} - inserted = file_dict.add_files_from_dict(('firmware',), input_files_dict) + input_files_dict = {"firmware": "~/iot-lab/b/1.elf"} + inserted = file_dict.add_files_from_dict(("firmware",), input_files_dict) - assert inserted['firmware'] == keys[1] - assert file_dict == {keys[0]: b'ELF32_1', - keys[1]: b'ELF32_2'} + assert inserted["firmware"] == keys[1] + assert file_dict == {keys[0]: b"ELF32_1", keys[1]: b"ELF32_2"} - input_files_dict = {'firmware': '/tmp/c/1.elf'} - inserted = file_dict.add_files_from_dict(('firmware',), input_files_dict) + input_files_dict = {"firmware": "/tmp/c/1.elf"} + inserted = file_dict.add_files_from_dict(("firmware",), input_files_dict) - assert inserted['firmware'] == keys[2] - assert file_dict == {keys[0]: b'ELF32_1', - keys[1]: b'ELF32_2', - keys[2]: b'ELF32_3'} + assert inserted["firmware"] == keys[2] + assert file_dict == {keys[0]: b"ELF32_1", keys[1]: b"ELF32_2", keys[2]: b"ELF32_3"} -@patch('iotlabcli.helpers.read_file') +@patch("iotlabcli.helpers.read_file") def test_add_file_method(read_file, file_dict): """Test FilesDict add_file methods.""" + def _read_file(name, *_): """Read file mock.""" files = { - '1.elf': b'ELF32_1', - '2.elf': b'ELF32_2', - 'prof.json': b'{}', + "1.elf": b"ELF32_1", + "2.elf": b"ELF32_2", + "prof.json": b"{}", } return files[name] + read_file.side_effect = _read_file # Add some files - input_files_dict = {'firmware': '1.elf', 'profile': 'prof.json'} - file_dict.add_files_from_dict(['firmware', 'profile', 'script'], - input_files_dict) - assert file_dict == {'1.elf': b'ELF32_1', 'prof.json': b'{}'} + input_files_dict = {"firmware": "1.elf", "profile": "prof.json"} + file_dict.add_files_from_dict(["firmware", "profile", "script"], input_files_dict) + assert file_dict == {"1.elf": b"ELF32_1", "prof.json": b"{}"} # Add some other files - input_files_dict = {'firmware': '2.elf', 'scriptconfig': 'otherfile'} - file_dict.add_files_from_dict(['firmware', 'profile', 'script'], - input_files_dict) - assert file_dict == {'1.elf': b'ELF32_1', '2.elf': b'ELF32_2', - 'prof.json': b'{}', } + input_files_dict = {"firmware": "2.elf", "scriptconfig": "otherfile"} + file_dict.add_files_from_dict(["firmware", "profile", "script"], input_files_dict) + assert file_dict == { + "1.elf": b"ELF32_1", + "2.elf": b"ELF32_2", + "prof.json": b"{}", + } diff --git a/iotlabcli/tests/main_parser_test.py b/iotlabcli/tests/main_parser_test.py index 01adf3d..8177dc4 100644 --- a/iotlabcli/tests/main_parser_test.py +++ b/iotlabcli/tests/main_parser_test.py @@ -19,10 +19,10 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.experiment_parser module """ +"""Test the iotlabcli.experiment_parser module""" + import argparse import subprocess - import sys import pytest @@ -32,29 +32,31 @@ from .c23 import patch, version_info -@pytest.mark.parametrize('entry', - ['auth', 'experiment', - 'node', 'profile', - 'robot', 'status']) +@pytest.mark.parametrize( + "entry", ["auth", "experiment", "node", "profile", "robot", "status"] +) def test_main_parser(entry): - """ test main parser dispatching """ - with patch(f'iotlabcli.parser.{entry}.main') as entrypoint_func: - main_parser.main([entry, '-i', '123']) - entrypoint_func.assert_called_with(['-i', '123']) - - -@pytest.mark.parametrize('argv,exc', - argvalues=((['iotlab'], None), - (['iotlab', 'help'], None), - (['iotlab', '--help'], SystemExit),), - ids=['no subcommand', - 'help subcommand', - '--help argument']) + """test main parser dispatching""" + with patch(f"iotlabcli.parser.{entry}.main") as entrypoint_func: + main_parser.main([entry, "-i", "123"]) + entrypoint_func.assert_called_with(["-i", "123"]) + + +@pytest.mark.parametrize( + "argv,exc", + argvalues=( + (["iotlab"], None), + (["iotlab", "help"], None), + (["iotlab", "--help"], SystemExit), + ), + ids=["no subcommand", "help subcommand", "--help argument"], +) def test_help(argv, exc): """Tests that the help entrypoints print the help.""" - with patch.object(argparse.ArgumentParser, 'print_help') \ - as argparser_print_help, \ - patch.object(sys, 'argv', argv): + with ( + patch.object(argparse.ArgumentParser, "print_help") as argparser_print_help, + patch.object(sys, "argv", argv), + ): if exc: with pytest.raises(exc): main_parser.main() @@ -85,84 +87,81 @@ def test_help(argv, exc): def with_ssh_tools(function): """decorator, skip test if iotlabsshcli is not installed""" - return pytest.mark.skipif( - iotlabsshcli is None, - reason="iotlabsshcli is required")(function) + return pytest.mark.skipif(iotlabsshcli is None, reason="iotlabsshcli is required")( + function + ) def with_aggregator_tools(function): """decorator, skip test if iotlabaggregator is not installed""" return pytest.mark.skipif( - iotlabaggregator is None, - reason="iotlabaggregator is required")(function) + iotlabaggregator is None, reason="iotlabaggregator is required" + )(function) def with_oml_plot_tools(function): """decorator, skip test if oml_plot_tools not installed""" return pytest.mark.skipif( - oml_plot_tools is None, - reason="oml_plot_tools is required")(function) + oml_plot_tools is None, reason="oml_plot_tools is required" + )(function) def without_tools(function): """decorator, skip test if any tool is installed""" return pytest.mark.skipif( - oml_plot_tools is not None or - iotlabaggregator is not None or - iotlabsshcli is not None, - reason="No tools should be installed")(function) + oml_plot_tools is not None + or iotlabaggregator is not None + or iotlabsshcli is not None, + reason="No tools should be installed", + )(function) @with_aggregator_tools -@pytest.mark.parametrize('entry', ('serial', 'sniffer')) +@pytest.mark.parametrize("entry", ("serial", "sniffer")) def test_aggregator_main(entry): - """ test main parser dispatching for subcommands""" + """test main parser dispatching for subcommands""" - with patch('iotlabcli.parser.main.iotlabaggregator') as mocked_module: + with patch("iotlabcli.parser.main.iotlabaggregator") as mocked_module: mocked_main = getattr(mocked_module, entry).main - main_parser.main([entry, '-i', '123']) - mocked_main.assert_called_with(['-i', '123']) + main_parser.main([entry, "-i", "123"]) + mocked_main.assert_called_with(["-i", "123"]) - assert subprocess.check_call(['iotlab', entry, '--help']) == 0 + assert subprocess.check_call(["iotlab", entry, "--help"]) == 0 @with_oml_plot_tools -@pytest.mark.parametrize('entry', ('traj', 'radio', 'consum')) +@pytest.mark.parametrize("entry", ("traj", "radio", "consum")) def test_oml_main(entry): - """ test main parser dispatching for subcommands""" + """test main parser dispatching for subcommands""" - with patch('iotlabcli.parser.main.oml_plot_tools') as mocked_module: + with patch("iotlabcli.parser.main.oml_plot_tools") as mocked_module: mocked_main = getattr(mocked_module, entry).main - main_parser.main(['plot', entry, '-i', '123']) - mocked_main.assert_called_with(['-i', '123']) + main_parser.main(["plot", entry, "-i", "123"]) + mocked_main.assert_called_with(["-i", "123"]) - assert subprocess.check_call(['iotlab', 'plot', '--help']) == 0 - assert subprocess.check_call(['iotlab', 'plot', entry, '--help']) == 0 + assert subprocess.check_call(["iotlab", "plot", "--help"]) == 0 + assert subprocess.check_call(["iotlab", "plot", entry, "--help"]) == 0 @with_ssh_tools def test_ssh_main(): - """ test main parser dispatching for subcommands""" + """test main parser dispatching for subcommands""" - with patch('iotlabcli.parser.main.iotlabsshcli') as mocked_module: + with patch("iotlabcli.parser.main.iotlabsshcli") as mocked_module: mocked_main = mocked_module.parser.open_linux_parser.main - main_parser.main(['ssh', '-i', '123']) - mocked_main.assert_called_with(['-i', '123']) + main_parser.main(["ssh", "-i", "123"]) + mocked_main.assert_called_with(["-i", "123"]) - assert subprocess.check_call(['iotlab', 'ssh', '--help']) == 0 + assert subprocess.check_call(["iotlab", "ssh", "--help"]) == 0 @without_tools def test_main_parser_no_tools(): """tools subcommands returning""" - pytest.raises(SystemExit, - lambda: main_parser.main(['ssh'])) - pytest.raises(SystemExit, - lambda: main_parser.main(['serial'])) - pytest.raises(SystemExit, - lambda: main_parser.main(['sniffer'])) - pytest.raises(SystemExit, - lambda: main_parser.main(['plot'])) + pytest.raises(SystemExit, lambda: main_parser.main(["ssh"])) + pytest.raises(SystemExit, lambda: main_parser.main(["serial"])) + pytest.raises(SystemExit, lambda: main_parser.main(["sniffer"])) + pytest.raises(SystemExit, lambda: main_parser.main(["plot"])) @with_aggregator_tools diff --git a/iotlabcli/tests/my_mock.py b/iotlabcli/tests/my_mock.py index b15b4cd..38e3402 100644 --- a/iotlabcli/tests/my_mock.py +++ b/iotlabcli/tests/my_mock.py @@ -19,57 +19,57 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" common TestCase class for testing commands """ +"""common TestCase class for testing commands""" +import json import sys import unittest -import json from iotlabcli import experiment -from iotlabcli.rest import Api from iotlabcli.helpers import json_dumps +from iotlabcli.rest import Api -from .c23 import patch, Mock - +from .c23 import Mock, patch API_RET = {"result": "test"} -class RequestRet(): # pylint:disable=too-few-public-methods - """ Mock of Request return value """ +class RequestRet: # pylint:disable=too-few-public-methods + """Mock of Request return value""" def __init__(self, status_code, content, headers=None): self.status_code = status_code - self.content = content.encode('utf-8') + self.content = content.encode("utf-8") self.headers = headers - self.text = self.content.decode('utf-8') + self.text = self.content.decode("utf-8") def json(self): - """ Load output as JSON """ + """Load output as JSON""" return json.loads(self.text) def api_mock(ret=None): - """ Return a mock of an api object + """Return a mock of an api object returned value for api methods will be 'ret' parameter or API_RET """ ret = ret or API_RET ret_val = RequestRet(content=json_dumps(ret), status_code=200) # HTTP OK - patch('requests.request', return_value=ret_val).start() - api_class = patch('iotlabcli.rest.Api').start() - api_class.return_value = Mock(wraps=Api('user', 'password')) + patch("requests.request", return_value=ret_val).start() + api_class = patch("iotlabcli.rest.Api").start() + api_class.return_value = Mock(wraps=Api("user", "password")) return api_class.return_value def api_mock_stop(): - """ Stop all patches started by api_mock. - Actually it stops everything but not a problem """ + """Stop all patches started by api_mock. + Actually it stops everything but not a problem""" patch.stopall() # pylint: disable=too-many-public-methods class CommandMock(unittest.TestCase): - """ Common mock needed for testing commands """ + """Common mock needed for testing commands""" + def setUp(self): self.api = api_mock() experiment.AliasNodes._alias = 0 # pylint:disable=protected-access @@ -80,20 +80,26 @@ def tearDown(self): class MainMock(unittest.TestCase): - """ Common mock needed for testing main function of parsers """ + """Common mock needed for testing main function of parsers""" + def setUp(self): self.api = api_mock() - patch('sys.stderr', sys.stdout).start() - patch('iotlabcli.parser.common.sites_list', Mock( - return_value=['grenoble', 'strasbourg', 'euratech'])).start() + patch("sys.stderr", sys.stdout).start() + patch( + "iotlabcli.parser.common.sites_list", + Mock(return_value=["grenoble", "strasbourg", "euratech"]), + ).start() + + patch( + "iotlabcli.auth.get_user_credentials", + Mock(return_value=("username", "password")), + ).start() - patch('iotlabcli.auth.get_user_credentials', - Mock(return_value=('username', 'password'))).start() + def get_exp(_a, x, running_only=True): + return x if x is not None else (123 if running_only else 234) - get_exp = (lambda a, x, running_only=True: - x if x is not None else (123 if running_only else 234)) - patch('iotlabcli.helpers.get_current_experiment', get_exp).start() + patch("iotlabcli.helpers.get_current_experiment", get_exp).start() def tearDown(self): api_mock_stop() diff --git a/iotlabcli/tests/node_parser_test.py b/iotlabcli/tests/node_parser_test.py index 79788d1..3fc3d79 100644 --- a/iotlabcli/tests/node_parser_test.py +++ b/iotlabcli/tests/node_parser_test.py @@ -19,7 +19,7 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.parser.node module """ +"""Test the iotlabcli.parser.node module""" import iotlabcli.parser.node as node_parser from iotlabcli.tests.my_mock import MainMock @@ -30,113 +30,121 @@ # pylint: disable=too-few-public-methods -@patch('iotlabcli.node.node_command') -@patch('iotlabcli.parser.common.list_nodes') +@patch("iotlabcli.node.node_command") +@patch("iotlabcli.parser.common.list_nodes") class TestMainNodeParser(MainMock): - """ Test iotlab-node main parser """ + """Test iotlab-node main parser""" + def test_main(self, list_nodes, node_command): - """ Run the parser.node.main function """ - node_command.return_value = {'result': 'test'} + """Run the parser.node.main function""" + node_command.return_value = {"result": "test"} list_nodes.return_value = [] # start - args = ['--start'] + args = ["--start"] node_parser.main(args) list_nodes.assert_called_with(self.api, 123, None, None) - node_command.assert_called_with(self.api, 'start', 123, [], None) + node_command.assert_called_with(self.api, "start", 123, [], None) # stop - args = ['--stop'] + args = ["--stop"] node_parser.main(args) list_nodes.assert_called_with(self.api, 123, None, None) - node_command.assert_called_with(self.api, 'stop', 123, [], None) + node_command.assert_called_with(self.api, "stop", 123, [], None) # debug-start - args = ['--debug-start'] + args = ["--debug-start"] node_parser.main(args) list_nodes.assert_called_with(self.api, 123, None, None) - node_command.assert_called_with(self.api, 'debug-start', 123, [], None) + node_command.assert_called_with(self.api, "debug-start", 123, [], None) # debug-stop - args = ['--debug-stop'] + args = ["--debug-stop"] node_parser.main(args) list_nodes.assert_called_with(self.api, 123, None, None) - node_command.assert_called_with(self.api, 'debug-stop', 123, [], None) + node_command.assert_called_with(self.api, "debug-stop", 123, [], None) # Reset command with many arguments - args = ['--reset', '-l', 'grenoble,m3,1-2', '-l', 'grenoble,m3,3'] - list_nodes.return_value = ['m3-1', 'm3-2', 'm3-3'] # simplify + args = ["--reset", "-l", "grenoble,m3,1-2", "-l", "grenoble,m3,3"] + list_nodes.return_value = ["m3-1", "m3-2", "m3-3"] # simplify node_parser.main(args) list_nodes.assert_called_with( - self.api, 123, - [['m3-1.grenoble.iot-lab.info', 'm3-2.grenoble.iot-lab.info'], - ['m3-3.grenoble.iot-lab.info']], None) + self.api, + 123, + [ + ["m3-1.grenoble.iot-lab.info", "m3-2.grenoble.iot-lab.info"], + ["m3-3.grenoble.iot-lab.info"], + ], + None, + ) node_command.assert_called_with( - self.api, 'reset', 123, ['m3-1', 'm3-2', 'm3-3'], None) + self.api, "reset", 123, ["m3-1", "m3-2", "m3-3"], None + ) def test_main_update(self, list_nodes, node_command): """Run the parser.node.main function regarding update.""" - node_command.return_value = {'result': 'test'} + node_command.return_value = {"result": "test"} list_nodes.return_value = [] # update with exclude list - args = ['--flash', 'tp.elf', '-e', 'grenoble,m3,1-2'] - list_nodes.return_value = ['m3-3'] # simplify + args = ["--flash", "tp.elf", "-e", "grenoble,m3,1-2"] + list_nodes.return_value = ["m3-3"] # simplify node_parser.main(args) list_nodes.assert_called_with( - self.api, 123, None, - [['m3-1.grenoble.iot-lab.info', 'm3-2.grenoble.iot-lab.info']]) - node_command.assert_called_with( - self.api, 'flash', 123, ['m3-3'], 'tp.elf') + self.api, + 123, + None, + [["m3-1.grenoble.iot-lab.info", "m3-2.grenoble.iot-lab.info"]], + ) + node_command.assert_called_with(self.api, "flash", 123, ["m3-3"], "tp.elf") # update idle node_command.reset_mock() - args = ['--flash-idle'] + args = ["--flash-idle"] list_nodes.return_value = [] node_parser.main(args) list_nodes.assert_called_with(self.api, 123, None, None) - node_command.assert_called_with(self.api, 'flash-idle', 123, [], None) + node_command.assert_called_with(self.api, "flash-idle", 123, [], None) def test_deprecated_option(self, list_nodes, node_command): """Run the parser.node.main with deprecated option.""" - node_command.return_value = {'result': 'test'} - args = ['--update', 'tp.elf'] - list_nodes.return_value = ['m3-3'] + node_command.return_value = {"result": "test"} + args = ["--update", "tp.elf"] + list_nodes.return_value = ["m3-3"] node_parser.main(args) - node_command.assert_called_with( - self.api, 'flash', 123, ['m3-3'], 'tp.elf') + node_command.assert_called_with(self.api, "flash", 123, ["m3-3"], "tp.elf") # update idle node_command.reset_mock() - args = ['--update-idle'] + args = ["--update-idle"] list_nodes.return_value = [] node_parser.main(args) list_nodes.assert_called_with(self.api, 123, None, None) - node_command.assert_called_with(self.api, 'flash-idle', 123, [], None) + node_command.assert_called_with(self.api, "flash-idle", 123, [], None) def test_main_profile(self, list_nodes, node_command): """Run the parser.node.main function regarding profile.""" - node_command.return_value = {'result': 'test'} + node_command.return_value = {"result": "test"} list_nodes.return_value = [] # profile update - args = ['--profile', 'profm3'] + args = ["--profile", "profm3"] node_parser.main(args) list_nodes.assert_called_with(self.api, 123, None, None) - node_command.assert_called_with(self.api, 'profile', 123, [], 'profm3') + node_command.assert_called_with(self.api, "profile", 123, [], "profm3") # profile-load node_command.reset_mock() - args = ['--profile-load', 'profile.json'] + args = ["--profile-load", "profile.json"] list_nodes.return_value = [] node_parser.main(args) list_nodes.assert_called_with(self.api, 123, None, None) - node_command.assert_called_with(self.api, 'profile-load', 123, [], - 'profile.json') + node_command.assert_called_with( + self.api, "profile-load", 123, [], "profile.json" + ) # profile-reset node_command.reset_mock() - args = ['--profile-reset'] + args = ["--profile-reset"] list_nodes.return_value = [] node_parser.main(args) list_nodes.assert_called_with(self.api, 123, None, None) - node_command.assert_called_with(self.api, 'profile-reset', 123, [], - None) + node_command.assert_called_with(self.api, "profile-reset", 123, [], None) diff --git a/iotlabcli/tests/node_test.py b/iotlabcli/tests/node_test.py index 6caac7a..ff0de3d 100644 --- a/iotlabcli/tests/node_test.py +++ b/iotlabcli/tests/node_test.py @@ -19,7 +19,7 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.node module """ +"""Test the iotlabcli.node module""" # pylint: disable=too-many-public-methods # Issues with 'mock' @@ -34,94 +34,96 @@ class TestNode(unittest.TestCase): - """ Test the 'iotlabcli.node' module """ + """Test the 'iotlabcli.node' module""" + def tearDown(self): my_mock.api_mock_stop() - @patch('iotlabcli.helpers.read_file') + @patch("iotlabcli.helpers.read_file") def test_node_command(self, read_file_mock): - """ Test 'node_command' """ + """Test 'node_command'""" nodes_list = ["m3-1", "m3-2", "m3-3"] - read_file_mock.return_value = 'file_data' + read_file_mock.return_value = "file_data" api = my_mock.api_mock() api.reset_mock() - res = node.node_command(api, 'start', 123, nodes_list) + res = node.node_command(api, "start", 123, nodes_list) self.assertEqual(my_mock.API_RET, res) - api.node_command.assert_called_with('start', 123, nodes_list) + api.node_command.assert_called_with("start", 123, nodes_list) api.reset_mock() - res = node.node_command(api, 'stop', 123, nodes_list) + res = node.node_command(api, "stop", 123, nodes_list) self.assertEqual(my_mock.API_RET, res) - api.node_command.assert_called_with('stop', 123, nodes_list) + api.node_command.assert_called_with("stop", 123, nodes_list) api.reset_mock() - res = node.node_command(api, 'reset', 123, nodes_list) + res = node.node_command(api, "reset", 123, nodes_list) self.assertEqual(my_mock.API_RET, res) - api.node_command.assert_called_with('reset', 123, nodes_list) + api.node_command.assert_called_with("reset", 123, nodes_list) api.reset_mock() - res = node.node_command(api, 'profile', 123, nodes_list, 'p_m3') + res = node.node_command(api, "profile", 123, nodes_list, "p_m3") self.assertEqual(my_mock.API_RET, res) - api.node_command.assert_called_with('monitoring', 123, nodes_list, - 'p_m3') + api.node_command.assert_called_with("monitoring", 123, nodes_list, "p_m3") api.reset_mock() - res = node.node_command(api, 'flash', 123, nodes_list, - '~/../filename.elf') + res = node.node_command(api, "flash", 123, nodes_list, "~/../filename.elf") self.assertEqual(my_mock.API_RET, res) self.assertEqual(1, api.node_update.call_count) - api.node_update.assert_called_with(123, { - "filename.elf": "file_data", - 'nodes.json': '["m3-1", "m3-2", "m3-3"]', - }) + api.node_update.assert_called_with( + 123, + { + "filename.elf": "file_data", + "nodes.json": '["m3-1", "m3-2", "m3-3"]', + }, + ) api.reset_mock() - res = node.node_command(api, 'flash', 123, nodes_list, - '~/../filename.bin') + res = node.node_command(api, "flash", 123, nodes_list, "~/../filename.bin") self.assertEqual(my_mock.API_RET, res) self.assertEqual(1, api.node_update.call_count) api.node_update.assert_called_once() args = api.node_update.call_args.args kwargs = api.node_update.call_args.kwargs assert args[0] == 123 - assert 'experiment.json' in args[1] - data_dict = json.loads(args[1]['experiment.json']) - assert 'offset' in data_dict - assert data_dict['offset'] == 0 - assert 'nodes' in data_dict - assert data_dict['nodes'] == ["m3-1", "m3-2", "m3-3"] - assert kwargs == {'binary': True} + assert "experiment.json" in args[1] + data_dict = json.loads(args[1]["experiment.json"]) + assert "offset" in data_dict + assert data_dict["offset"] == 0 + assert "nodes" in data_dict + assert data_dict["nodes"] == ["m3-1", "m3-2", "m3-3"] + assert kwargs == {"binary": True} # no firmware for update command - self.assertRaises(AssertionError, node.node_command, - api, 'flash', 123, nodes_list) + self.assertRaises( + AssertionError, node.node_command, api, "flash", 123, nodes_list + ) api.reset_mock() - res = node.node_command(api, 'flash-idle', 123, nodes_list) + res = node.node_command(api, "flash-idle", 123, nodes_list) self.assertEqual(my_mock.API_RET, res) - api.node_command.assert_called_with('flash-idle', 123, nodes_list) + api.node_command.assert_called_with("flash-idle", 123, nodes_list) # profile-load - read_file_mock.return_value = '{profilejson}' # no local content check + read_file_mock.return_value = "{profilejson}" # no local content check json_file = { - 'profile.json': '{profilejson}', - 'nodes.json': '["m3-1", "m3-2", "m3-3"]', + "profile.json": "{profilejson}", + "nodes.json": '["m3-1", "m3-2", "m3-3"]', } api.reset_mock() - res = node.node_command(api, 'profile-load', 123, nodes_list, - 'profile.json') + res = node.node_command(api, "profile-load", 123, nodes_list, "profile.json") self.assertEqual(my_mock.API_RET, res) api.node_profile_load.assert_called_with(123, json_file) # profile-load without profile - self.assertRaises(AssertionError, node.node_command, - api, 'profile-load', 123, nodes_list) + self.assertRaises( + AssertionError, node.node_command, api, "profile-load", 123, nodes_list + ) # profile-reset api.reset_mock() - res = node.node_command(api, 'profile-reset', 123, nodes_list) + res = node.node_command(api, "profile-reset", 123, nodes_list) self.assertEqual(my_mock.API_RET, res) - api.node_command.assert_called_with('profile-reset', 123, nodes_list) + api.node_command.assert_called_with("profile-reset", 123, nodes_list) diff --git a/iotlabcli/tests/profile_parser_test.py b/iotlabcli/tests/profile_parser_test.py index 083ec21..75fa70c 100644 --- a/iotlabcli/tests/profile_parser_test.py +++ b/iotlabcli/tests/profile_parser_test.py @@ -19,47 +19,52 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.parser.profile module """ +"""Test the iotlabcli.parser.profile module""" # pylint:disable=missing-docstring,too-many-public-methods # Mock object not being recognized # pylint: disable=no-member,maybe-no-member -from iotlabcli.tests.my_mock import MainMock import iotlabcli.parser.profile as profile_parser import iotlabcli.profile +from iotlabcli.tests.my_mock import MainMock from .c23 import patch class TestMainProfileParser(MainMock): - def test_main_add_parser(self): # add simple add - profile_parser.main(['addwsn430', '-n', 'profile_name', '-p', 'dc']) + profile_parser.main(["addwsn430", "-n", "profile_name", "-p", "dc"]) self.api.add_profile.assert_called_with( - iotlabcli.profile.ProfileWSN430('profile_name', 'dc')) + iotlabcli.profile.ProfileWSN430("profile_name", "dc") + ) - profile_parser.main(['addm3', '-n', 'profile_name', '-p', 'dc']) + profile_parser.main(["addm3", "-n", "profile_name", "-p", "dc"]) self.api.add_profile.assert_called_with( - iotlabcli.profile.ProfileM3('profile_name', 'dc')) + iotlabcli.profile.ProfileM3("profile_name", "dc") + ) - profile_parser.main(['adda8', '-n', 'profile_name', '-p', 'dc']) + profile_parser.main(["adda8", "-n", "profile_name", "-p", "dc"]) self.api.add_profile.assert_called_with( - iotlabcli.profile.ProfileA8('profile_name', 'dc')) + iotlabcli.profile.ProfileA8("profile_name", "dc") + ) - profile_parser.main(['addcustom', '-n', 'profile_name', '-p', 'dc']) + profile_parser.main(["addcustom", "-n", "profile_name", "-p", "dc"]) self.api.add_profile.assert_called_with( - iotlabcli.profile.ProfileCustom('profile_name', 'dc')) + iotlabcli.profile.ProfileCustom("profile_name", "dc") + ) # invalid configuration 'power' without period and average self.assertRaises( - SystemExit, profile_parser.main, - ['addm3', '-n', 'profile_name', '-p', 'dc', '-power']) + SystemExit, + profile_parser.main, + ["addm3", "-n", "profile_name", "-p", "dc", "-power"], + ) @staticmethod - @patch('iotlabcli.parser.profile.ProfileM3') + @patch("iotlabcli.parser.profile.ProfileM3") def test_opts_parsing_m3(prof_m3_class): - """ Test that M3profile parsing matches profile class + """Test that M3profile parsing matches profile class Check that default values are ok and that values are correctly passed """ # keep 'choices' valid for argparse @@ -67,43 +72,45 @@ def test_opts_parsing_m3(prof_m3_class): profilem3 = prof_m3_class.return_value parser = profile_parser.parse_options() - args = ['addm3', '-n', 'name', '-p', 'dc'] + args = ["addm3", "-n", "name", "-p", "dc"] opts = parser.parse_args(args) profile_parser._m3_profile(opts) # pylint: disable=protected-access profilem3.set_consumption.assert_called_with( - period=None, average=None, power=False, - voltage=False, current=False) + period=None, average=None, power=False, voltage=False, current=False + ) profilem3.set_radio.assert_called_with( - mode=None, channels=None, period=None, num_per_channel=None) + mode=None, channels=None, period=None, num_per_channel=None + ) # Test for RSSI - args = ['addm3', '-n', 'name', '-p', 'dc'] - args += ['-period', '140', '-avg', '1', - '-power', '-voltage', '-current'] - args += ['-rssi', '-channels', '11', '12', '13', - '-num', '1', '-rperiod', '1'] + args = ["addm3", "-n", "name", "-p", "dc"] + args += ["-period", "140", "-avg", "1", "-power", "-voltage", "-current"] + args += ["-rssi", "-channels", "11", "12", "13", "-num", "1", "-rperiod", "1"] opts = parser.parse_args(args) profile_parser._m3_profile(opts) # pylint: disable=protected-access profilem3.set_consumption.assert_called_with( - period=140, average=1, power=True, voltage=True, current=True) + period=140, average=1, power=True, voltage=True, current=True + ) profilem3.set_radio.assert_called_with( - mode='rssi', channels=[11, 12, 13], period=1, num_per_channel=1) + mode="rssi", channels=[11, 12, 13], period=1, num_per_channel=1 + ) # Test for Radio Sniffer only - args = ['addm3', '-n', 'name', '-p', 'dc'] - args += ['-sniffer', '-channels', '11'] + args = ["addm3", "-n", "name", "-p", "dc"] + args += ["-sniffer", "-channels", "11"] opts = parser.parse_args(args) profile_parser._m3_profile(opts) # pylint: disable=protected-access profilem3.set_consumption.assert_called_with( - period=None, average=None, power=False, - voltage=False, current=False) + period=None, average=None, power=False, voltage=False, current=False + ) profilem3.set_radio.assert_called_with( - mode='sniffer', channels=[11], period=None, num_per_channel=None) + mode="sniffer", channels=[11], period=None, num_per_channel=None + ) @staticmethod - @patch('iotlabcli.parser.profile.ProfileWSN430') + @patch("iotlabcli.parser.profile.ProfileWSN430") def test_opts_parsing_wsn430(prof_wsn430_class): - """ Test that WSN430profile parsing matches profile class + """Test that WSN430profile parsing matches profile class Check that default values are ok and that values are correctly passed """ # keep 'choices' valid for argparse @@ -112,59 +119,65 @@ def test_opts_parsing_wsn430(prof_wsn430_class): parser = profile_parser.parse_options() # pylint: disable=protected-access - args = ['addwsn430', '-n', 'name', '-p', 'dc'] + args = ["addwsn430", "-n", "name", "-p", "dc"] opts = parser.parse_args(args) profile_parser._wsn430_profile(opts) profilewsn430.set_consumption.assert_called_with( - frequency=None, power=False, voltage=False, current=False) + frequency=None, power=False, voltage=False, current=False + ) profilewsn430.set_radio.assert_called_with(frequency=None) profilewsn430.set_sensors.assert_called_with( - frequency=None, temperature=False, luminosity=False) + frequency=None, temperature=False, luminosity=False + ) - args += ['-cfreq', '70', '-power', '-voltage', '-current'] - args += ['-rfreq', '500'] - args += ['-sfreq', '1000', '-luminosity', '-temperature'] + args += ["-cfreq", "70", "-power", "-voltage", "-current"] + args += ["-rfreq", "500"] + args += ["-sfreq", "1000", "-luminosity", "-temperature"] opts = parser.parse_args(args) profile_parser._wsn430_profile(opts) profilewsn430.set_consumption.assert_called_with( - frequency=70, power=True, voltage=True, current=True) + frequency=70, power=True, voltage=True, current=True + ) profilewsn430.set_radio.assert_called_with(frequency=500) profilewsn430.set_sensors.assert_called_with( - frequency=1000, temperature=True, luminosity=True) + frequency=1000, temperature=True, luminosity=True + ) def test__add_profile(self): ret = profile_parser._add_profile( # pylint: disable=protected-access - self.api, {'test_profile': 1}, json_out=True) - self.assertEqual(ret, {'test_profile': 1}) + self.api, {"test_profile": 1}, json_out=True + ) + self.assertEqual(ret, {"test_profile": 1}) self.assertFalse(self.api.add_profile.called) def test_main_get_parser(self): - profile_parser.main(['get', '--name', 'profile_name']) - self.api.get_profile.assert_called_with('profile_name') + profile_parser.main(["get", "--name", "profile_name"]) + self.api.get_profile.assert_called_with("profile_name") - profile_parser.main(['get', '--list']) + profile_parser.main(["get", "--list"]) self.api.get_profiles.assert_called_with(None) - profile_parser.main(['get', '--list', '--archi', 'm3']) - self.api.get_profiles.assert_called_with('m3') + profile_parser.main(["get", "--list", "--archi", "m3"]) + self.api.get_profiles.assert_called_with("m3") def test_main_del_parser(self): - profile_parser.main(['del', '--name', 'profile_name']) - self.api.del_profile.assert_called_with('profile_name') + profile_parser.main(["del", "--name", "profile_name"]) + self.api.del_profile.assert_called_with("profile_name") - @patch('iotlabcli.helpers.read_file') + @patch("iotlabcli.helpers.read_file") def test_main_load_parser(self, read_file_mock): read_file_mock.return_value = '{"profilename": "prof_name"}' - profile_parser.main(['load', '--file', 'prof.json']) - self.api.add_profile.assert_called_with({'profilename': "prof_name"}) + profile_parser.main(["load", "--file", "prof.json"]) + self.api.add_profile.assert_called_with({"profilename": "prof_name"}) # invalid profile file self.api.add_profile.reset_mock() - read_file_mock.return_value = 'not json input' - self.assertRaises(SystemExit, profile_parser.main, - ['load', '--file', 'prof.json']) + read_file_mock.return_value = "not json input" + self.assertRaises( + SystemExit, profile_parser.main, ["load", "--file", "prof.json"] + ) def test_parser_error(self): - """ Test some parser errors directly """ + """Test some parser errors directly""" parser = profile_parser.parse_options() # Python3 didn't raised error without subcommand self.assertRaises(SystemExit, parser.parse_args, []) diff --git a/iotlabcli/tests/profile_test.py b/iotlabcli/tests/profile_test.py index 00e0ec2..d86a29b 100644 --- a/iotlabcli/tests/profile_test.py +++ b/iotlabcli/tests/profile_test.py @@ -19,104 +19,103 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.module """ +"""Test the iotlabcli.module""" # pylint:disable=missing-docstring,too-many-public-methods import unittest + from iotlabcli import profile class TestM3Profile(unittest.TestCase): - def test_valid_full_profile_rssi(self): - m3_prof = profile.ProfileM3('name', 'dc') + m3_prof = profile.ProfileM3("name", "dc") m3_prof.set_consumption(140, 1, True, True, True) - m3_prof.set_radio('rssi', (11, 12, 13), period=1, num_per_channel=1) + m3_prof.set_radio("rssi", (11, 12, 13), period=1, num_per_channel=1) self.assertEqual( m3_prof.__dict__, { - 'power': 'dc', - 'profilename': 'name', - 'nodearch': 'm3', - 'radio': { - 'channels': (11, 12, 13), - 'num_per_channel': 1, - 'mode': 'rssi', - 'period': 1, + "power": "dc", + "profilename": "name", + "nodearch": "m3", + "radio": { + "channels": (11, 12, 13), + "num_per_channel": 1, + "mode": "rssi", + "period": 1, }, - 'consumption': { - 'current': True, - 'average': 1, - 'period': 140, - 'power': True, - 'voltage': True, + "consumption": { + "current": True, + "average": 1, + "period": 140, + "power": True, + "voltage": True, }, - } + }, ) # Test with default values for num_per_channel - m3_prof = profile.ProfileM3('name', 'dc') + m3_prof = profile.ProfileM3("name", "dc") m3_prof.set_consumption(None, None) - m3_prof.set_radio('rssi', (26,), period=42) + m3_prof.set_radio("rssi", (26,), period=42) self.assertEqual( m3_prof.__dict__, { - 'power': 'dc', - 'profilename': 'name', - 'nodearch': 'm3', - 'radio': { - 'channels': (26,), - 'num_per_channel': 0, - 'mode': 'rssi', - 'period': 42, + "power": "dc", + "profilename": "name", + "nodearch": "m3", + "radio": { + "channels": (26,), + "num_per_channel": 0, + "mode": "rssi", + "period": 42, }, - 'consumption': None, - } + "consumption": None, + }, ) def test_valid_sniffer_profile(self): - m3_prof = profile.ProfileM3('sniff_11', 'dc') + m3_prof = profile.ProfileM3("sniff_11", "dc") m3_prof.set_consumption(None, None) - m3_prof.set_radio('sniffer', (11,)) + m3_prof.set_radio("sniffer", (11,)) self.assertEqual( m3_prof.__dict__, { - 'power': 'dc', - 'profilename': 'sniff_11', - 'nodearch': 'm3', - 'radio': { - 'channels': (11,), - 'mode': 'sniffer', - 'num_per_channel': None, - 'period': None, + "power": "dc", + "profilename": "sniff_11", + "nodearch": "m3", + "radio": { + "channels": (11,), + "mode": "sniffer", + "num_per_channel": None, + "period": None, }, - 'consumption': None, - } + "consumption": None, + }, ) def test_valid_empty_profile(self): - m3_prof = profile.ProfileM3('name', 'dc') + m3_prof = profile.ProfileM3("name", "dc") m3_prof.set_consumption(None, None) m3_prof.set_radio(mode=None, channels=None) self.assertEqual( m3_prof.__dict__, { - 'power': 'dc', - 'profilename': 'name', - 'nodearch': 'm3', - 'consumption': None, - 'radio': None, - } + "power": "dc", + "profilename": "name", + "nodearch": "m3", + "consumption": None, + "radio": None, + }, ) class TestWSN430Profile(unittest.TestCase): - def test_valid_full_profile(self): - wsn430_prof = profile.ProfileWSN430('name', 'dc') + wsn430_prof = profile.ProfileWSN430("name", "dc") wsn430_prof.set_consumption(5000, True, True, True) wsn430_prof.set_radio(5000) wsn430_prof.set_sensors(30000, True, True) @@ -124,29 +123,29 @@ def test_valid_full_profile(self): self.assertEqual( wsn430_prof.__dict__, { - 'profilename': 'name', - 'power': 'dc', - 'nodearch': 'wsn430', - 'consumption': { - 'frequency': 5000, - 'current': True, - 'voltage': True, - 'power': True, + "profilename": "name", + "power": "dc", + "nodearch": "wsn430", + "consumption": { + "frequency": 5000, + "current": True, + "voltage": True, + "power": True, + }, + "radio": { + "frequency": 5000, + "rssi": True, }, - 'radio': { - 'frequency': 5000, - 'rssi': True, + "sensor": { + "frequency": 30000, + "luminosity": True, + "temperature": True, }, - 'sensor': { - 'frequency': 30000, - 'luminosity': True, - 'temperature': True, - } - } + }, ) def test_valid_empty_profile(self): - wsn430_prof = profile.ProfileWSN430('name', 'dc') + wsn430_prof = profile.ProfileWSN430("name", "dc") wsn430_prof.set_consumption(None) wsn430_prof.set_radio(frequency=None) wsn430_prof.set_sensors(None) @@ -154,11 +153,11 @@ def test_valid_empty_profile(self): self.assertEqual( wsn430_prof.__dict__, { - 'profilename': 'name', - 'power': 'dc', - 'nodearch': 'wsn430', - 'consumption': None, - 'radio': None, - 'sensor': None, - } + "profilename": "name", + "power": "dc", + "nodearch": "wsn430", + "consumption": None, + "radio": None, + "sensor": None, + }, ) diff --git a/iotlabcli/tests/rest_test.py b/iotlabcli/tests/rest_test.py index dca7da5..a017b60 100644 --- a/iotlabcli/tests/rest_test.py +++ b/iotlabcli/tests/rest_test.py @@ -19,7 +19,7 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.rest module """ +"""Test the iotlabcli.rest module""" # pylint: disable=too-many-public-methods # pylint: disable=protected-access @@ -34,93 +34,120 @@ class TestRest(unittest.TestCase): - """ Test the iotlabcli.rest module + """Test the iotlabcli.rest module As the interface itself cannot really be unit-tested against the remote REST server, I only test the internal functionnalities. Also, most of the internal code execution has been done by the upper layers So I don't re-check every method that just does formatting. """ - _url = 'http://url.test.org/rest/' + + _url = "http://url.test.org/rest/" def setUp(self): - self.api = rest.Api('user', 'password') + self.api = rest.Api("user", "password") self.api.url = self._url def test_method_no_content(self): - """ Test Api.method rest code 204 """ - ret_val = RequestRet(204, content='') - m_req = patch('requests.request', return_value=ret_val).start() + """Test Api.method rest code 204""" + ret_val = RequestRet(204, content="") + m_req = patch("requests.request", return_value=ret_val).start() _auth = self.api.auth - ret = self.api.method('resources/123', 'delete') - m_req.assert_called_with('delete', self._url + 'resources/123', - files=None, json=None, auth=_auth) + ret = self.api.method("resources/123", "delete") + m_req.assert_called_with( + "delete", + self._url + "resources/123", + timeout=None, + files=None, + json=None, + auth=_auth, + ) self.assertIsNone(ret) def test_method(self): - """ Test Api.method rest submission """ - ret_expected = {'test': 'val'} + """Test Api.method rest submission""" + ret_expected = {"test": "val"} ret_val = RequestRet(200, content=json_dumps(ret_expected)) - m_req = patch('requests.request', return_value=ret_val).start() + m_req = patch("requests.request", return_value=ret_val).start() # pylint:disable=protected-access _auth = self.api.auth # call get - ret = self.api.method('page') - m_req.assert_called_with('get', self._url + 'page', - files=None, json=None, auth=_auth) + ret = self.api.method("page") + m_req.assert_called_with( + "get", self._url + "page", timeout=None, files=None, json=None, auth=_auth + ) self.assertEqual(ret_expected, ret) - ret = self.api.method('page?1', 'get') - m_req.assert_called_with('get', self._url + 'page?1', - files=None, json=None, auth=_auth) + ret = self.api.method("page?1", "get") + m_req.assert_called_with( + "get", self._url + "page?1", timeout=None, files=None, json=None, auth=_auth + ) self.assertEqual(ret_expected, ret) # call delete - ret = self.api.method('deeel', 'delete') - m_req.assert_called_with('delete', self._url + 'deeel', - files=None, json=None, auth=_auth) + ret = self.api.method("deeel", "delete") + m_req.assert_called_with( + "delete", + self._url + "deeel", + timeout=None, + files=None, + json=None, + auth=_auth, + ) self.assertEqual(ret_expected, ret) # call post - ret = self.api.method('post_page', 'post', json={}) - m_req.assert_called_with('post', self._url + 'post_page', - files=None, json={}, auth=_auth) + ret = self.api.method("post_page", "post", json={}) + m_req.assert_called_with( + "post", + self._url + "post_page", + timeout=None, + files=None, + json={}, + auth=_auth, + ) self.assertEqual(ret_expected, ret) # call multipart - _files = {'entry': '{}'} - ret = self.api.method('multip', 'post', files=_files) - m_req.assert_called_with('post', self._url + 'multip', - files=_files, json=None, auth=_auth) + _files = {"entry": "{}"} + ret = self.api.method("multip", "post", files=_files) + m_req.assert_called_with( + "post", + self._url + "multip", + timeout=None, + files=_files, + json=None, + auth=_auth, + ) self.assertEqual(ret_expected, ret) patch.stopall() - @patch('iotlabcli.rest.Api.method') + @patch("iotlabcli.rest.Api.method") def test__get_with_cache(self, api_method): - """ Test Api._get_with_cache """ - ret = {'ret': 'my_url'} + """Test Api._get_with_cache""" + ret = {"ret": "my_url"} api_method.return_value = ret - self.assertEqual(ret, rest.Api._get_with_cache('my_url')) - self.assertEqual(ret, rest.Api._get_with_cache('my_url')) - self.assertEqual(ret, rest.Api._get_with_cache('my_url')) - self.assertEqual(ret, rest.Api._get_with_cache('my_url')) - self.assertEqual(ret, rest.Api._get_with_cache('my_url')) + self.assertEqual(ret, rest.Api._get_with_cache("my_url")) + self.assertEqual(ret, rest.Api._get_with_cache("my_url")) + self.assertEqual(ret, rest.Api._get_with_cache("my_url")) + self.assertEqual(ret, rest.Api._get_with_cache("my_url")) + self.assertEqual(ret, rest.Api._get_with_cache("my_url")) self.assertEqual(1, api_method.call_count) - ret = {'ret': 'my_url_2'} + ret = {"ret": "my_url_2"} api_method.return_value = ret - self.assertEqual(ret, rest.Api._get_with_cache('my_url_2')) - self.assertEqual(ret, rest.Api._get_with_cache('my_url_2')) - self.assertEqual(ret, rest.Api._get_with_cache('my_url_2')) + self.assertEqual(ret, rest.Api._get_with_cache("my_url_2")) + self.assertEqual(ret, rest.Api._get_with_cache("my_url_2")) + self.assertEqual(ret, rest.Api._get_with_cache("my_url_2")) self.assertEqual(2, api_method.call_count) def test_check_credentials(self): - """ Test Api.method rest submission """ + """Test Api.method rest submission""" ret_val = RequestRet(200, content='"OK"') - patch('requests.request', return_value=ret_val).start() + patch("requests.request", return_value=ret_val).start() ret_val.status_code = 200 self.assertTrue(self.api.check_credential()) @@ -134,63 +161,62 @@ def test_check_credentials(self): patch.stopall() def test_ssh_key(self): - """ Test ssh keys get/set """ + """Test ssh keys get/set""" test_ssh_keys = '{"sshkey": ["test"]}' test_ssh_keys_json = json.loads(test_ssh_keys) ret_val = RequestRet(200, content=test_ssh_keys) - patch('requests.request', return_value=ret_val).start() + patch("requests.request", return_value=ret_val).start() assert self.api.get_ssh_keys() == test_ssh_keys_json assert self.api.set_ssh_keys(test_ssh_keys_json) is None patch.stopall() def test_method_raw(self): - """ Run as Raw mode """ - ret_val = RequestRet(200, content='text_only') - with patch('requests.request', return_value=ret_val): + """Run as Raw mode""" + ret_val = RequestRet(200, content="text_only") + with patch("requests.request", return_value=ret_val): ret = self.api.method(self._url, raw=True) - self.assertEqual(ret, 'text_only'.encode('utf-8')) + self.assertEqual(ret, "text_only".encode("utf-8")) def test_method_errors(self): - """ Test Api.method rest submission error cases """ + """Test Api.method rest submission error cases""" # invalid status code - ret_val = RequestRet(404, content='return_text') - with patch('requests.request', return_value=ret_val): + ret_val = RequestRet(404, content="return_text") + with patch("requests.request", return_value=ret_val): self.assertRaises(HTTPError, self.api.method, self._url) # using older requests version fail because of json argument - with patch('requests.request', side_effect=TypeError()): + with patch("requests.request", side_effect=TypeError()): self.assertRaises(RuntimeError, self.api.method, self._url) class TestGetNodesSelection(unittest.TestCase): """Test get_nodes selection.""" + # pylint:disable=no-self-use - @patch('iotlabcli.rest.Api.method') + @patch("iotlabcli.rest.Api.method") def test_get_nodes_selection(self, _method): """Test get_nodes selection.""" - _method.return_value = {'state': 'Running'} + _method.return_value = {"state": "Running"} api = rest.Api(None, None) - api.get_nodes(False, 'grenoble', archi='m3', state='Alive') - _method.assert_called_with('nodes' - '?archi=m3&site=grenoble&state=Alive') + api.get_nodes(False, "grenoble", archi="m3", state="Alive") + _method.assert_called_with("nodes?archi=m3&site=grenoble&state=Alive") - api.get_nodes(True, archi='a8', state='Busy', site='lille') - _method.assert_called_with('nodes/ids' - '?archi=a8&site=lille&state=Busy') + api.get_nodes(True, archi="a8", state="Busy", site="lille") + _method.assert_called_with("nodes/ids?archi=a8&site=lille&state=Busy") class TestGetCircuitsSelection(unittest.TestCase): """Test get_circuits selection.""" + # pylint:disable=no-self-use - @patch('iotlabcli.rest.Api.method') + @patch("iotlabcli.rest.Api.method") def test_get_circuits_selection(self, _method): """Test get_circuits selection.""" - _method.return_value = {'circuit': 'test'} + _method.return_value = {"circuit": "test"} api = rest.Api(None, None) - api.get_circuits(site='grenoble', type='predefined') - _method.assert_called_with('mobilities/circuits' - '?site=grenoble&type=predefined') + api.get_circuits(site="grenoble", type="predefined") + _method.assert_called_with("mobilities/circuits?site=grenoble&type=predefined") diff --git a/iotlabcli/tests/robot_parser_test.py b/iotlabcli/tests/robot_parser_test.py index 088d82b..3baeacd 100644 --- a/iotlabcli/tests/robot_parser_test.py +++ b/iotlabcli/tests/robot_parser_test.py @@ -20,7 +20,7 @@ # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.parser.robot module """ +"""Test the iotlabcli.parser.robot module""" import iotlabcli.parser.robot as robot_parser from iotlabcli.tests.my_mock import MainMock @@ -34,55 +34,56 @@ class TestMainRobotParser(MainMock): """Test the iotlab-robot main parser.""" - @patch('iotlabcli.robot.robot_command') - @patch('iotlabcli.parser.common.list_nodes') + @patch("iotlabcli.robot.robot_command") + @patch("iotlabcli.parser.common.list_nodes") def test_main_status(self, list_nodes, robot_command): """Run the parser.robot.main function for status commands.""" - robot_command.return_value = {'result': 'test'} + robot_command.return_value = {"result": "test"} list_nodes.return_value = [] - args = ['status'] + args = ["status"] robot_parser.main(args) list_nodes.assert_called_with(self.api, 123, None, None) - robot_command.assert_called_with(self.api, 'status', 123, []) + robot_command.assert_called_with(self.api, "status", 123, []) - args = ['status', '-l', 'grenoble,m3,1-2', '-l', 'grenoble,m3,3'] - list_nodes.return_value = ['m3-1', 'm3-2', 'm3-3'] # simplify + args = ["status", "-l", "grenoble,m3,1-2", "-l", "grenoble,m3,3"] + list_nodes.return_value = ["m3-1", "m3-2", "m3-3"] # simplify robot_parser.main(args) - robot_command.assert_called_with(self.api, 'status', 123, - ['m3-1', 'm3-2', 'm3-3']) + robot_command.assert_called_with( + self.api, "status", 123, ["m3-1", "m3-2", "m3-3"] + ) - @patch('iotlabcli.robot.robot_update_mobility') - @patch('iotlabcli.parser.common.list_nodes') + @patch("iotlabcli.robot.robot_update_mobility") + @patch("iotlabcli.parser.common.list_nodes") def test_main_update(self, list_nodes, robot_update_mobility): """Run the parser.robot.main function for update commands.""" - robot_update_mobility.return_value = {'result': 'test'} + robot_update_mobility.return_value = {"result": "test"} list_nodes.return_value = [] - args = ['update', '-n', 'traj'] + args = ["update", "-n", "traj"] robot_parser.main(args) list_nodes.assert_called_with(self.api, 123, None, None) - robot_update_mobility.assert_called_with( - self.api, 123, 'traj', []) + robot_update_mobility.assert_called_with(self.api, 123, "traj", []) - @patch('iotlabcli.robot.circuit_command') + @patch("iotlabcli.robot.circuit_command") def test_main_mobility(self, circuit_command): """Run the parser.robot.main function for circuit commands.""" - circuit_command.return_value = {'result': 'test'} + circuit_command.return_value = {"result": "test"} # List circuits - args = ['get', '--list'] + args = ["get", "--list"] robot_parser.main(args) - circuit_command.assert_called_with(self.api, 'list') + circuit_command.assert_called_with(self.api, "list") # List mobility - args = ['get', '--list', '--site', 'grenoble', '--type', 'predefined'] + args = ["get", "--list", "--site", "grenoble", "--type", "predefined"] robot_parser.main(args) - circuit_command.assert_called_with(self.api, 'list', site='grenoble', - type='predefined') + circuit_command.assert_called_with( + self.api, "list", site="grenoble", type="predefined" + ) # Get mobility - args = ['get', '-n', 'site_name'] + args = ["get", "-n", "site_name"] robot_parser.main(args) - circuit_command.assert_called_with(self.api, 'get', 'site_name') + circuit_command.assert_called_with(self.api, "get", "site_name") diff --git a/iotlabcli/tests/robot_test.py b/iotlabcli/tests/robot_test.py index 2cf919e..a072cbd 100644 --- a/iotlabcli/tests/robot_test.py +++ b/iotlabcli/tests/robot_test.py @@ -19,18 +19,19 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.robot module """ +"""Test the iotlabcli.robot module""" # pylint: disable=too-many-public-methods # Issues with 'mock' # pylint: disable=no-member,maybe-no-member import unittest + from iotlabcli import robot from iotlabcli.tests import my_mock class TestRobot(unittest.TestCase): - """ Test the iotlabcli.node module """ + """Test the iotlabcli.node module""" def setUp(self): self.api = my_mock.api_mock() @@ -42,43 +43,47 @@ def test_robot_command(self): """Test 'robot_command'.""" nodes_list = ["m3-1", "m3-2", "m3-3"] - ret = robot.robot_command(self.api, 'status', 123, nodes_list) + ret = robot.robot_command(self.api, "status", 123, nodes_list) self.assertEqual(my_mock.API_RET, ret) - self.api.robot_command.assert_called_with('status', 123, nodes_list) + self.api.robot_command.assert_called_with("status", 123, nodes_list) def test_robot_update_mobility(self): """Test robot_update_mobility.""" nodes_list = ["m3-1", "m3-2", "m3-3"] # With site name - ret = robot.robot_update_mobility(self.api, 123, 'mob_name', - nodes_list) + ret = robot.robot_update_mobility(self.api, 123, "mob_name", nodes_list) self.assertEqual(my_mock.API_RET, ret) - self.api.robot_update_mobility.assert_called_with( - 123, 'mob_name', nodes_list) + self.api.robot_update_mobility.assert_called_with(123, "mob_name", nodes_list) def test_circuit_command(self): """Test 'mobility_command'.""" # mobility-list - ret = robot.circuit_command(self.api, 'list') + ret = robot.circuit_command(self.api, "list") self.assertEqual(my_mock.API_RET, ret) self.api.get_circuits.assert_called_with() self.api.reset_mock() # invalid circuit type - self.assertRaises(AssertionError, robot.circuit_command, - self.api, 'list', site='grenoble', type='no_defined') + self.assertRaises( + AssertionError, + robot.circuit_command, + self.api, + "list", + site="grenoble", + type="no_defined", + ) # mobility-get - ret = robot.circuit_command(self.api, 'get', 'm_name') + ret = robot.circuit_command(self.api, "get", "m_name") self.assertEqual(my_mock.API_RET, ret) - self.api.get_circuit.assert_called_with('m_name') + self.api.get_circuit.assert_called_with("m_name") self.api.reset_mock() def test_robot_get_map(self): """Test robot_get_map.""" - ret = robot.robot_get_map('grenoble') - self.assertEqual(sorted(ret.keys()), ['config', 'dock', 'image']) + ret = robot.robot_get_map("grenoble") + self.assertEqual(sorted(ret.keys()), ["config", "dock", "image"]) # Lazy to check calls to Api.method should use proxy to call it... diff --git a/iotlabcli/tests/status_parser_test.py b/iotlabcli/tests/status_parser_test.py index 90004ba..04df128 100644 --- a/iotlabcli/tests/status_parser_test.py +++ b/iotlabcli/tests/status_parser_test.py @@ -19,7 +19,7 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.parser.status module """ +"""Test the iotlabcli.parser.status module""" import iotlabcli.parser.status as status_parser from iotlabcli.tests.my_mock import MainMock @@ -27,36 +27,36 @@ from .c23 import patch -@patch('iotlabcli.status.status_command') +@patch("iotlabcli.status.status_command") class TestMainStatusParser(MainMock): - """ Test iotlab-status main parser """ + """Test iotlab-status main parser""" + def test_main(self, status_command): - """ Run the parser.status.main function """ - status_command.return_value = {'result': 'test'} + """Run the parser.status.main function""" + status_command.return_value = {"result": "test"} # sites - args = ['--sites'] + args = ["--sites"] status_parser.main(args) - status_command.assert_called_with(self.api, 'sites') + status_command.assert_called_with(self.api, "sites") # nodes - args = ['--nodes'] + args = ["--nodes"] status_parser.main(args) - status_command.assert_called_with(self.api, 'nodes') + status_command.assert_called_with(self.api, "nodes") # nodes - args = ['--nodes-ids'] + args = ["--nodes-ids"] status_parser.main(args) - status_command.assert_called_with(self.api, 'nodes-ids') + status_command.assert_called_with(self.api, "nodes-ids") # experiments - args = ['--experiments-running'] + args = ["--experiments-running"] status_parser.main(args) - status_command.assert_called_with(self.api, 'experiments') + status_command.assert_called_with(self.api, "experiments") # Use other selections - status_parser.main(['--nodes', '--archi', 'm3', - '--state', 'Alive']) - status_command.assert_called_with(self.api, 'nodes', archi='m3', - state='Alive') - - status_parser.main(['--nodes-ids', '--site', 'lille', '--archi', 'm3']) - status_command.assert_called_with(self.api, 'nodes-ids', site='lille', - archi='m3') + status_parser.main(["--nodes", "--archi", "m3", "--state", "Alive"]) + status_command.assert_called_with(self.api, "nodes", archi="m3", state="Alive") + + status_parser.main(["--nodes-ids", "--site", "lille", "--archi", "m3"]) + status_command.assert_called_with( + self.api, "nodes-ids", site="lille", archi="m3" + ) diff --git a/iotlabcli/tests/status_test.py b/iotlabcli/tests/status_test.py index 21db6e5..a9efa8b 100644 --- a/iotlabcli/tests/status_test.py +++ b/iotlabcli/tests/status_test.py @@ -19,7 +19,7 @@ # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. -""" Test the iotlabcli.status module """ +"""Test the iotlabcli.status module""" # pylint: disable=too-many-public-methods # Issues with 'mock' @@ -31,31 +31,32 @@ class TestStatus(unittest.TestCase): - """ Test the 'iotlabcli.status' module """ + """Test the 'iotlabcli.status' module""" + def tearDown(self): my_mock.api_mock_stop() def test_status_command(self): - """ Test 'status_command' """ + """Test 'status_command'""" api = my_mock.api_mock() api.reset_mock() - res = status.status_command(api, 'sites') + res = status.status_command(api, "sites") self.assertEqual(my_mock.API_RET, res) api.get_sites_details.assert_called() api.reset_mock() - res = status.status_command(api, 'nodes') + res = status.status_command(api, "nodes") self.assertEqual(my_mock.API_RET, res) api.get_nodes.assert_called() api.reset_mock() - res = status.status_command(api, 'nodes-ids', site='grenoble') + res = status.status_command(api, "nodes-ids", site="grenoble") self.assertEqual(my_mock.API_RET, res) - api.get_nodes.assert_called_with(list_id=True, site='grenoble') + api.get_nodes.assert_called_with(list_id=True, site="grenoble") api.reset_mock() - res = status.status_command(api, 'experiments') + res = status.status_command(api, "experiments") self.assertEqual(my_mock.API_RET, res) api.get_running_experiments.assert_called() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..04d1550 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,68 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "iotlabcli" +dynamic = ["version"] +description = "IoT-LAB testbed command-line client" +readme = "README.rst" +license = {text = "CeCILL v2.1"} +authors = [{name = "IoT-LAB Team", email = "admin@iot-lab.info"}] +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Intended Audience :: End Users/Desktop", + "Environment :: Console", + "Topic :: Utilities", +] +dependencies = [ + "requests>2.4.2", + "jmespath", +] + +[project.optional-dependencies] +secure = ["pyOpenSSL", "ndg-httpsclient", "pyasn1"] + +[project.urls] +Homepage = "https://www.iot-lab.info" +Repository = "https://github.com/iot-lab/cli-tools" +Download = "https://github.com/iot-lab/cli-tools/" + +[project.scripts] +iotlab = "iotlabcli.parser.main:main" +iotlab-auth = "iotlabcli.parser.auth:main" +iotlab-experiment = "iotlabcli.parser.experiment:main" +iotlab-node = "iotlabcli.parser.node:main" +iotlab-profile = "iotlabcli.parser.profile:main" +iotlab-robot = "iotlabcli.parser.robot:main" +iotlab-status = "iotlabcli.parser.status:main" + +[tool.hatch.version] +path = "iotlabcli/__init__.py" + +[tool.pytest.ini_options] +addopts = "-v --ignore iotlabcli/integration --junit-xml=test-report.xml --doctest-modules" +testpaths = ["iotlabcli"] +junit_family = "legacy" + +[tool.pylint."messages control"] +disable = ["bad-option-value", "raise-missing-from", "unspecified-encoding"] + +[tool.pylint.reports] +reports = false +msg-template = "{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" + +[tool.ruff] +line-length = 88 + +[tool.ruff.lint] +select = ["E", "F", "W", "C901"] + +[tool.ruff.lint.mccabe] +max-complexity = 4 + +[tool.isort] +profile = "black" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 56334b8..0000000 --- a/setup.cfg +++ /dev/null @@ -1,15 +0,0 @@ -[tool:pytest] -addopts = -v --ignore iotlabcli/integration - --junit-xml=test-report.xml - --doctest-modules -testpaths = iotlabcli -junit_family = legacy - -[pylint] -reports=no -disable=bad-option-value,raise-missing-from,unspecified-encoding -msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} - -[flake8] -exclude = .tox,dist,doc,build,*.egg -max-complexity = 4 diff --git a/setup.py b/setup.py deleted file mode 100644 index c25d754..0000000 --- a/setup.py +++ /dev/null @@ -1,90 +0,0 @@ -#! /usr/bin/env python -# -*- coding:utf-8 -*- - -# This file is a part of IoT-LAB cli-tools -# Copyright (C) 2015 INRIA (Contact: admin@iot-lab.info) -# Contributor(s) : see AUTHORS file -# -# This software is governed by the CeCILL license under French law -# and abiding by the rules of distribution of free software. You can use, -# modify and/ or redistribute the software under the terms of the CeCILL -# license as circulated by CEA, CNRS and INRIA at the following URL -# http://www.cecill.info. -# -# As a counterpart to the access to the source code and rights to copy, -# modify and redistribute granted by the license, users are provided only -# with a limited warranty and the software's author, the holder of the -# economic rights, and the successive licensors have only limited -# liability. -# -# The fact that you are presently reading this means that you have had -# knowledge of the CeCILL license and that you accept its terms. - -"""CLI-Tools setuptools script.""" - -import os -from setuptools import setup, find_packages - -PACKAGE = 'iotlabcli' -# GPL compatible http://www.gnu.org/licenses/license-list.html#CeCILL -LICENSE = 'CeCILL v2.1' - - -def cat(files, join_str=''): - """Concatenate `files` content with `join_str` between them.""" - # pylint:disable=consider-using-with - files_content = (open(f).read() for f in files) - return join_str.join(files_content) - - -def get_version(package): - """ Extract package version without importing file - Importing cause issues with coverage, - (modules can be removed from sys.modules to prevent this) - Importing __init__.py triggers importing rest and then requests too - - Inspired from pep8 setup.py - """ - version = '-1' - with open(os.path.join(package, '__init__.py')) as init_fd: - for line in init_fd: - if line.startswith('__version__'): - version = eval(line.split('=')[-1]) # pylint:disable=eval-used - break - return version - - -SCRIPTS = ['iotlab-auth', 'iotlab-experiment', 'iotlab-node', 'iotlab-profile', - 'iotlab-robot', 'iotlab-status', 'iotlab'] - -LONG_DESCRIPTION_FILES = ['README.rst', 'CHANGELOG.rst'] - - -setup( - name=PACKAGE, - version=get_version(PACKAGE), - description='IoT-LAB testbed command-line client', - long_description=cat(LONG_DESCRIPTION_FILES, '\n\n'), - long_description_content_type='text/x-rst', - author='IoT-LAB Team', - author_email='admin@iot-lab.info', - url='http://www.iot-lab.info', - license=LICENSE, - download_url='http://github.com/iot-lab/cli-tools/', - packages=find_packages(), - package_data={'integration/firmwares': ['integration/firmwares/*']}, - include_package_data=True, - scripts=SCRIPTS, - classifiers=['Development Status :: 5 - Production/Stable', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Intended Audience :: End Users/Desktop', - 'Environment :: Console', - 'Topic :: Utilities', ], - extras_require={ - # https://urllib3.readthedocs.org/en/latest/\ - # security.html#openssl-pyopenssl - 'secure': ['pyOpenSSL', 'ndg-httpsclient', 'pyasn1'], - }, - install_requires=['requests>2.4.2', 'jmespath'], -) diff --git a/tests_utils/check_license.sh b/tests_utils/check_license.sh index d25709a..ceedbb9 100644 --- a/tests_utils/check_license.sh +++ b/tests_utils/check_license.sh @@ -9,7 +9,6 @@ files_list=$(git ls-tree -r HEAD --full-tree --name-only) files_list=$(echo "${files_list}" | grep -v \ -e 'tests_utils/' \ -e '.gitignore' \ - -e 'setup.cfg' \ -e 'tox.ini' \ -e '.md$' \ -e '.rst$' \ @@ -18,7 +17,6 @@ files_list=$(echo "${files_list}" | grep -v \ -e '.json'\ -e 'AUTHORS' \ -e 'COPYING' \ - -e 'MANIFEST.in' \ -e 'iotlabcli/parser/help/*' \ -e 'iotlabcli/tests/script.sh' \ -e 'iotlabcli/tests/script_2.sh' \ @@ -29,6 +27,7 @@ files_list=$(echo "${files_list}" | grep -v \ -e '.github/workflows/continuous-integration.yml' \ -e '.github/workflows/continuous-integration-tools.yml' \ -e 'utils/iotlabcli-bash-completion.sh' \ + -e 'pyproject.toml' \ ) # Verify that 'AUTHORS' and 'COPYING' files exist diff --git a/tests_utils/test-requirements.txt b/tests_utils/test-requirements.txt index 7715f79..682a254 100644 --- a/tests_utils/test-requirements.txt +++ b/tests_utils/test-requirements.txt @@ -2,8 +2,7 @@ pytest pytest-cov mock pylint -pycodestyle -# issues with pep8 >= 1.6 -flake8==2.3.0 +ruff +isort codecov>=1.4.0 twine diff --git a/tox.ini b/tox.ini index f996aed..50ac1cc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = copying,{py37,py38,py39,py310,py311,py312,py313}-{lint,tests,tools,cli,checksetup} +envlist = copying,checksetup,{py310,py313,py314}-{lint,tests,tools,cli} skip_missing_interpreters = true [testenv] @@ -7,15 +7,12 @@ allowlist_externals = cli: {[testenv:cli]allowlist_externals} deps= -rtests_utils/test-requirements.txt - checksetup: {[testenv:checksetup]deps} tools: {[testenv:tools]deps} commands= - tests: {[testenv:tests]commands} - tools: {[testenv:tools]commands} - lint: {[testenv:lint]commands} - cli: {[testenv:cli]commands} - checksetup: {[testenv:checksetup]commands} - coverage: {[testenv:coverage]commands} + tests: {[testenv:tests]commands} + tools: {[testenv:tools]commands} + lint: {[testenv:lint]commands} + cli: {[testenv:cli]commands} [testenv:tests] commands= @@ -31,10 +28,10 @@ commands= [testenv:lint] commands= - pycodestyle iotlabcli setup.py - pylint --rcfile=setup.cfg iotlabcli setup.py - flake8 - twine check {distdir}/* + pylint iotlabcli + ruff check iotlabcli + ruff format --check iotlabcli + isort --check-only iotlabcli [testenv:copying] allowlist_externals= @@ -56,12 +53,16 @@ commands= [testenv:checksetup] deps = - docutils - readme-renderer + hatch + twine skip_install = True usedevelop = False +allowlist_externals = + rm commands = - python setup.py check --strict --metadata + rm -rf dist + hatch build + twine check dist/* [testenv:coverage_upload_tests] passenv = CI TRAVIS TRAVIS_* @@ -72,14 +73,12 @@ passenv = CI TRAVIS TRAVIS_* commands = codecov -e TOXENV [testenv:integration] -# Use develop to get 'iotlabcli' coverage output -# Either it would be in .tox/integration/..../site-packages/ usedevelop=True deps= -rtests_utils/test-requirements.txt passenv = IOTLAB_TEST_PASSWORD commands= - pip install --upgrade -e.[secure] # Install iotlabcli[secure] dependencies + pip install --upgrade -e.[secure] coverage run --source iotlabcli --omit='iotlabcli/tests/*' iotlabcli/integration/test_integration.py {posargs} coverage report coverage html