# Copyright (c) Alibaba, Inc. and its affiliates. """Helper module for loading Studio test environment from a local .env file. The ``.env`` file (sibling of this module) is optional and not committed to version control. ``.env.example`` documents the supported variables. """ import os def load_test_env(): """Load test environment variables from a local ``.env`` file if present. Existing environment variables take precedence (``setdefault`` semantics) so that CI overrides remain authoritative. """ env_file = os.path.join(os.path.dirname(__file__), '.env') if not os.path.exists(env_file): return with open(env_file, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#') or '=' not in line: continue key, value = line.split('=', 1) os.environ.setdefault(key.strip(), value.strip()) def get_test_config(): """Return the merged Studio test configuration dictionary.""" load_test_env() cleanup_raw = os.environ.get('TEST_CLEANUP_REMOTE', 'true').strip().lower() return { 'token': os.environ.get('MODELSCOPE_API_TOKEN'), 'owner': os.environ.get('TEST_STUDIO_OWNER', 'test_user'), 'visibility': os.environ.get('TEST_STUDIO_VISIBILITY', 'private'), 'endpoint': os.environ.get('MODELSCOPE_ENDPOINT', 'https://modelscope.cn'), 'studio_id': os.environ.get('TEST_STUDIO_ID'), 'cleanup': cleanup_raw not in ('false', '0', 'no'), # Studio creation defaults (required by OpenAPI) 'license': os.environ.get('TEST_STUDIO_LICENSE', 'apache-2.0'), 'sdk_type': os.environ.get('TEST_STUDIO_SDK_TYPE', 'gradio'), 'sdk_version': os.environ.get('TEST_STUDIO_SDK_VERSION', '6.2.0'), 'base_image': os.environ.get('TEST_STUDIO_BASE_IMAGE', 'ubuntu22.04-py311-torch2.9.1-modelscope1.35.0'), 'hardware': os.environ.get('TEST_STUDIO_HARDWARE', 'platform/2v-cpu-16g-mem'), } def create_temp_studio(config, name_prefix='ut_test_cli'): """Create a temporary studio for testing purposes. Args: config: dict from get_test_config() name_prefix: prefix for the generated studio name Returns: str: The studio_id (owner/name) of the created studio. Raises: unittest.SkipTest: If token or owner are not configured. """ import unittest from uuid import uuid4 from modelscope.hub.api import HubApi token = config['token'] owner = config['owner'] if not token or not owner: raise unittest.SkipTest( 'MODELSCOPE_API_TOKEN and TEST_STUDIO_OWNER required') name = f'{name_prefix}_{uuid4().hex[:8]}' repo_id = f'{owner}/{name}' visibility = config.get('visibility', 'private') api = HubApi() api.create_repo( repo_id, repo_type='studio', visibility=visibility, license=config.get('license', 'apache-2.0'), token=token, endpoint=config.get('endpoint'), sdk_type=config.get('sdk_type', 'gradio'), sdk_version=config.get('sdk_version', '6.2.0'), base_image=config.get('base_image', 'ubuntu22.04-py311-torch2.9.1-modelscope1.35.0'), hardware=config.get('hardware', 'platform/2v-cpu-16g-mem'), create_default_config=False, ) return repo_id class TestResultMixin: """Mixin that prints test pass/fail status after each test method. Must be placed BEFORE ``unittest.TestCase`` in the MRO so that this ``tearDown`` runs and still chains to ``TestCase.tearDown`` via ``super``. """ def tearDown(self): try: super().tearDown() finally: status = 'PASSED' outcome = getattr(self, '_outcome', None) if outcome is not None: # Python <= 3.10 exposes ``result`` directly. result = getattr(outcome, 'result', None) if result is not None and hasattr(result, 'failures'): test_failed = any(test is self for test, _ in (result.failures + result.errors)) status = 'FAILED' if test_failed else 'PASSED' elif hasattr(outcome, 'errors'): # Python 3.11+: inspect ``errors`` list of (ctx, exc_info). errors = getattr(outcome, 'errors', []) test_failed = any(exc_info is not None for _, exc_info in errors) status = 'FAILED' if test_failed else 'PASSED' # else: running under pytest or other runner — cannot detect print(f'[TEST {status}] {self.id()}')