diff --git a/.github/workflows/check-main-repos.yml b/.github/workflows/check-main-repos.yml index 455f125495..228e600500 100644 --- a/.github/workflows/check-main-repos.yml +++ b/.github/workflows/check-main-repos.yml @@ -1,8 +1,6 @@ name: Test main tier of ecosystem on: - schedule: - - cron: '5 8 * * 2' # each Tuesday at 8 05 workflow_dispatch: jobs: @@ -15,7 +13,7 @@ jobs: steps: - name: Get current datetime id: date - run: echo "::set-output name=date::$(date +'%Y-%m-%d %H:%M')" + run: echo "::set-output name=date::$(date +'%Y_%m_%d_%H_%M')" - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 @@ -33,23 +31,17 @@ jobs: pip install -r requirements.txt pip install -r requirements-dev.txt - # test runs with standard tests - - name: Standard test for Qiskit-nature - run: python manager.py standard_tests https://github.com/Qiskit/qiskit-nature --tox_python=py39 - - name: Standard test for Qiskit-finance - run: python manager.py standard_tests https://github.com/Qiskit/qiskit-finance --tox_python=py39 - # test runs with stable version of Qiskit - name: Stable version of Qiskit test for Qiskit-nature - run: python manager.py stable_compatibility_tests https://github.com/Qiskit/qiskit-nature --tox_python=py39 + run: python manager.py python_stable_tests https://github.com/Qiskit/qiskit-nature --tox_python=py39 - name: Stable version of Qiskit test for Qiskit-finance - run: python manager.py stable_compatibility_tests https://github.com/Qiskit/qiskit-finance --tox_python=py39 + run: python manager.py python_stable_tests https://github.com/Qiskit/qiskit-finance --tox_python=py39 # test runs with dev version of Qiskit - name: Dev version of Qiskit test for Qiskit-nature - run: python manager.py dev_compatibility_tests https://github.com/Qiskit/qiskit-nature --tox_python=py39 + run: python manager.py python_dev_tests https://github.com/Qiskit/qiskit-nature --tox_python=py39 - name: Dev version of Qiskit test for Qiskit-finance - run: python manager.py dev_compatibility_tests https://github.com/Qiskit/qiskit-finance --tox_python=py39 + run: python manager.py python_dev_tests https://github.com/Qiskit/qiskit-finance --tox_python=py39 - name: State of members.json file run: cat ecosystem/resources/members.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 174501976d..0d7fcd6a95 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: tests: runs-on: ubuntu-latest strategy: - max-parallel: 4 + max-parallel: 2 matrix: python-version: [3.9] steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cf43fd43e..21c324a04b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,4 +5,13 @@ included in the qiskit documentation: https://qiskit.org/documentation/contributing_to_qiskit.html -# Joining the Ecosystem +## Joining the Ecosystem + +To join ecosystem you need to create +[submission issue](https://github.com/qiskit-community/ecosystem/issues/new?labels=&template=submission.yml&title=%5BSubmission%5D%3A+) +and fill in all required details. That's it! + + +## Dev contributions + +[Refer to dev docs](./docs/dev/dev-doc.md) \ No newline at end of file diff --git a/docs/dev/dev-doc.md b/docs/dev/dev-doc.md new file mode 100644 index 0000000000..c62b9332df --- /dev/null +++ b/docs/dev/dev-doc.md @@ -0,0 +1,46 @@ +Dev docs +======== + +As entire repository is designed to be run through GitHub Actions, +we implemented ecosystem python package as runner of CLI commands +to be executed from steps in Actions. + +Entrypoint is ``manager.py`` file in the root of repository. + +Example of commands: +```shell +python manager.py python_dev_tests https://github.com/IceKhan13/demo-implementation --python_version=py39 +python manager.py python_stable_tests https://github.com/IceKhan13/demo-implementation --python_version=py39 +``` +or in general +```shell +python manager.py [FLAGS] +``` + +#### Ecosystem workflows configuration + +In order to talk control of execution workflow of tests in ecosystem +repository should have `qe_config.json` file in a root directory. + +Structure of config file: +- dependencies_files: list[string] - files with package dependencies (ex: requirements.txt, packages.json) +- extra_dependencies: list[string] - names of additional packages to install before tests execution +- language: string - programming language for tests env. Only supported lang is Python at this moment. +- tests_command: list[string] - list of commands to execute tests + +Example: +```json +{ + "dependencies_files": [ + "requirements.txt", + "requirements-dev.txt" + ], + "extra_dependencies": [ + "pytest" + ], + "language": "python", + "tests_command": [ + "pytest -p no:warnings --pyargs test" + ] +} +``` diff --git a/ecosystem/commands.py b/ecosystem/commands.py index a0f4560280..c3530e0d6a 100644 --- a/ecosystem/commands.py +++ b/ecosystem/commands.py @@ -8,14 +8,16 @@ from jinja2 import Template from ecosystem.entities import CommandExecutionSummary -from ecosystem.logging import logger +from ecosystem.utils import logger def _execute_command(command: List[str], - cwd: Optional[str] = None) -> CommandExecutionSummary: + cwd: Optional[str] = None, + name: Optional[str] = None) -> CommandExecutionSummary: """Executes specified command as subprocess in a directory.""" with subprocess.Popen(command, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, cwd=cwd) as process: logs = [] while True: @@ -30,7 +32,8 @@ def _execute_command(command: List[str], logs.append(str(output).strip()) return CommandExecutionSummary(code=return_code, - logs=logs) + logs=logs, + name=name) def _clone_repo(repo: str, directory: str) -> CommandExecutionSummary: @@ -51,7 +54,8 @@ def _run_tox(directory: str, env: str) -> CommandExecutionSummary: """Run tox test.""" return _execute_command(["tox", "-e{}".format(env), "--workdir", directory], - cwd=directory) + cwd=directory, + name="tox") def _cleanup(directory: Optional[str] = None): diff --git a/ecosystem/controller.py b/ecosystem/controller.py index 31b53cb8c1..6241dc157a 100644 --- a/ecosystem/controller.py +++ b/ecosystem/controller.py @@ -1,9 +1,9 @@ """Entrypoint for CLI.""" from typing import Optional, List -from tinydb import TinyDB, Query, where +from tinydb import TinyDB, Query -from .entities import Repository, MainRepository, Tier +from .entities import Repository, Tier, TestResult class Controller: @@ -23,52 +23,35 @@ def insert(self, repo: Repository) -> int: table = self.database.table(repo.tier) return table.insert(repo.to_dict()) - def get_all_main(self) -> List[MainRepository]: + def get_all_main(self) -> List[Repository]: """Returns all repositories from database.""" table = self.database.table(Tier.MAIN) - return [MainRepository(**r) for r in table.all()] + return [Repository.from_dict(r) for r in table.all()] def get_by_url(self, url: str, tier: str) -> Optional[Repository]: """Returns repository by URL.""" res = self.database.table(tier).get(Query().url == url) - return MainRepository(**res) if res else None + return Repository.from_dict(res) if res else None - def update_repo_tests_passed(self, repo: Repository, - tests_passed: List[str]) -> List[int]: - """Updates repository passed tests.""" - table = self.database.table(repo.tier) - return table.update({"tests_passed": tests_passed}, - where('name') == repo.name) - - def add_repo_test_passed(self, - repo_url: str, - test_passed: str, - tier: str): - """Adds passed test if is not there yet.""" + def add_repo_test_result(self, repo_url: str, + tier: str, + test_result: TestResult) -> Optional[List[int]]: + """Adds test result for repository.""" table = self.database.table(tier) - repo = self.get_by_url(repo_url, tier) - if repo: - tests_passed = repo.tests_passed - if test_passed not in tests_passed: - tests_passed.append(test_passed) - return table.update({"tests_passed": tests_passed}, - where('name') == repo.name) - return [0] + repository = Query() - def remove_repo_test_passed(self, - repo_url: str, - test_remove: str, - tier: str): - """Remove passed tests.""" - table = self.database.table(tier) - repo = self.get_by_url(repo_url, tier) - if repo: - tests_passed = repo.tests_passed - if test_remove in tests_passed: - tests_passed.remove(test_remove) - return table.update({"tests_passed": tests_passed}, - where('name') == repo.name) - return [0] + fetched_repo_json = table.get(repository.url == repo_url) + if fetched_repo_json is not None: + fetched_repo = Repository.from_dict(fetched_repo_json) + fetched_test_results = fetched_repo.tests_results + + new_test_results = [tr for tr in fetched_test_results + if tr.test_type != test_result.test_type or + tr.terra_version != test_result.terra_version] + [test_result] + fetched_repo.tests_results = new_test_results + + return table.upsert(fetched_repo.to_dict(), repository.url == repo_url) + return None def delete(self, repo: Repository) -> List[int]: """Deletes entry.""" diff --git a/ecosystem/controllers/__init__.py b/ecosystem/controllers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ecosystem/controllers/runner.py b/ecosystem/controllers/runner.py new file mode 100644 index 0000000000..b104a3d0b6 --- /dev/null +++ b/ecosystem/controllers/runner.py @@ -0,0 +1,137 @@ +"""Ecosystem test runner.""" +import os +import shutil +from abc import abstractmethod +from logging import Logger +from typing import Optional, Union, cast, List, Tuple + +from ecosystem.commands import _clone_repo, _run_tox +from ecosystem.entities import CommandExecutionSummary, Repository +from ecosystem.utils import logger as ecosystem_logger +from ecosystem.models import RepositoryConfiguration, PythonRepositoryConfiguration +from ecosystem.utils import QiskitEcosystemException + + +class Runner: + """Runner for repository checks. + + General class to run workflow for repository. + """ + + def __init__(self, + repo: Union[str, Repository], + working_directory: Optional[str] = None, + logger: Optional[Logger] = None): + self.repo: str = repo.url if isinstance(repo, Repository) else repo + self.working_directory = f"{working_directory}/cloned_repo_directory" or "./" + self.logger = logger or ecosystem_logger + name = self.repo.split("/")[-1] + self.cloned_repo_directory = f"{self.working_directory}/{name}" + + def set_up(self): + """Preparation step before running workload.""" + if self.cloned_repo_directory and \ + os.path.exists(self.cloned_repo_directory): + shutil.rmtree(self.cloned_repo_directory) + os.makedirs(self.cloned_repo_directory) + + def tear_down(self): + """Execution after workload is finished.""" + if self.cloned_repo_directory and \ + os.path.exists(self.cloned_repo_directory): + shutil.rmtree(self.cloned_repo_directory) + + @abstractmethod + def workload(self) -> Tuple[str, List[CommandExecutionSummary]]: + """Runs workload of commands to check repository. + + Returns: tuple (qiskit_version, CommandExecutionSummary) + """ + + def run(self) -> Tuple[str, List[CommandExecutionSummary]]: + """Runs chain of commands to check repository.""" + self.set_up() + # clone repository + self.logger.info("Cloning repository: %s", self.repo) + clone_res = _clone_repo(self.repo, directory=self.working_directory) + + if not clone_res.ok: + raise QiskitEcosystemException( + f"Something went wrong with cloning {self.repo} repository.") + + try: + result = self.workload() + except Exception as exception: # pylint: disable=broad-except) + result = ("-", CommandExecutionSummary(1, [], summary=str(exception))) + self.logger.error(exception) + self.tear_down() + return result + + +class PythonRunner(Runner): + """Runners for Python repositories.""" + + def __init__(self, + repo: Union[str, Repository], + working_directory: Optional[str] = None, + ecosystem_deps: Optional[List[str]] = None, + python_version: str = "py39", + repo_config: Optional[RepositoryConfiguration] = None): + super().__init__(repo=repo, + working_directory=working_directory) + self.python_version = python_version + self.ecosystem_deps = ecosystem_deps or ["qiskit"] + self.repo_config = repo_config + + def workload(self) -> Tuple[str, List[CommandExecutionSummary]]: + """Runs checks for python repository. + + Steps: + - check for configuration file + - optional: check for tox file + - optional: render tox file + - run tests + - form report + + Returns: execution summary of steps + """ + # check for configuration file + if self.repo_config is not None: + repo_config = self.repo_config + elif os.path.exists(f"{self.cloned_repo_directory}/qe_config.json"): + self.logger.info("Configuration file exists.") + loaded_config = RepositoryConfiguration.load( + f"{self.cloned_repo_directory}/qe_config.json") + repo_config = cast(PythonRepositoryConfiguration, loaded_config) + else: + repo_config = PythonRepositoryConfiguration.default() + + # check for existing tox file + if os.path.exists(f"{self.cloned_repo_directory}/tox.ini"): + self.logger.info("Tox file exists.") + os.rename(f"{self.cloned_repo_directory}/tox.ini", + f"{self.cloned_repo_directory}/tox_default.ini") + + # render new tox file for tests + with open(f"{self.cloned_repo_directory}/tox.ini", "w") as tox_file: + tox_file.write(repo_config.render_tox_file( + ecosystem_deps=self.ecosystem_deps)) + + terra_version = "-" + if not os.path.exists(f"{self.cloned_repo_directory}/setup.py"): + self.logger.error("No setup.py file for repository %s", self.repo) + return terra_version, [] + + # run tox + tox_tests_res = _run_tox(directory=self.cloned_repo_directory, + env=self.python_version) + + # get terra version from file + if os.path.exists(f"{self.cloned_repo_directory}/terra_version.txt"): + with open(f"{self.cloned_repo_directory}/terra_version.txt", "r") as version_file: + terra_version = version_file.read() + self.logger.info("Terra version: %s", terra_version) + else: + self.logger.warning("There in no terra version file...") + + return terra_version, [tox_tests_res] diff --git a/ecosystem/entities.py b/ecosystem/entities.py index 28ae94d748..95e4b4178c 100644 --- a/ecosystem/entities.py +++ b/ecosystem/entities.py @@ -1,8 +1,7 @@ """Classes and controllers for json storage.""" -import inspect +import pprint from abc import ABC from datetime import datetime -import pprint from typing import Optional, List @@ -32,10 +31,68 @@ def all(cls): return [cls.STANDARD, cls.DEV_COMPATIBLE, cls.STABLE_COMPATIBLE] -class Repository(ABC): - """Main repository class.""" +class JsonSerializable(ABC): + """Classes that can be serialized as json.""" + + @classmethod + def from_dict(cls, dictionary: dict): + """Converts dict to object.""" + + def to_dict(self) -> dict: + """Converts repo to dict.""" + result = {} + for key, val in self.__dict__.items(): + if key.startswith("_"): + continue + element = [] + if isinstance(val, list): + for item in val: + if isinstance(item, JsonSerializable): + element.append(item.to_dict()) + else: + element.append(item) + elif isinstance(val, JsonSerializable): + element = val.to_dict() + else: + element = val + result[key] = element + return result - tier: str + +class TestResult(JsonSerializable): + """Tests status.""" + _TEST_PASSED: str = "passed" + _TEST_FAILED: str = "failed" + + def __init__(self, + passed: bool, + terra_version: str, + test_type: str): + self.test_type = test_type + self.passed = passed + self.terra_version = terra_version + + @classmethod + def from_dict(cls, dictionary: dict): + return TestResult(passed=dictionary.get("passed"), + terra_version=dictionary.get("terra_version"), + test_type=dictionary.get("test_type")) + + def to_string(self) -> str: + """Test result as string.""" + return self._TEST_PASSED if self.passed else self._TEST_FAILED + + def __eq__(self, other: 'TestResult'): + return self.passed == other.passed \ + and self.test_type == other.test_type \ + and self.terra_version == other.terra_version + + def __repr__(self): + return f"TestResult({self.passed}, {self.test_type}, {self.terra_version})" + + +class Repository(JsonSerializable): + """Main repository class.""" def __init__(self, name: str, @@ -47,9 +104,8 @@ def __init__(self, labels: Optional[List[str]] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None, - tier: Optional[str] = None, - tests_to_run: List[str] = None, - tests_passed: List[str] = None): + tier: str = Tier.MAIN, + tests_results: Optional[List[TestResult]] = None): """Repository controller. Args: @@ -62,8 +118,7 @@ def __init__(self, labels: labels created_at: creation date updated_at: update date - tests_to_run: tests need to be executed against repo - tests_passed: tests passed by repo + tests_results: tests passed by repo """ self.name = name self.url = url @@ -74,20 +129,24 @@ def __init__(self, self.labels = labels if labels is not None else [] self.created_at = created_at if created_at is not None else datetime.now().timestamp() self.updated_at = updated_at if updated_at is not None else datetime.now().timestamp() - self.tests_to_run = tests_to_run if tests_to_run else [] - self.tests_passed = tests_passed if tests_passed else [] - if tier: - self.tier = tier + self.tests_results = tests_results if tests_results else [] + self.tier = tier - def to_dict(self) -> dict: - """Converts repo to dict.""" - result = dict() - for name, value in inspect.getmembers(self): - if not name.startswith('_') and \ - not inspect.ismethod(value) and not inspect.isfunction(value) and \ - hasattr(self, name): - result[name] = value - return result + @classmethod + def from_dict(cls, dictionary: dict): + tests_results = [] + if "tests_results" in dictionary: + tests_results = [TestResult.from_dict(r) for r in dictionary.get("tests_results", [])] + + return Repository(name=dictionary.get("name"), + url=dictionary.get("url"), + description=dictionary.get("description"), + licence=dictionary.get("licence"), + contact_info=dictionary.get("contact_info"), + alternatives=dictionary.get("alternatives"), + labels=dictionary.get("labels"), + tier=dictionary.get("tier"), + tests_results=tests_results) def __eq__(self, other: 'Repository'): return (self.tier == other.tier @@ -102,19 +161,16 @@ def __str__(self): return f"Repository({self.tier} | {self.name} | {self.url})" -class MainRepository(Repository): - """Main tier repository.""" - tier = Tier.MAIN - - class CommandExecutionSummary: """Utils for command execution results.""" def __init__(self, code: int, logs: List[str], - summary: Optional[str] = None): + summary: Optional[str] = None, + name: Optional[str] = None): """CommandExecutionSummary class.""" + self.name = name or "" self.code = code self.logs = logs if summary: @@ -139,4 +195,4 @@ def empty(cls) -> 'CommandExecutionSummary': return cls(0, []) def __repr__(self): - return f"CommandExecutionSummary(code: {self.code} | {self.summary})" + return f"CommandExecutionSummary({self.name} | code: {self.code} | {self.summary})" diff --git a/ecosystem/manager.py b/ecosystem/manager.py index 6e28b2b456..d2ee67a428 100644 --- a/ecosystem/manager.py +++ b/ecosystem/manager.py @@ -5,14 +5,19 @@ from jinja2 import Environment, PackageLoader, select_autoescape from .controller import Controller -from .entities import Tier, TestType -from .commands import run_tests -from .logging import logger +from .controllers.runner import PythonRunner +from .entities import Tier, TestType, TestResult +from .utils import logger class Manager: """Manager class. Entrypoint for all CLI commands. + + Each public method of this class is CLI command + and arguments for method are options/flags for this command. + + Ex: `python manager.py generate_readme --path=` """ def __init__(self): @@ -27,6 +32,7 @@ def __init__(self): self.readme_template = self.env.get_template("readme.md") self.tox_template = self.env.get_template("tox.ini") self.controller = Controller(path=self.resources_dir) + self.logger = logger def generate_readme(self, path: Optional[str] = None): """Generates entire readme for ecosystem repository. @@ -40,80 +46,65 @@ def generate_readme(self, path: Optional[str] = None): with open(f"{path}/README.md", "w") as file: file.write(readme_content) - def _run(self, - repo_url: str, - tier: str, - test_type: str, - tox_python: str, - dependencies: Optional[List[str]] = None): - """Run tests on repository. + def _run_python_tests(self, + repo_url: str, + tier: str, + python_version: str, + test_type: str, + ecosystem_deps: Optional[List[str]] = None): + """Runs tests using python runner. Args: repo_url: repository url - tier: tier of membership - tox_python: tox env to run tests on - dependencies: list of extra dependencies to install + tier: tier of project + python_version: ex: py36, py37 etc + test_type: [dev, stable] + ecosystem_deps: extra dependencies to install for tests """ - try: - if dependencies is not None: - dev_tests_results = run_tests( - repo_url, - resources_dir=self.resources_dir, - tox_python=tox_python, - template_and_deps=(self.tox_template, dependencies)) - else: - dev_tests_results = run_tests( - repo_url, - resources_dir=self.resources_dir, - tox_python=tox_python) - # if all steps of test are successful - if all(c.ok for c in dev_tests_results.values()): - # update repo entry and assign successful tests - self.controller.add_repo_test_passed(repo_url=repo_url, - test_passed=test_type, - tier=tier) - else: - logger.warning("Some commands failed. Check logs.") - self.controller.remove_repo_test_passed(repo_url=repo_url, - test_remove=test_type, - tier=tier) - except Exception as exception: # pylint: disable=broad-except) - logger.error("Exception: %s", exception) - # remove from passed tests if anything went wrong - self.controller.remove_repo_test_passed(repo_url=repo_url, - test_remove=test_type, - tier=tier) - - def standard_tests(self, repo_url: str, - tier: str = Tier.MAIN, - tox_python: str = "py39"): - """Perform general checks for repository.""" - return self._run(repo_url=repo_url, - tier=tier, - test_type=TestType.STANDARD, - tox_python=tox_python) + ecosystem_deps = ecosystem_deps or [] + runner = PythonRunner(repo_url, + working_directory=self.resources_dir, + ecosystem_deps=ecosystem_deps, + python_version=python_version) + terra_version, results = runner.run() + if len(results) > 0: + test_result = TestResult(passed=all(r.ok for r in results), + terra_version=terra_version, + test_type=test_type) + # save test res to db + result = self.controller.add_repo_test_result(repo_url=repo_url, + tier=tier, + test_result=test_result) + # print report + if result is None: + self.logger.warning("Test result was not saved." + "There is not repo for url %s", repo_url) + self.logger.info("Test results for %s: %s", repo_url, test_result) + else: + self.logger.warning("Runner returned 0 results.") - def stable_compatibility_tests(self, - repo_url: str, - tier: str = Tier.MAIN, - tox_python: str = "py39"): - """Runs tests against stable version of Qiskit.""" - return self._run(repo_url=repo_url, - tier=tier, - test_type=TestType.STABLE_COMPATIBLE, - tox_python=tox_python, - dependencies=["qiskit"]) + return terra_version - def dev_compatibility_tests(self, - repo_url: str, - tier: str = Tier.MAIN, - tox_python: str = "py39"): - """Runs tests against dev version of Qiskit (main branch).""" - return self._run(repo_url=repo_url, - tier=tier, - test_type=TestType.DEV_COMPATIBLE, - tox_python=tox_python, - dependencies=["git+https://github.com/Qiskit/qiskit-terra.git@main"]) + def python_dev_tests(self, + repo_url: str, + tier: str = Tier.MAIN, + python_version: str = "py39"): + """Runs tests against dev version of qiskit.""" + return self._run_python_tests( + repo_url=repo_url, + tier=tier, + python_version=python_version, + test_type=TestType.DEV_COMPATIBLE, + ecosystem_deps=["git+https://github.com/Qiskit/qiskit-terra.git@main"]) - def __repr__(self): - return "Manager(CLI entrypoint)" + def python_stable_tests(self, + repo_url: str, + tier: str = Tier.MAIN, + python_version: str = "py39"): + """Runs tests against stable version of qiskit.""" + return self._run_python_tests( + repo_url=repo_url, + tier=tier, + python_version=python_version, + test_type=TestType.STABLE_COMPATIBLE, + ecosystem_deps=["qiskit"]) diff --git a/ecosystem/models/__init__.py b/ecosystem/models/__init__.py new file mode 100644 index 0000000000..49e183ec57 --- /dev/null +++ b/ecosystem/models/__init__.py @@ -0,0 +1,3 @@ +"""Models for ecosystem.""" + +from .configuration import RepositoryConfiguration, PythonRepositoryConfiguration diff --git a/ecosystem/models/configuration.py b/ecosystem/models/configuration.py new file mode 100644 index 0000000000..4e89a00f0b --- /dev/null +++ b/ecosystem/models/configuration.py @@ -0,0 +1,112 @@ +"""Configuration for ecosystem repository.""" +import json +import pprint +from typing import Optional, List + +from jinja2 import Environment, PackageLoader, select_autoescape + +from ecosystem.entities import JsonSerializable +from ecosystem.utils import QiskitEcosystemException + + +class Languages: + """Supported configuration languages.""" + PYTHON: str = "python" + + def all(self) -> List[str]: + """Return all supported languages.""" + return [self.PYTHON] + + def __repr__(self): + return "Languages({})".format(",".join(self.all())) + + +class RepositoryConfiguration(JsonSerializable): + """Configuration for ecosystem repository.""" + + def __init__(self, + language: str = Languages.PYTHON, + dependencies_files: Optional[List[str]] = None, + extra_dependencies: Optional[List[str]] = None, + tests_command: Optional[List[str]] = None, + styles_check_command: Optional[List[str]] = None, ): + """Configuration for ecosystem repository. + + Args: + language: repository language + dependencies_files: list of dependencies files paths relative to root of repo + ex: for python `requirements.txt` + extra_dependencies: list of extra dependencies for project to install during tests run + ex: for python it might be `qiskit==0.19` + tests_command: list of commands to run tests + ex: for python `python -m unittest -v` + styles_check_command: list of commands to run style checks + ex: for python `pylint -rn ecosystem tests` + """ + self.language = language + self.dependencies_files = dependencies_files or [] + self.extra_dependencies = extra_dependencies or [] + self.tests_command = tests_command or [] + self.styles_check_command = styles_check_command or [] + + def save(self, path: str): + """Saves configuration as json file.""" + with open(path, "w") as json_file: + json.dump(self.to_dict(), json_file, indent=4) + + @classmethod + def load(cls, path: str) -> 'RepositoryConfiguration': + """Loads json file into object.""" + with open(path, "r") as json_file: + config: RepositoryConfiguration = json.load( + json_file, object_hook=lambda d: RepositoryConfiguration(**d)) + if config.language == Languages.PYTHON: # pylint: disable=no-else-return + return PythonRepositoryConfiguration( + dependencies_files=config.dependencies_files, + extra_dependencies=config.extra_dependencies, + tests_command=config.tests_command, + styles_check_command=config.styles_check_command) + else: + raise QiskitEcosystemException( + f"Unsupported language configuration type: {config.language}") + + def __repr__(self): + return pprint.pformat(self.to_dict(), indent=4) + + +class PythonRepositoryConfiguration(RepositoryConfiguration): + """Repository configuration for python based projects.""" + + def __init__(self, + dependencies_files: Optional[List[str]] = None, + extra_dependencies: Optional[List[str]] = None, + tests_command: Optional[List[str]] = None, + styles_check_command: Optional[List[str]] = None): + super().__init__(language=Languages.PYTHON, + dependencies_files=dependencies_files, + extra_dependencies=extra_dependencies, + tests_command=tests_command, + styles_check_command=styles_check_command) + env = Environment( + loader=PackageLoader("ecosystem"), + autoescape=select_autoescape() + ) + self.tox_template = env.get_template("configured_tox.ini") + + @classmethod + def default(cls) -> 'PythonRepositoryConfiguration': + """Returns default python repository configuration.""" + return PythonRepositoryConfiguration( + dependencies_files=[ + "requirements.txt" + ], + tests_command=[ + "pip check", + "pytest -W error::DeprecationWarning" + ]) + + def render_tox_file(self, ecosystem_deps: List[str] = None): + """Renders tox template from configuration.""" + ecosystem_deps = ecosystem_deps or [] + return self.tox_template.render({**self.to_dict(), + **{'ecosystem_deps': ecosystem_deps}}) diff --git a/ecosystem/resources/members.json b/ecosystem/resources/members.json index 4e707c5c5f..17a79e7bdb 100644 --- a/ecosystem/resources/members.json +++ b/ecosystem/resources/members.json @@ -13,7 +13,7 @@ "tier": "MAIN", "updated_at": 1628883441.117589, "url": "https://github.com/Qiskit/qiskit", - "tests_passed": [] + "tests_results": [] }, "2": { "alternatives": null, @@ -29,7 +29,7 @@ "tier": "MAIN", "updated_at": 1628883441.118061, "url": "https://github.com/Qiskit/qiskit-terra", - "tests_passed": [] + "tests_results": [] }, "3": { "alternatives": null, @@ -44,7 +44,7 @@ "tier": "MAIN", "updated_at": 1628883441.118472, "url": "https://github.com/Qiskit/qiskit-aer", - "tests_passed": [] + "tests_results": [] }, "4": { "alternatives": null, @@ -60,7 +60,7 @@ "tier": "MAIN", "updated_at": 1628883441.118821, "url": "https://github.com/Qiskit/qiskit-optimization", - "tests_passed": [] + "tests_results": [] }, "5": { "alternatives": null, @@ -76,7 +76,7 @@ "tier": "MAIN", "updated_at": 1628883441.119205, "url": "https://github.com/Qiskit/qiskit-metal", - "tests_passed": [] + "tests_results": [] }, "6": { "alternatives": null, @@ -92,7 +92,7 @@ "tier": "MAIN", "updated_at": 1628883441.119529, "url": "https://github.com/Qiskit/qiskit-machine-learning", - "tests_passed": [] + "tests_results": [] }, "7": { "alternatives": null, @@ -109,7 +109,7 @@ "tier": "MAIN", "updated_at": 1628883441.119862, "url": "https://github.com/Qiskit/qiskit-nature", - "tests_passed": [] + "tests_results": [] }, "8": { "alternatives": null, @@ -125,7 +125,7 @@ "tier": "MAIN", "updated_at": 1628883441.120268, "url": "https://github.com/Qiskit/qiskit-finance", - "tests_passed": [] + "tests_results": [] }, "9": { "alternatives": null, @@ -141,7 +141,7 @@ "tier": "MAIN", "updated_at": 1628883441.120709, "url": "https://github.com/Qiskit/qiskit-tutorials", - "tests_passed": [] + "tests_results": [] }, "10": { "alternatives": null, @@ -157,7 +157,7 @@ "tier": "MAIN", "updated_at": 1628883441.121111, "url": "https://github.com/Qiskit/qiskit.org", - "tests_passed": [] + "tests_results": [] } } } \ No newline at end of file diff --git a/ecosystem/templates/configured_tox.ini b/ecosystem/templates/configured_tox.ini new file mode 100644 index 0000000000..111c6409a4 --- /dev/null +++ b/ecosystem/templates/configured_tox.ini @@ -0,0 +1,28 @@ +[tox] +minversion = 3.6 +envlist = py36, py37, py38, py39 +skipsdist = True + +[testenv] +usedevelop = true +install_command = pip install -U {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} + LANGUAGE=en_US + LC_ALL=en_US.utf-8 +deps = pytest +{% for dep_file in dependencies_files -%} + {{"-r"|indent(7, True)}} {{ dep_file }} +{% endfor -%} +{% for dep in extra_dependencies -%} + {{ dep|indent(7, True) }} +{% endfor -%} +{% for dep in ecosystem_deps -%} + {{ dep|indent(7, True) }} +{% endfor -%} +commands = + python -c 'import qiskit; f = open("./terra_version.txt", "w"); f.write(qiskit.__qiskit_version__["qiskit-terra"]); f.close();' +{% for command in tests_command -%} + {{ command|indent(2, True) }} +{% endfor -%} + diff --git a/ecosystem/logging.py b/ecosystem/utils.py similarity index 88% rename from ecosystem/logging.py rename to ecosystem/utils.py index 8636e9b96e..882867862a 100644 --- a/ecosystem/logging.py +++ b/ecosystem/utils.py @@ -3,6 +3,10 @@ import os +class QiskitEcosystemException(Exception): + """Exceptions for qiskit ecosystem.""" + + class OneLineExceptionFormatter(logging.Formatter): """Exception formatter""" def formatException(self, ei): diff --git a/tests/common.py b/tests/common.py new file mode 100644 index 0000000000..63a2579e62 --- /dev/null +++ b/tests/common.py @@ -0,0 +1,18 @@ +"""Common test classes.""" +import os +import shutil +import unittest + + +class TestCaseWithResources(unittest.TestCase): + """Test case with additional resources folder.""" + path: str + + def setUp(self) -> None: + self.path = "./resources/tests_tmp_data" + if not os.path.exists(self.path): + os.makedirs(self.path) + + def tearDown(self) -> None: + if os.path.exists(self.path): + shutil.rmtree(self.path) diff --git a/tests/resources/configured_python_repository/qe_config.json b/tests/resources/configured_python_repository/qe_config.json new file mode 100644 index 0000000000..e0b9a278fb --- /dev/null +++ b/tests/resources/configured_python_repository/qe_config.json @@ -0,0 +1,13 @@ +{ + "dependencies_files": [ + "requirements.txt", + "requirements-dev.txt" + ], + "extra_dependencies": [ + "pytest" + ], + "language": "python", + "tests_command": [ + "pytest" + ] +} \ No newline at end of file diff --git a/tests/resources/configured_python_repository/qlib/__init__.py b/tests/resources/configured_python_repository/qlib/__init__.py new file mode 100644 index 0000000000..ceb3b68a52 --- /dev/null +++ b/tests/resources/configured_python_repository/qlib/__init__.py @@ -0,0 +1,2 @@ +"""Docstring.""" +from .impl import Impl diff --git a/tests/resources/configured_python_repository/qlib/impl.py b/tests/resources/configured_python_repository/qlib/impl.py new file mode 100644 index 0000000000..11b9eaad49 --- /dev/null +++ b/tests/resources/configured_python_repository/qlib/impl.py @@ -0,0 +1,18 @@ +"""Docstring.""" +import math +from typing import Union + + +class Impl: + """Demo impl.""" + def __init__(self): + """Demo impl.""" + self.pow = 2 + + def run(self, number: Union[int, float]) -> Union[int, float]: + """Run method.""" + from collections import Hashable + return math.pow(number, self.pow) + + def __repr__(self): + return f"Impl(pow: {self.pow})" diff --git a/tests/resources/configured_python_repository/requirements-dev.txt b/tests/resources/configured_python_repository/requirements-dev.txt new file mode 100644 index 0000000000..95ea1e6a02 --- /dev/null +++ b/tests/resources/configured_python_repository/requirements-dev.txt @@ -0,0 +1 @@ +pytest==6.2.4 diff --git a/tests/resources/configured_python_repository/requirements.txt b/tests/resources/configured_python_repository/requirements.txt new file mode 100644 index 0000000000..95ea1e6a02 --- /dev/null +++ b/tests/resources/configured_python_repository/requirements.txt @@ -0,0 +1 @@ +pytest==6.2.4 diff --git a/tests/resources/configured_python_repository/setup.cfg b/tests/resources/configured_python_repository/setup.cfg new file mode 100644 index 0000000000..b88034e414 --- /dev/null +++ b/tests/resources/configured_python_repository/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/tests/resources/configured_python_repository/setup.py b/tests/resources/configured_python_repository/setup.py new file mode 100644 index 0000000000..203a2d4621 --- /dev/null +++ b/tests/resources/configured_python_repository/setup.py @@ -0,0 +1,15 @@ +"""Setup file for demo-impl.""" + +import setuptools + +with open('requirements.txt') as fp: + install_requires = fp.read() + +setuptools.setup( + name="demo-impl", + description="demo-impl", + long_description="", + packages=setuptools.find_packages(), + install_requires=install_requires, + python_requires='>=3.6' +) diff --git a/tests/resources/configured_python_repository/tests/__init__.py b/tests/resources/configured_python_repository/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/resources/configured_python_repository/tests/test_impl.py b/tests/resources/configured_python_repository/tests/test_impl.py new file mode 100644 index 0000000000..c753c27d8c --- /dev/null +++ b/tests/resources/configured_python_repository/tests/test_impl.py @@ -0,0 +1,16 @@ +"""Docstring.""" +import unittest +import warnings + +from qlib import Impl + + +class TestImpl(unittest.TestCase): + """Tests Impl class implementation.""" + + def test_run(self): + """Tests run method implementation.""" + impl = Impl() + + warnings.warn("test warning", DeprecationWarning) + self.assertEqual(impl.run(2), 4) diff --git a/tests/resources/simple_python_repository/qlib/__init__.py b/tests/resources/simple_python_repository/qlib/__init__.py new file mode 100644 index 0000000000..ceb3b68a52 --- /dev/null +++ b/tests/resources/simple_python_repository/qlib/__init__.py @@ -0,0 +1,2 @@ +"""Docstring.""" +from .impl import Impl diff --git a/tests/resources/simple_python_repository/qlib/impl.py b/tests/resources/simple_python_repository/qlib/impl.py new file mode 100644 index 0000000000..11b9eaad49 --- /dev/null +++ b/tests/resources/simple_python_repository/qlib/impl.py @@ -0,0 +1,18 @@ +"""Docstring.""" +import math +from typing import Union + + +class Impl: + """Demo impl.""" + def __init__(self): + """Demo impl.""" + self.pow = 2 + + def run(self, number: Union[int, float]) -> Union[int, float]: + """Run method.""" + from collections import Hashable + return math.pow(number, self.pow) + + def __repr__(self): + return f"Impl(pow: {self.pow})" diff --git a/tests/resources/simple_python_repository/requirements.txt b/tests/resources/simple_python_repository/requirements.txt new file mode 100644 index 0000000000..95ea1e6a02 --- /dev/null +++ b/tests/resources/simple_python_repository/requirements.txt @@ -0,0 +1 @@ +pytest==6.2.4 diff --git a/tests/resources/simple_python_repository/setup.cfg b/tests/resources/simple_python_repository/setup.cfg new file mode 100644 index 0000000000..b88034e414 --- /dev/null +++ b/tests/resources/simple_python_repository/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/tests/resources/simple_python_repository/setup.py b/tests/resources/simple_python_repository/setup.py new file mode 100644 index 0000000000..203a2d4621 --- /dev/null +++ b/tests/resources/simple_python_repository/setup.py @@ -0,0 +1,15 @@ +"""Setup file for demo-impl.""" + +import setuptools + +with open('requirements.txt') as fp: + install_requires = fp.read() + +setuptools.setup( + name="demo-impl", + description="demo-impl", + long_description="", + packages=setuptools.find_packages(), + install_requires=install_requires, + python_requires='>=3.6' +) diff --git a/tests/resources/simple_python_repository/tests/__init__.py b/tests/resources/simple_python_repository/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/resources/simple_python_repository/tests/test_impl.py b/tests/resources/simple_python_repository/tests/test_impl.py new file mode 100644 index 0000000000..c753c27d8c --- /dev/null +++ b/tests/resources/simple_python_repository/tests/test_impl.py @@ -0,0 +1,16 @@ +"""Docstring.""" +import unittest +import warnings + +from qlib import Impl + + +class TestImpl(unittest.TestCase): + """Tests Impl class implementation.""" + + def test_run(self): + """Tests run method implementation.""" + impl = Impl() + + warnings.warn("test warning", DeprecationWarning) + self.assertEqual(impl.run(2), 4) diff --git a/tests/test_commands.py b/tests/test_commands.py index 7816cba93a..083eec8c81 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,22 +1,13 @@ """Tests for shell commands.""" import os -import shutil -from unittest import TestCase from ecosystem.commands import _execute_command, _clone_repo from ecosystem.entities import CommandExecutionSummary +from .common import TestCaseWithResources -class TestCommands(TestCase): +class TestCommands(TestCaseWithResources): """Tests shell commands.""" - def setUp(self) -> None: - self.path = "./resources" - if not os.path.exists(self.path): - os.makedirs(self.path) - - def tearDown(self) -> None: - if os.path.exists(self.path): - shutil.rmtree(self.path) def test_execute_command(self): """Tests command execution.""" diff --git a/tests/test_configuration.py b/tests/test_configuration.py new file mode 100644 index 0000000000..ee4fb2ad19 --- /dev/null +++ b/tests/test_configuration.py @@ -0,0 +1,38 @@ +"""Tests for configuration files.""" + +from ecosystem.models import RepositoryConfiguration, PythonRepositoryConfiguration +from tests.common import TestCaseWithResources + + +class TestRepositoryConfiguration(TestCaseWithResources): + """Tests for RepositoryConfiguration file.""" + + def test_save_and_load(self): + """Tests saving and loading of configuration,""" + config = RepositoryConfiguration(dependencies_files=["requirements.txt", + "requirements-dev.txt"], + extra_dependencies=["qiskit"], + tests_command=["python -m unittest -v"], + styles_check_command=["pylint -rn ecosystem tests"]) + save_path = f"{self.path}/config.json" + config.save(save_path) + + recovered_config = RepositoryConfiguration.load(save_path) + + self.assertEqual(config.language, recovered_config.language) + self.assertEqual(config.tests_command, recovered_config.tests_command) + self.assertEqual(config.dependencies_files, recovered_config.dependencies_files) + self.assertEqual(config.extra_dependencies, recovered_config.extra_dependencies) + self.assertEqual(config.styles_check_command, recovered_config.styles_check_command) + + def test_python_configuration(self): + """Tests python configurations.""" + config = PythonRepositoryConfiguration.default() + rendered_tox = config.render_tox_file() + self.assertTrue(config) + for command in config.tests_command: + self.assertTrue(command in rendered_tox) + for dep in config.extra_dependencies: + self.assertTrue(dep in rendered_tox) + for dep_file in config.dependencies_files: + self.assertTrue(dep_file in rendered_tox) diff --git a/tests/test_controller.py b/tests/test_controller.py index 6d00e1a407..4ca62f4417 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -1,10 +1,21 @@ """Tests for entities.""" import os from unittest import TestCase -from ecosystem.entities import MainRepository, TestType +from ecosystem.entities import Repository, TestResult, TestType from ecosystem.controller import Controller +def get_main_repo() -> Repository: + """Return main mock repo.""" + return Repository(name="mock-qiskit-terra-with-success-dev-test", + url="https://github.com/MockQiskit/mock-qiskit-wsdt.terra", + description="Mock description for repo. wsdt", + licence="Apache 2.0", + labels=["mock", "tests", "wsdt"], + tests_results=[ + TestResult(True, "0.18.1", TestType.DEV_COMPATIBLE)]) + + class TestController(TestCase): """Tests repository related functions.""" @@ -29,11 +40,7 @@ def test_repository_insert_and_delete(self): """Tests repository.""" self._delete_members_json() - main_repo = MainRepository(name="mock-qiskit-terra", - url="https://github.com/MockQiskit/mock-qiskit.terra", - description="Mock description for repo.", - licence="Apache 2.0", - labels=["mock", "tests"]) + main_repo = get_main_repo() controller = Controller(self.path) # insert entry @@ -41,60 +48,35 @@ def test_repository_insert_and_delete(self): fetched_repo = controller.get_all_main()[0] self.assertEqual(main_repo, fetched_repo) self.assertEqual(main_repo.labels, fetched_repo.labels) + self.assertEqual(len(fetched_repo.tests_results), 1) # delete entry controller.delete(main_repo) self.assertEqual([], controller.get_all_main()) - def test_add_and_remove_repo_test_passed(self): - """Tests addition of passed test to repo.""" + def test_add_test_result(self): + """Tests adding result to repo.""" self._delete_members_json() - main_repo = MainRepository(name="mock-qiskit-terra", - url="https://github.com/MockQiskit/mock-qiskit.terra", - description="Mock description for repo.", - licence="Apache 2.0", - labels=["mock", "tests"]) controller = Controller(self.path) - controller.insert(main_repo) - controller.add_repo_test_passed(repo_url=main_repo.url, - test_passed=TestType.STANDARD, - tier=main_repo.tier) - fetched_repo = controller.get_by_url(main_repo.url, tier=main_repo.tier) - self.assertEqual(fetched_repo.tests_passed, [TestType.STANDARD]) - - controller.remove_repo_test_passed(repo_url=main_repo.url, - test_remove=TestType.STANDARD, - tier=main_repo.tier) - fetched_repo = controller.get_by_url(main_repo.url, tier=main_repo.tier) - self.assertEqual(fetched_repo.tests_passed, []) - - def test_update_repo_tests_passed(self): - """Tests repository tests passed field.""" - self._delete_members_json() - - url = "https://github.com/MockQiskit/mock-qiskit.terra" - main_repo = MainRepository(name="mock-qiskit-terra", - url=url, - description="Mock description for repo.", - licence="Apache 2.0", - labels=["mock", "tests"]) - controller = Controller(self.path) + main_repo = get_main_repo() controller.insert(main_repo) - - controller.update_repo_tests_passed(main_repo, [TestType.STANDARD]) - fetched_repo = controller.get_by_url(url, main_repo.tier) - self.assertEqual(len(fetched_repo.tests_passed), 1) - self.assertEqual(fetched_repo.tests_passed, [TestType.STANDARD]) - - controller.update_repo_tests_passed(main_repo, [TestType.STANDARD, - TestType.DEV_COMPATIBLE]) - fetched_repo = controller.get_by_url(url, main_repo.tier) - self.assertEqual(len(fetched_repo.tests_passed), 2) - self.assertEqual(fetched_repo.tests_passed, [TestType.STANDARD, - TestType.DEV_COMPATIBLE]) - - controller.update_repo_tests_passed(main_repo, []) - fetched_repo = controller.get_by_url(url, main_repo.tier) - self.assertEqual(len(fetched_repo.tests_passed), 0) - self.assertEqual(fetched_repo.tests_passed, []) + res = controller.add_repo_test_result(main_repo.url, + main_repo.tier, + TestResult(False, "0.18.1", + TestType.DEV_COMPATIBLE)) + self.assertEqual(res, [1]) + recovered_repo = controller.get_by_url(main_repo.url, tier=main_repo.tier) + self.assertEqual(recovered_repo.tests_results, [TestResult(False, "0.18.1", + TestType.DEV_COMPATIBLE)]) + + res = controller.add_repo_test_result(main_repo.url, + main_repo.tier, + TestResult(True, "0.18.2", + TestType.DEV_COMPATIBLE)) + self.assertEqual(res, [1]) + recovered_repo = controller.get_by_url(main_repo.url, tier=main_repo.tier) + self.assertEqual(recovered_repo.tests_results, [TestResult(False, "0.18.1", + TestType.DEV_COMPATIBLE), + TestResult(True, "0.18.2", + TestType.DEV_COMPATIBLE)]) diff --git a/tests/test_entities.py b/tests/test_entities.py new file mode 100644 index 0000000000..4f0f551a00 --- /dev/null +++ b/tests/test_entities.py @@ -0,0 +1,24 @@ +"""Tests for entities.""" + +import unittest + +from ecosystem.entities import Repository, TestResult, TestType + + +class TestRepository(unittest.TestCase): + """Tests repository class.""" + + def test_serialization(self): + """Tests json serialization.""" + main_repo = Repository(name="mock-qiskit-terra", + url="https://github.com/MockQiskit/mock-qiskit.terra", + description="Mock description for repo.", + licence="Apache 2.0", + labels=["mock", "tests"], + tests_results=[ + TestResult(True, '0.18.1', TestType.DEV_COMPATIBLE) + ]) + repo_dict = main_repo.to_dict() + recovered = Repository.from_dict(repo_dict) + self.assertEqual(main_repo, recovered) + self.assertEqual(main_repo.tests_results, recovered.tests_results) diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 0000000000..c1d8881a4f --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,48 @@ +"""Tests for runners.""" +import os +import unittest + +from ecosystem.controllers.runner import PythonRunner + + +class TestPythonRunner(unittest.TestCase): + """Tests for Python runner.""" + + def setUp(self) -> None: + current_directory = os.path.dirname(os.path.abspath(__file__)) + self.simple_project_dir = f"{current_directory}/" \ + f"resources/simple_python_repository" + self.configured_project_dir = f"{current_directory}/" \ + f"resources/configured_python_repository" + + def tearDown(self) -> None: + files_to_delete = ["tox.ini", "terra_version.txt"] + for directory in [self.simple_project_dir, + self.configured_project_dir]: + for file in files_to_delete: + if os.path.exists(f"{directory}/{file}"): + os.remove(f"{directory}/{file}") + + def test_runner_on_simple_repo(self): + """Simple runner test.""" + runner = PythonRunner("test", + working_directory=self.simple_project_dir, + ecosystem_deps=["qiskit"]) + + runner.cloned_repo_directory = self.simple_project_dir + terra_version, result = runner.workload() + + self.assertFalse(all(r.ok for r in result)) + self.assertTrue(terra_version) + + def test_runner_on_configured_repo(self): + """Configured repo runner test.""" + runner = PythonRunner("test", + working_directory=self.configured_project_dir, + ecosystem_deps=["qiskit"]) + + runner.cloned_repo_directory = self.configured_project_dir + terra_version, result = runner.workload() + + self.assertTrue(all(r.ok for r in result)) + self.assertTrue(terra_version)