ayon-core/tests/lib/testing_classes.py
Jakub Trllo 47473a8a23 General: Connect to AYON server (base) (#3924)
* implemented 'get_workfile_info' in entities

* removed 'prepare_asset_update_data' which is not used

* disable settings and project manager if in v4 mode

* prepared conversion helper functions for v4 entities

* prepared conversion functions for hero versions

* fix hero versions

* implemented get_archived_representations

* fix get latest versions

* return prepared changes

* handle archived representation

* raise exception on failed json conversion

* map archived to active properly

* make sure default fields are added

* fix conversion of hero version entity

* fix conversion of archived representations

* fix some conversions of representations and versions

* changed active behavior in queries

* fixed hero versions

* implemented basic thumbnail caching

* added raw variants of crud methods

* implemented methods to get and create thumbnail

* fix from flat dict

* implemented some basic folder conversion for updates

* fix thumbnail updates for version

* implemented v4 thumbnail integrator

* simplified data mapping

* 'get_thumbnail' function also expect entity type and entity id for which is the thumbnail received

* implemented 'get_thumbnail' for server

* fix how thumbnail id is received from entity

* removed unnecessary method 'get_thumbnail_id_from_source'

* implemented thumbnail resolver for v4

* removed unnecessary print

* move create and delete project directly to server api

* disable local settings action too on v4

* OP-3521 - added method to check and download updated addons from v4 server

* OP-3521 - added more descriptive error message for missing source

* OP-3521 - added default implementation of addon downloader to import

* OP-3521 - added check for dependency package zips

WIP - server doesn't contain required endpoint. Testing only with mockup data for now.

* OP-3521 - fixed parsing of DependencyItem

Added Server Url type and ServerAddonDownloader - v4 server doesn't know its own DNS for static files so it is sending unique name and url must be created during runtime.

* OP-3521 - fixed creation of targed directories

* change nev keys to look for and don't set them automatically

* fix task type conversion

* implemented base of loading v4 addons in v3

* Refactored argument name in Downloaders

* Updated parsing to DependencyItem according to current schema

* Implemented downloading of package from server

* Updated resolving of failures

Uses Enum items.

* Introduced passing of authorization token

Better to inject it than to have it from env var.

* Remove weird parsing of server_url

Not necessary, endpoints have same prefix.

* Fix doubling asset version name in addons folder

Zip file should already contain `addonName_addonVersion` as first subfolder

* Fix doubling asset version name in addons folder

Zip file should already contain `addonName_addonVersion` as first subfolder

* Made server_endpoint optional

Argument should be better for testing, but for calling from separate methods it would be better to encapsulate it.

Removed unwanted temporary productionPackage value

* Use existing method to pull addon info from Server to load v4 version of addon

* Raise exception when server doesn't have any production dependency package

* added ability to specify v3 alias of addon name

* expect v3_alias as uppered constant

* Re-implemented method to get addon info

Previous implementation wouldn't work in Python2 hosts.
Will be refactored in the future.

* fix '__getattr__'

* added ayon api to pyproject.toml and lock file

* use ayon api in common connection

* added mapping for label

* use ayon_api in client codebase

* separated clearing cache of url and username

* bump ayon api version

* rename env 'OP4_TEST' to 'USE_AYON_SERVER'

* Move and renamend get_addons_info to get_addons_info_as_dict in addon_distribution

Should be moved to ayon_api later

* Replaced requests calls with ayon_api

* Replaced OP4_TEST_ENABLED with AYON_SERVER_ENABLED

fixed endpoints

* Hound

* Hound

* OP-3521 - fix wrong key in get_representation_parents

parents overloads parents

* OP-3521 - changes for v4 of SiteSync addon

* OP-3521 - fix names

* OP-3521 - remove storing project_name

It should be safer to go thorug self.dbcon apparently

* OP-3521 - remove unwanted

"context["folder"]" can be only in dummy test data

* OP-3521 - move site sync loaders to addon

* Use only project instead of self.project

* OP-3521 - added missed get_progress_for_repre

* base of settings conversion script

* simplified ayon functions in start.py

* added loading of settings from ayon server

* added a note about colors

* fix global and local settings functions

* AvalonMongoDB is not using mongo connection on ayon server enabled

* 'get_dynamic_modules_dirs' is not checking system settings for paths in setting

* log viewer is disabled when ayon server is enabled

* basic logic of enabling/disabled addons

* don't use mongo logging if ayon server is enabled

* update ayon api

* bump ayon api again

* use ayon_api to get addons info in modules/base

* update ayon api

* moved helper functions to get addons and dependencies dir to common functions

* Initialization of AddonInfo is not crashing on unkonwn sources

* renamed 'DependencyDownloader' to 'AyonServerDownloader'

* renamed function 'default_addon_downloader' to 'get_default_addon_downloader'

* Added ability to convert 'WebAddonSource' to 'ServerResourceSorce'

* missing dependency package on server won't cause crash

* data sent to downloaders don't contain ayon specific headers

* modified addon distribution to not duplicate 'ayon_api' functionality

* fix doubled function defintioin

* unzip client file to addon destination

* formatting - unify quotes

* disable usage of mongo connection if in ayon mode

* renamed window.py to login_window.py

* added webpublisher settings conversion

* added maya conversion function

* reuse variable

* reuse variable (similar to previous commit)

* fix ayon addons loading

* fix typo 'AyonSettingsCahe' -> 'AyonSettingsCache'

* fix enabled state changes

* fix rr_path in royal render conversion

* avoid mongo calls in AYON state

* implemented custom AYON start script

* fix formatting (after black)

* ayon_start cleanup

* 'get_addons_dir' and 'get_dependencies_dir' store value to environment variable

* add docstrings to local dir functions

* addon info has full name

* fix modules enabled states

* removed unused 'run_disk_mapping_commands'

* removed ayon logic from 'start.py'

* fix warning message

* renamed 'openpype_common' to 'ayon_common'

* removed unused import

* don't import igniter

* removed startup validations of third parties

* change what's shown in version info

* fix which keys are applied from ayon values

* fix method name

* get applications from attribs

* Implemented UI basics to be able change user or logout

* merged server.py and credentials.py

* add more metadata to urls

* implemented change token

* implemented change user ui functionality

* implemented change user ui

* modify window to handle username and token value

* pass username to add server

* fix show UI cases

* added loggin action to tray

* update ayon api

* added missing dependency

* convert applications to config in a right way

* initial implementation of 'nuke' settings conversion

* removed few nuke comments

* implemented hiero conversion

* added imageio conversion

* added run ayon tray script

* fix few settings conversions

* Renamed class of source classes as they are not just for addons

* implemented objec to track source transfer progress

* Implemented distribution item with multiple sources

* Implemented ayon distribution wrapper to care about multiple things during distribution

* added 'cleanup' method for downlaoders

* download gets tranfer progress object

* Change UploadState enum

* added missing imports

* use AyonDistribution in ayon_start.py

* removed unused functions

* removed implemented TODOs

* fix import

* fix key used for Web source

* removed temp development fix

* formatting fix

* keep information if source require distribution

* handle 'require_distribution' attribute in distribution process

* added path attribute to server source

* added option to pass addons infor to ayon distribution

* fix tests

* fix formatting

* Fix typo

* Fix typo

* remove '_try_convert_to_server_source'

* renamed attributes and methods to match their content

* it is possible to pass dependency package info to AyonDistribution

* fix called methods in tests

* added public properties for error message and error detail

* Added filename to WebSourceInfo

Useful for GDrive sharable links where target file name is unknown/unparsable, it should be provided explicitly.

* unify source conversion by adding 'convert_source' function

* Fix error message

Co-authored-by: Roy Nieterau <roy_nieterau@hotmail.com>

* added docstring for 'transfer_progress'

* don't create metadata file on read

* added few docstrings

* add default folder fields to folder/task queries

* fix generators

* add dependencies when runnign from code

* add sys paths from distribution to pythonpath env

* fix missing applications

* added missing conversions for maya renderers

* fix formatting

* update ayon api

* fix hashes in lock file

* Use better exception

Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com>

* Use Python 3 syntax

Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com>

* apply some of sugested changes in ayon_start

* added some docstrings and suggested modifications

* copy create env from develop

* fix rendersettings conversion

* change code by suggestions

* added missing args to docstring

* added missing docstrings

* separated downloader and download factory

* fix ayon settings

* added some basic file docstring to ayon_settings

* join else conditions

* fix project settings conversion

* fix created at conversion

* fix workfile info query

* fix publisher UI

* added utils function 'get_ayon_appdirs'

* fix 'get_all_current_info'

* fix server url assignment when url is set

* updated ayon api

* added utils functions to create local site id for ayon

* added helper functions to create global connection

* create global connection in ayon start to start use site id

* use ayon site id in ayon mode

* formatting cleanup

* added header docstring

* fixes after ayon_api update

* load addons from ynput appdirs

* fix function call

* added docstring

* update ayon pyton api

* fix settings access

* use ayon_api to get root overrides in Anatomy

* bumbayon version to 0.1.13

* nuke: fixing settings keys from settings

* fix burnins definitions

* change v4 to AYON in thumbnail integrate

* fix one more v4 information

* Fixes after rebase

* fix extract burnin conversion

* additional fix of extract burnin

* SiteSync:added missed loaders or v3 compatibility (#4587)

* Added site sync loaders for v3 compatibility

* Fix get_progress_for_repre

* use 'files.name' instead of 'files.baseName'

* update ayon api to 0.1.14

* add common to include files

* change arguments for hero version creation

* skip shotgrid settings conversion if different ayon addon is used

* added ayon icons

* fix labels of application variants

* added option to show login window always on top

* login window on invalid credentials is always on top

* update ayon api

* update ayon api

* add entityType to project and folders

* AYON: Editorial hierarchy creation (#4699)

* disable extract hierarchy avalon when ayon mode is enabled

* implemented extract hierarchy to AYON

---------

Co-authored-by: Petr Kalis <petr.kalis@gmail.com>
Co-authored-by: Roy Nieterau <roy_nieterau@hotmail.com>
Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com>
Co-authored-by: Jakub Jezek <jakubjezek001@gmail.com>
2023-07-11 17:50:50 +02:00

475 lines
17 KiB
Python

"""Testing classes for module testing and publishing in hosts."""
import os
import sys
import six
import json
import pytest
import tempfile
import shutil
import glob
import platform
import requests
import re
from tests.lib.db_handler import DBHandler
from common.ayon_common.distribution.file_handler import RemoteFileHandler
from openpype.modules import ModulesManager
from openpype.settings import get_project_settings
class BaseTest:
"""Empty base test class"""
class ModuleUnitTest(BaseTest):
"""Generic test class for testing modules
Use PERSIST==True to keep temporary folder and DB prepared for
debugging or preparation of test files.
Implemented fixtures:
monkeypatch_session - fixture for env vars with session scope
project_settings - fixture for project settings with session scope
download_test_data - tmp folder with extracted data from GDrive
env_var - sets env vars from input file
db_setup - prepares avalon AND openpype DBs for testing from
binary dumps from input data
dbcon - returns DBConnection to AvalonDB
dbcon_openpype - returns DBConnection for OpenpypeMongoDB
"""
PERSIST = False # True to not purge temporary folder nor test DB
TEST_OPENPYPE_MONGO = "mongodb://localhost:27017"
TEST_DB_NAME = "avalon_tests"
TEST_PROJECT_NAME = "test_project"
TEST_OPENPYPE_NAME = "openpype_tests"
TEST_FILES = []
PROJECT = "test_project"
ASSET = "test_asset"
TASK = "test_task"
TEST_DATA_FOLDER = None
@pytest.fixture(scope='session')
def monkeypatch_session(self):
"""Monkeypatch couldn't be used with module or session fixtures."""
from _pytest.monkeypatch import MonkeyPatch
m = MonkeyPatch()
yield m
m.undo()
@pytest.fixture(scope='module')
def project_settings(self):
yield get_project_settings(
self.PROJECT
)
@pytest.fixture(scope="module")
def download_test_data(self, test_data_folder, persist, request):
test_data_folder = test_data_folder or self.TEST_DATA_FOLDER
if test_data_folder:
print("Using existing folder {}".format(test_data_folder))
yield test_data_folder
else:
tmpdir = tempfile.mkdtemp()
print("Temporary folder created:: {}".format(tmpdir))
for test_file in self.TEST_FILES:
file_id, file_name, md5 = test_file
f_name, ext = os.path.splitext(file_name)
RemoteFileHandler.download_file_from_google_drive(file_id,
str(tmpdir),
file_name)
if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: # noqa: E501
RemoteFileHandler.unzip(os.path.join(tmpdir, file_name))
yield tmpdir
persist = (persist or self.PERSIST or
self.is_test_failed(request))
if not persist:
print("Removing {}".format(tmpdir))
shutil.rmtree(tmpdir)
@pytest.fixture(scope="module")
def output_folder_url(self, download_test_data):
"""Returns location of published data, cleans it first if exists."""
path = os.path.join(download_test_data, "output")
if os.path.exists(path):
print("Purging {}".format(path))
shutil.rmtree(path)
yield path
@pytest.fixture(scope="module")
def env_var(self, monkeypatch_session, download_test_data):
"""Sets temporary env vars from json file."""
env_url = os.path.join(download_test_data, "input",
"env_vars", "env_var.json")
if not os.path.exists(env_url):
raise ValueError("Env variable file {} doesn't exist".
format(env_url))
env_dict = {}
try:
with open(env_url) as json_file:
env_dict = json.load(json_file)
except ValueError:
print("{} doesn't contain valid JSON")
six.reraise(*sys.exc_info())
for key, value in env_dict.items():
all_vars = globals()
all_vars.update(vars(ModuleUnitTest)) # TODO check
value = value.format(**all_vars)
print("Setting {}:{}".format(key, value))
monkeypatch_session.setenv(key, str(value))
#reset connection to openpype DB with new env var
import openpype.settings.lib as sett_lib
sett_lib._SETTINGS_HANDLER = None
sett_lib._LOCAL_SETTINGS_HANDLER = None
sett_lib.create_settings_handler()
sett_lib.create_local_settings_handler()
import openpype
openpype_root = os.path.dirname(os.path.dirname(openpype.__file__))
# ?? why 2 of those
monkeypatch_session.setenv("OPENPYPE_ROOT", openpype_root)
monkeypatch_session.setenv("OPENPYPE_REPOS_ROOT", openpype_root)
# for remapping purposes (currently in Nuke)
monkeypatch_session.setenv("TEST_SOURCE_FOLDER", download_test_data)
@pytest.fixture(scope="module")
def db_setup(self, download_test_data, env_var, monkeypatch_session,
request):
"""Restore prepared MongoDB dumps into selected DB."""
backup_dir = os.path.join(download_test_data, "input", "dumps")
uri = os.environ.get("OPENPYPE_MONGO")
db_handler = DBHandler(uri)
db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir,
overwrite=True,
db_name_out=self.TEST_DB_NAME)
db_handler.setup_from_dump(self.TEST_OPENPYPE_NAME, backup_dir,
overwrite=True,
db_name_out=self.TEST_OPENPYPE_NAME)
yield db_handler
persist = self.PERSIST or self.is_test_failed(request)
if not persist:
db_handler.teardown(self.TEST_DB_NAME)
db_handler.teardown(self.TEST_OPENPYPE_NAME)
@pytest.fixture(scope="module")
def dbcon(self, db_setup, output_folder_url):
"""Provide test database connection.
Database prepared from dumps with 'db_setup' fixture.
"""
from openpype.pipeline import AvalonMongoDB
dbcon = AvalonMongoDB()
dbcon.Session["AVALON_PROJECT"] = self.PROJECT
dbcon.Session["AVALON_ASSET"] = self.ASSET
dbcon.Session["AVALON_TASK"] = self.TASK
# set project root to temp folder
platform_str = platform.system().lower()
root_key = "config.roots.work.{}".format(platform_str)
dbcon.update_one(
{"type": "project"},
{"$set":
{
root_key: output_folder_url
}}
)
yield dbcon
@pytest.fixture(scope="module")
def dbcon_openpype(self, db_setup):
"""Provide test database connection for OP settings.
Database prepared from dumps with 'db_setup' fixture.
"""
from openpype.lib import OpenPypeMongoConnection
mongo_client = OpenPypeMongoConnection.get_mongo_client()
yield mongo_client[self.TEST_OPENPYPE_NAME]["settings"]
def is_test_failed(self, request):
# if request.node doesn't have rep_call, something failed
try:
return request.node.rep_call.failed
except AttributeError:
return True
class PublishTest(ModuleUnitTest):
"""Test class for publishing in hosts.
Implemented fixtures:
launched_app - launches APP with last_workfile_path
publish_finished - waits until publish is finished, host must
kill its process when finished publishing. Includes timeout
which raises ValueError
Not implemented:
last_workfile_path - returns path to testing workfile
startup_scripts - provide script for setup in host
Implemented tests:
test_folder_structure_same - compares published and expected
subfolders if they contain same files. Compares only on file
presence
TODO: implement test on file size, file content
"""
APP_GROUP = ""
TIMEOUT = 120 # publish timeout
# could be overwritten by command line arguments
# command line value takes precedence
# keep empty to locate latest installed variant or explicit
APP_VARIANT = ""
PERSIST = True # True - keep test_db, test_openpype, outputted test files
TEST_DATA_FOLDER = None # use specific folder of unzipped test file
SETUP_ONLY = False
@pytest.fixture(scope="module")
def app_name(self, app_variant):
"""Returns calculated value for ApplicationManager. Eg.(nuke/12-2)"""
from openpype.lib import ApplicationManager
app_variant = app_variant or self.APP_VARIANT
application_manager = ApplicationManager()
if not app_variant:
variant = (
application_manager.find_latest_available_variant_for_group(
self.APP_GROUP))
app_variant = variant.name
yield "{}/{}".format(self.APP_GROUP, app_variant)
@pytest.fixture(scope="module")
def app_args(self, download_test_data):
"""Returns additional application arguments from a test file.
Test zip file should contain file at:
FOLDER_DIR/input/app_args/app_args.json
containing a list of command line arguments (like '-x' etc.)
"""
app_args = []
args_url = os.path.join(download_test_data, "input",
"app_args", "app_args.json")
if not os.path.exists(args_url):
print("App argument file {} doesn't exist".format(args_url))
else:
try:
with open(args_url) as json_file:
app_args = json.load(json_file)
if not isinstance(app_args, list):
raise ValueError
except ValueError:
print("{} doesn't contain valid JSON".format(args_url))
six.reraise(*sys.exc_info())
yield app_args
@pytest.fixture(scope="module")
def launched_app(self, dbcon, download_test_data, last_workfile_path,
startup_scripts, app_args, app_name, output_folder_url,
setup_only):
"""Launch host app"""
if setup_only or self.SETUP_ONLY:
print("Creating only setup for test, not launching app")
yield
return
# set schema - for integrate_new
from openpype import PACKAGE_DIR
# Path to OpenPype's schema
schema_path = os.path.join(
os.path.dirname(PACKAGE_DIR),
"schema"
)
os.environ["AVALON_SCHEMA"] = schema_path
os.environ["OPENPYPE_EXECUTABLE"] = sys.executable
from openpype.lib import ApplicationManager
application_manager = ApplicationManager()
data = {
"last_workfile_path": last_workfile_path,
"start_last_workfile": True,
"project_name": self.PROJECT,
"asset_name": self.ASSET,
"task_name": self.TASK
}
if app_args:
data["app_args"] = app_args
app_process = application_manager.launch(app_name, **data)
yield app_process
@pytest.fixture(scope="module")
def publish_finished(self, dbcon, launched_app, download_test_data,
timeout, setup_only):
"""Dummy fixture waiting for publish to finish"""
if setup_only or self.SETUP_ONLY:
print("Creating only setup for test, not launching app")
yield False
return
import time
time_start = time.time()
timeout = timeout or self.TIMEOUT
timeout = float(timeout)
while launched_app.poll() is None:
time.sleep(0.5)
if time.time() - time_start > timeout:
launched_app.terminate()
raise ValueError("Timeout reached")
# some clean exit test possible?
print("Publish finished")
yield True
def test_folder_structure_same(self, dbcon, publish_finished,
download_test_data, output_folder_url,
skip_compare_folders,
setup_only):
"""Check if expected and published subfolders contain same files.
Compares only presence, not size nor content!
"""
if setup_only or self.SETUP_ONLY:
print("Creating only setup for test, not launching app")
return
published_dir_base = output_folder_url
expected_dir_base = os.path.join(download_test_data,
"expected")
print("Comparing published:'{}' : expected:'{}'".format(
published_dir_base, expected_dir_base))
published = set(f.replace(published_dir_base, '') for f in
glob.glob(published_dir_base + "\\**", recursive=True)
if f != published_dir_base and os.path.exists(f))
expected = set(f.replace(expected_dir_base, '') for f in
glob.glob(expected_dir_base + "\\**", recursive=True)
if f != expected_dir_base and os.path.exists(f))
filtered_published = self._filter_files(published,
skip_compare_folders)
# filter out temp files also in expected
# could be polluted by accident by copying 'output' to zip file
filtered_expected = self._filter_files(expected, skip_compare_folders)
not_mtched = filtered_expected.symmetric_difference(filtered_published)
if not_mtched:
raise AssertionError("Missing {} files".format(
"\n".join(sorted(not_mtched))))
def _filter_files(self, source_files, skip_compare_folders):
"""Filter list of files according to regex pattern."""
filtered = set()
for file_path in source_files:
if skip_compare_folders:
if not any([re.search(val, file_path)
for val in skip_compare_folders]):
filtered.add(file_path)
else:
filtered.add(file_path)
return filtered
class DeadlinePublishTest(PublishTest):
@pytest.fixture(scope="module")
def publish_finished(self, dbcon, launched_app, download_test_data,
timeout):
"""Dummy fixture waiting for publish to finish"""
import time
time_start = time.time()
timeout = timeout or self.TIMEOUT
timeout = float(timeout)
while launched_app.poll() is None:
time.sleep(0.5)
if time.time() - time_start > timeout:
launched_app.terminate()
raise ValueError("Timeout reached")
metadata_json = glob.glob(os.path.join(download_test_data,
"output",
"**/*_metadata.json"),
recursive=True)
if not metadata_json:
raise RuntimeError("No metadata file found. No job id.")
if len(metadata_json) > 1:
# depends on creation order of published jobs
metadata_json.sort(key=os.path.getmtime, reverse=True)
with open(metadata_json[0]) as fp:
job_info = json.load(fp)
deadline_job_id = job_info["deadline_publish_job_id"]
manager = ModulesManager()
deadline_module = manager.modules_by_name["deadline"]
deadline_url = deadline_module.deadline_urls["default"]
if not deadline_url:
raise ValueError("Must have default deadline url.")
url = "{}/api/jobs?JobId={}".format(deadline_url, deadline_job_id)
valid_date_finished = None
time_start = time.time()
while not valid_date_finished:
time.sleep(0.5)
if time.time() - time_start > timeout:
raise ValueError("Timeout for DL finish reached")
response = requests.get(url, timeout=10)
if not response.ok:
msg = "Couldn't connect to {}".format(deadline_url)
raise RuntimeError(msg)
if not response.json():
raise ValueError("Couldn't find {}".format(deadline_job_id))
# '0001-...' returned until job is finished
valid_date_finished = response.json()[0]["DateComp"][:4] != "0001"
# some clean exit test possible?
print("Publish finished")
yield True
class HostFixtures():
"""Host specific fixtures. Should be implemented once per host."""
@pytest.fixture(scope="module")
def last_workfile_path(self, download_test_data, output_folder_url):
"""Returns url of workfile"""
raise NotImplementedError
@pytest.fixture(scope="module")
def startup_scripts(self, monkeypatch_session, download_test_data):
""""Adds init scripts (like userSetup) to expected location"""
raise NotImplementedError
@pytest.fixture(scope="module")
def skip_compare_folders(self):
"""Use list of regexs to filter out published folders from comparing"""
raise NotImplementedError