Skip to content

Fix/skip test when no python avail #4

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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 191 additions & 92 deletions dku_plugin_test_utils/pytest_plugin/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@
from dku_plugin_test_utils.run_config import ScenarioConfiguration
from dku_plugin_test_utils.run_config import PluginInfo


# Entry point for integration test cession, load the logger configuration
Log()

logger = logging.getLogger("dss-plugin-test.pytest_plugin")

logger.setLevel(logging.DEBUG)

def pytest_addoption(parser):
parser.addoption(
"--exclude-dss-targets", action="store", help="\"Target,[other targets]\". Exclude DSS target from the instance configuration file."
"--exclude-dss-targets",
action="store",
help='"Target,[other targets]". Exclude DSS target from the instance configuration file.',
)


def pytest_generate_tests(metafunc):
"""
Pytest exposed hook allowing to dynamically alterate the pytest representation of a test which is metafunc
Here we use that hook to dynamically paramertrize the "client" fixture of each tests.
Pytest exposed hook allowing to dynamically change the pytest representation
of a test which is metafunc.
Here we use that hook to dynamically parameterize the "client" fixture of each test.
Therefore, a new client will be instantiated for each DSS instance.

Args:
Expand All @@ -43,9 +45,13 @@ def pytest_generate_tests(metafunc):
targets = set(targets)

if excluded_targets.isdisjoint(targets):
raise RuntimeError("You have excluded non existing DSS targets. Actual DSS targets : {}".format(','.join(targets)))
raise RuntimeError(
"You have excluded non existing DSS targets. Actual DSS targets : {}".format(
",".join(targets)
)
)

# substract the excluded target from the target
# subtract the excluded target from the target
targets = list(targets - excluded_targets)

if len(targets) == 0:
Expand All @@ -54,49 +60,93 @@ def pytest_generate_tests(metafunc):
metafunc.parametrize("dss_target", targets, indirect=["dss_target"])


@pytest.fixture(scope="function")
@pytest.fixture(scope="session")
def dss_target(request):
"""
This is a parameterized fixture. Its value will be set with the different DSS target (DSS7, DSS8 ...) that are specified in the configuration file.
It returns the value of the considered DSS target for the test. Here it is only used by other fixtures, but one could use it
This is a parameterized fixture.
Its value will be set with the different DSS target (DSS7, DSS8 ...)
that are specified in the configuration file.
It returns the value of the considered DSS target for the test.
Here it is only used by other fixtures, but one could use it
as a test function parameter to access its value inside the test function.

Args:
request: The object to introspect the “requesting” test function, class or module context
request: The object to introspect the “requesting”
test function, class or module context

Returns:
The string corresponding to the considered DSS target for the test to be executed
The string corresponding to the considered DSS target for the
test to be executed
"""
return request.param

dss_target = request.param

current_run_config = ScenarioConfiguration()
current_plugin_config = PluginInfo().plugin_metadata

target_dss_available_interpreter = set(
current_run_config.full_config[dss_target]["python_interpreter"]
)

plugin_python_interpreter = set(
current_plugin_config["python_interpreter"]
if "python_interpreter" in current_plugin_config
else []
)

python_interpreters_for_code_env = list(
target_dss_available_interpreter.intersection(plugin_python_interpreter)
)
if not python_interpreters_for_code_env:
raise pytest.skip(
(
"No common python interpreter could be found "
"between the DSS target and the ones ask by the plugin [{plugin_id}]"
"From plugin: {plugin_interpreters}, "
"From target: {target_interpreters}"
).format(
plugin_id=current_plugin_config["id"],
plugin_interpreters=",".join(plugin_python_interpreter),
target_interpreters=",".join(target_dss_available_interpreter),
)
)

return dss_target


@pytest.fixture(scope="function")
def user_dss_clients(dss_clients, dss_target):
"""
Fixture that narrows down the dss clients to only the ones that are relevant considering the curent DSS target.
Fixture that narrows down the dss clients to only the ones that are relevant
considering the current DSS target.

Args:
dss_clients (fixture): All the instanciated dss client for each user and dss targets
dss_clients (fixture): All the dss client instances for
each user and dss targets
dss_target (fixture): The considered DSS target for the test to be executed

Returns:
A dict of dss client instances for the current DSS target and each of its specified users.
A dict of dss client instances for the current DSS target
and each of its specified users.
"""
return dss_clients[dss_target]


@pytest.fixture(scope="module")
@pytest.fixture(scope="session")
def dss_clients(request):
"""
The client fixture that is used by each of the test that will target a DSS instance.
The scope of that fixture is set to module, so upon exiting a test module the fixture is destroyed
The client fixture that is used by each of the tests that
will target a DSS instance.
The scope of that fixture is set to module, so upon exiting a test module,
the fixture is destroyed

Args:
request: A pytest obejct allowing to introspect the test context. It allows us to access
the value of host set in `pytest_generate_tests`
request: A pytest object allowing to introspect the test context.
It allows us to access the value of host set in `pytest_generate_tests`

Returns:
dssclient: return a instance of a DSS client. It will be the same reference for each test withing the associated context.
dssclient: return a instance of a DSS client.
It will be the same reference for each test withing the associated context.
"""
dss_clients = {}
current_run_config = ScenarioConfiguration()
Expand All @@ -113,106 +163,155 @@ def dss_clients(request):
dss_clients.update({target: {}})
url = host["url"]
for user, api_key in host["users"].items():
dss_clients[target].update({user: dataikuapi.DSSClient(url, api_key=api_key)})
dss_clients[target].update(
{user: dataikuapi.DSSClient(url, api_key=api_key)}
)

return dss_clients


@pytest.fixture(scope="module")
def plugin(dss_clients):
@pytest.fixture(scope="session")
def plugin(dss_clients, dss_target):
"""
The plugin fixture that is used by each of the test. It depends on the client fixture, as it needs to be
The plugin fixture that is used by each of the tests.
It depends on the client fixture, as it needs to be
uploaded on the proper DSS instance using the admin user.
The scope of that fixture is set to module, so upon exiting a test module the fixture is destroyed
The scope of that fixture is set to module,
so upon exiting a test module, the fixture is destroyed

Args:
client: A DSS client instance.
dss_clients: A DSS client instance.
dss_target:
"""
logger.setLevel(logging.DEBUG)

logger.info("Uploading the pluging to each DSS instances [{}]".format(",".join(dss_clients.keys())))
p = subprocess.Popen(['make', 'plugin'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info(
f"Uploading the plugin to [{dss_target}] instance"
)
p = subprocess.Popen(
["make", "plugin"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
stdout, stderr = p.communicate()
return_code = p.returncode
if return_code != 0:
raise RuntimeError("Error while compiling the plugin. \n Make command stderr : \n - stderr:\n{}".format(stderr.decode("utf-8")))

logger.debug("make command output:\n - stdout:\n{}\n - stderr:\n{}".format(stdout.decode("utf-8"), stderr.decode("utf-8")))
raise RuntimeError(
f"Error while compiling the plugin. \n "
f"Make command stderr : \n"
f" - stderr:\n{stderr.decode('utf-8')}"
)

logger.debug(
f"make command output:\n "
f"- stdout:\n{stdout.decode('utf-8')}\n "
f"- stderr:\n{stderr.decode('utf-8')}"
)

info = PluginInfo().plugin_metadata
plugin_zip_name = "dss-plugin-{plugin_id}-{plugin_version}.zip".format(plugin_id=info["id"], plugin_version=info["version"])
plugin_zip_name = f"dss-plugin-{info['id']}-{info['version']}.zip"
plugin_zip_path = os.path.join(os.getcwd(), "dist", plugin_zip_name)

uploaded_plugin = None

for target in dss_clients:
admin_client = dss_clients[target]["admin"]
get_plugin_ids = itemgetter("id")
available_plugins = list(map(get_plugin_ids, admin_client.list_plugins()))
if info["id"] in available_plugins:
logger.debug("Plugin [{plugin_id}] is already installed on [{dss_target}], updating it".format(plugin_id=info["id"], dss_target=target))
with open(plugin_zip_path, 'rb') as fd:
uploaded_plugin = admin_client.get_plugin(info["id"])
uploaded_plugin.update_from_zip(fd)
admin_client = dss_clients[dss_target]["admin"]
get_plugin_ids = itemgetter("id")
available_plugins = list(map(get_plugin_ids, admin_client.list_plugins()))
if info["id"] in available_plugins:
logger.debug(
f"Plugin [{info['id']}] is already installed on [{dss_target}], updating it"
)
with open(plugin_zip_path, "rb") as fd:
uploaded_plugin = admin_client.get_plugin(info["id"])
uploaded_plugin.update_from_zip(fd)
else:
logger.debug(
f"Plugin [{info['id']}] is not installed on [{dss_target}], installing it"
)
with open(plugin_zip_path, "rb") as fd:
admin_client.install_plugin_from_archive(fd)
uploaded_plugin = admin_client.get_plugin(info["id"])

plugin_settings = uploaded_plugin.get_settings()
raw_plugin_settings = plugin_settings.get_raw()

# install (or reinstall) code-env only if the plugin has a specific
# code-env defined (not using DSS built-in):
if PluginInfo().plugin_codenv_metadata is not None:
if (
"codeEnvName" in raw_plugin_settings
and len(raw_plugin_settings["codeEnvName"]) != 0
):
logger.debug(
f"Code env [{raw_plugin_settings['codeEnvName']}] "
f"is already associated to [{info['id']}] "
f"on [{dss_target}], deleting it"
)

code_env_list = admin_client.list_code_envs()
code_env_info = list(
filter(
lambda x: x["envName"] == raw_plugin_settings["codeEnvName"],
code_env_list,
)
)
if code_env_info:
code_env_info = code_env_info[0]
code_env = admin_client.get_code_env(
code_env_info["envLang"], code_env_info["envName"]
)
code_env.delete()
logger.debug(
f"Code env [{raw_plugin_settings['codeEnvName']}] is deleted. "
f"Creating it again and associating it back to [{info['id']}] "
f"on [{dss_target}]"
)
_install_code_env(dss_target, info, plugin_settings, uploaded_plugin)
else:
logger.debug("Plugin [{plugin_id}] is not installed on [{dss_target}], installing it".format(plugin_id=info["id"], dss_target=target))
with open(plugin_zip_path, 'rb') as fd:
admin_client.install_plugin_from_archive(fd)
uploaded_plugin = admin_client.get_plugin(info["id"])

plugin_settings = uploaded_plugin.get_settings()
raw_plugin_settings = plugin_settings.get_raw()

# install (or reinstall) code-env only if plugin has a specific code-env defined (not using DSS built-in):
if PluginInfo().plugin_codenv_metadata is not None:
if "codeEnvName" in raw_plugin_settings and len(raw_plugin_settings["codeEnvName"]) != 0:
logger.debug("Code env [{code_env_name}] is already associated to [{plugin_id}] on [{dss_target}], deleting it".format(code_env_name=raw_plugin_settings["codeEnvName"],
plugin_id=info["id"],
dss_target=target))

code_env_list = admin_client.list_code_envs()
code_env_info = list(filter(lambda x: x["envName"] == raw_plugin_settings["codeEnvName"], code_env_list))
if code_env_info:
code_env_info = code_env_info[0]
code_env = admin_client.get_code_env(code_env_info["envLang"], code_env_info["envName"])
code_env.delete()
logger.debug("Code env [{code_env_name}] is deleted. Creating it again and associating it back to [{plugin_id}] on [{dss_target}]".format(code_env_name=raw_plugin_settings["codeEnvName"],
plugin_id=info["id"],
dss_target=target))
_install_code_env(target, info, plugin_settings, uploaded_plugin)
else:
logger.debug("No code env is associated to [{plugin_id}] on [{dss_target}], creating it".format(plugin_id=info["id"], dss_target=target))
_install_code_env(target, info, plugin_settings, uploaded_plugin)
logger.debug(
f"No code env is associated to [{info['id']}] "
f"on [{dss_target}], creating it"
)
_install_code_env(dss_target, info, plugin_settings, uploaded_plugin)


def _install_code_env(target, plugin_info, plugin_settings, uploaded_plugin):
"""
Install the code env for the plugin. It is a private function to avoid code duplication
Install the code env for the plugin.
It is a private function to avoid code duplication

Args:
target(str): The DSS target to install the code env
plugin_info(dict): The plugin info based on the plugin.json and code-env desc.json.
plugin_settings: The plugin settings object from dataikuapi
uploaded_plugin: The plugin object corresping the to current plugin
plugin_settings: The plugin settings object from dataiku-api
uploaded_plugin: The plugin object corresponding to the current plugin
"""
current_run_config = ScenarioConfiguration()
target_available_interpreter = set(current_run_config.full_config[target]["python_interpreter"])
plugin_python_interpreter = set(plugin_info["python_interpreter"] if "python_interpreter" in plugin_info else [])
target_available_interpreter = set(
current_run_config.full_config[target]["python_interpreter"]
)
plugin_python_interpreter = set(
plugin_info["python_interpreter"] if "python_interpreter" in plugin_info else []
)

python_interpreters_for_code_env = list(target_available_interpreter.intersection(plugin_python_interpreter))
if not python_interpreters_for_code_env:
raise RuntimeError("No common python interpreter could be found "
"between the DSS target and the ones ask by the plugin [{plugin_id}]"
"From plugin: {plugin_interpreters}, From target: {target_interpreters}".format(plugin_id=plugin_info["id"],
plugin_interpreters=",".join(plugin_python_interpreter),
target_interpreters=",".join(target_available_interpreter)))

python_interpreter = python_interpreters_for_code_env[0] # if multiple in common taking the first one.
logger.debug("The code env will be installed using interpreter [{}]".format(python_interpreter if python_interpreter is not None else "PYTHON27"))
ret = uploaded_plugin.create_code_env(python_interpreter=python_interpreter).wait_for_result()
python_interpreters_for_code_env = list(
target_available_interpreter.intersection(plugin_python_interpreter)
)

python_interpreter = python_interpreters_for_code_env[
0
] # if multiple in common taking the first one.
logger.debug(
"The code env will be installed using interpreter [{}]".format(
python_interpreter if python_interpreter is not None else "PYTHON27"
)
)
ret = uploaded_plugin.create_code_env(
python_interpreter=python_interpreter
).wait_for_result()
if ret["messages"]["error"]:
raise RuntimeError("Error while installing the code-env [{code_env_name}], check DSS code-env creation logs on DSS".format(code_env_name=ret["envName"]))
raise RuntimeError(
"Error while installing the code-env [{ret['envName']}], "
"check DSS code-env creation logs on DSS"
)

logger.debug("The code env [{code_env_name}] is assocated with [{plugin_id}]".format(code_env_name=ret["envName"], plugin_id=plugin_info["id"]))
logger.debug(
f"The code env [{ret['envName']}] is assocated with [{plugin_info['id']}]"
)
plugin_settings.set_code_env(ret["envName"])
plugin_settings.save()