Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configuration for ecosystem members #8

Merged
merged 13 commits into from
Sep 20, 2021
18 changes: 5 additions & 13 deletions .github/workflows/check-main-repos.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Test main tier of ecosystem

on:
schedule:
- cron: '5 8 * * 2' # each Tuesday at 8 05
workflow_dispatch:

jobs:
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
tests:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
max-parallel: 2
matrix:
python-version: [3.9]
steps:
Expand Down
11 changes: 10 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
46 changes: 46 additions & 0 deletions docs/dev/dev-doc.md
Original file line number Diff line number Diff line change
@@ -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 <NAME_OF_FUNCTION_IN_MANAGER_FILE> <POSITIONAL_ARGUMENT> [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"
]
}
```
12 changes: 8 additions & 4 deletions ecosystem/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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):
Expand Down
61 changes: 22 additions & 39 deletions ecosystem/controller.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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."""
Expand Down
Empty file.
137 changes: 137 additions & 0 deletions ecosystem/controllers/runner.py
Original file line number Diff line number Diff line change
@@ -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]