test: remove duplicated descriptions from tests

This commit is contained in:
Evgenii Alekseev 2024-10-20 23:26:04 +03:00
parent 7c6c24a46d
commit 6fe77eb465
23 changed files with 38 additions and 73 deletions

View File

@ -139,7 +139,7 @@ def test_add_remote(application_packages: ApplicationPackages, package_descripti
def test_add_remote_missing(application_packages: ApplicationPackages, mocker: MockerFixture) -> None: def test_add_remote_missing(application_packages: ApplicationPackages, mocker: MockerFixture) -> None:
""" """
must add package from remote source must raise UnknownPackageError if remote package wasn't found
""" """
mocker.patch("requests.get", side_effect=Exception()) mocker.patch("requests.get", side_effect=Exception())
with pytest.raises(UnknownPackageError): with pytest.raises(UnknownPackageError):

View File

@ -79,7 +79,7 @@ def test_clean_packages(application_repository: ApplicationRepository, mocker: M
def test_clean_pacman(application_repository: ApplicationRepository, mocker: MockerFixture) -> None: def test_clean_pacman(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
""" """
must clean packages directory must clean pacman directory
""" """
clear_mock = mocker.patch("ahriman.core.repository.Repository.clear_pacman") clear_mock = mocker.patch("ahriman.core.repository.Repository.clear_pacman")
application_repository.clean(cache=False, chroot=False, manual=False, packages=False, pacman=True) application_repository.clean(cache=False, chroot=False, manual=False, packages=False, pacman=True)

View File

@ -171,7 +171,7 @@ def test_repositories_extract_repository(args: argparse.Namespace, configuration
def test_repositories_extract_repository_legacy(args: argparse.Namespace, configuration: Configuration, def test_repositories_extract_repository_legacy(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must generate list of available repositories based on flags and tree must generate list of available repositories based on flags and tree (legacy mode)
""" """
args.architecture = "arch" args.architecture = "arch"
args.configuration = configuration.path args.configuration = configuration.path

View File

@ -190,7 +190,7 @@ def test_extract_packages_by_status(application: Application, mocker: MockerFixt
def test_extract_packages_from_database(application: Application, mocker: MockerFixture) -> None: def test_extract_packages_from_database(application: Application, mocker: MockerFixture) -> None:
""" """
must extract packages from database must extract packages from database from database
""" """
packages_mock = mocker.patch("ahriman.core.database.SQLite.packages_get") packages_mock = mocker.patch("ahriman.core.database.SQLite.packages_get")
Rebuild.extract_packages(application, None, from_database=True) Rebuild.extract_packages(application, None, from_database=True)

View File

@ -40,7 +40,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
def test_run_eval(args: argparse.Namespace, configuration: Configuration, repository: Repository, def test_run_eval(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must run command must run command via eval
""" """
args = _default_args(args) args = _default_args(args)
args.code = """print("hello world")""" args.code = """print("hello world")"""

View File

@ -60,7 +60,7 @@ def test_remote_git_url(aur_package_ahriman: AURPackage) -> None:
def test_remote_web_url(aur_package_ahriman: AURPackage) -> None: def test_remote_web_url(aur_package_ahriman: AURPackage) -> None:
""" """
must generate package git url must generate package web url
""" """
web_url = AUR.remote_web_url(aur_package_ahriman.package_base) web_url = AUR.remote_web_url(aur_package_ahriman.package_base)
assert web_url.startswith(AUR.DEFAULT_AUR_URL) assert web_url.startswith(AUR.DEFAULT_AUR_URL)

View File

@ -87,7 +87,7 @@ def test_database_copy_skip(pacman: Pacman, mocker: MockerFixture) -> None:
def test_database_copy_no_directory(pacman: Pacman, mocker: MockerFixture) -> None: def test_database_copy_no_directory(pacman: Pacman, mocker: MockerFixture) -> None:
""" """
must do not copy database if local cache already exists must do not copy database if directory does not exist
""" """
database = next(db for db in pacman.handle.get_syncdbs() if db.name == "core") database = next(db for db in pacman.handle.get_syncdbs() if db.name == "core")
path = Path("randomname") path = Path("randomname")

View File

@ -44,7 +44,7 @@ def test_patches_list_filter(database: SQLite, package_ahriman: Package, package
def test_patches_list_filter_by_variable(database: SQLite, package_ahriman: Package, def test_patches_list_filter_by_variable(database: SQLite, package_ahriman: Package,
package_python_schedule: Package) -> None: package_python_schedule: Package) -> None:
""" """
must list all patches filtered by package name (same as get) must list all patches filtered by variable (same as get)
""" """
database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")]) database.patches_insert(package_ahriman.base, [PkgbuildPatch(None, "patch1")])
database.patches_insert(package_ahriman.base, [PkgbuildPatch("key", "patch2")]) database.patches_insert(package_ahriman.base, [PkgbuildPatch("key", "patch2")])

View File

@ -63,7 +63,7 @@ def test_in_package_context(database: SQLite, package_ahriman: Package, mocker:
def test_in_package_context_empty_version(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None: def test_in_package_context_empty_version(database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must set package log context must set package log context with empty version
""" """
set_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_set") set_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_set")
reset_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_reset") reset_mock = mocker.patch("ahriman.core.log.LazyLogging._package_logger_reset")

View File

@ -24,7 +24,7 @@ def test_template(configuration: Configuration) -> None:
def test_template_full(configuration: Configuration) -> None: def test_template_full(configuration: Configuration) -> None:
""" """
must correctly parse template name and path must correctly parse full template name and path
""" """
template = "template" template = "template"
root, repository_id = configuration.check_loaded() root, repository_id = configuration.check_loaded()

View File

@ -107,7 +107,7 @@ def test_generate_very_big_text(telegram: Telegram, package_ahriman: Package, re
def test_generate_no_empty(telegram: Telegram, package_ahriman: Package, mocker: MockerFixture) -> None: def test_generate_no_empty(telegram: Telegram, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must generate report must skip report generation if result is empty
""" """
send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send") send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send")
telegram.generate([package_ahriman], Result()) telegram.generate([package_ahriman], Result())

View File

@ -62,7 +62,7 @@ def test_clear_packages(cleaner: Cleaner, mocker: MockerFixture) -> None:
def test_clear_pacman(cleaner: Cleaner, mocker: MockerFixture) -> None: def test_clear_pacman(cleaner: Cleaner, mocker: MockerFixture) -> None:
""" """
must delete built packages must clear pacman root
""" """
_mock_clear(mocker) _mock_clear(mocker)
cleaner.clear_pacman() cleaner.clear_pacman()

View File

@ -142,7 +142,7 @@ def test_updates_aur_load_by_package(update_handler: UpdateHandler, package_pyth
def test_updates_aur_load_by_package_failed(update_handler: UpdateHandler, package_ahriman: Package, def test_updates_aur_load_by_package_failed(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must update status via client for failed load must update status via client for failed load if no remote package found
""" """
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=UnknownPackageError(package_ahriman.base)) mocker.patch("ahriman.models.package.Package.from_aur", side_effect=UnknownPackageError(package_ahriman.base))

View File

@ -9,50 +9,35 @@ from ahriman.core.sign.gpg import GPG
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
def test_repository_sign_args_1(gpg_with_key: GPG) -> None: def test_repository_sign_args(gpg_with_key: GPG) -> None:
""" """
must generate correct sign args must generate correct sign args
""" """
gpg_with_key.targets = {SignSettings.Repository} gpg_with_key.targets = {SignSettings.Repository}
assert gpg_with_key.repository_sign_args assert gpg_with_key.repository_sign_args
def test_repository_sign_args_2(gpg_with_key: GPG) -> None:
"""
must generate correct sign args
"""
gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository} gpg_with_key.targets = {SignSettings.Packages, SignSettings.Repository}
assert gpg_with_key.repository_sign_args assert gpg_with_key.repository_sign_args
def test_repository_sign_args_skip_1(gpg_with_key: GPG) -> None: def test_repository_sign_args_skip(gpg_with_key: GPG) -> None:
""" """
must return empty args if it is not set must return empty args if it is not set
""" """
gpg_with_key.targets = {} gpg_with_key.targets = {}
assert not gpg_with_key.repository_sign_args assert not gpg_with_key.repository_sign_args
def test_repository_sign_args_skip_2(gpg_with_key: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg_with_key.targets = {SignSettings.Packages} gpg_with_key.targets = {SignSettings.Packages}
assert not gpg_with_key.repository_sign_args assert not gpg_with_key.repository_sign_args
def test_repository_sign_args_skip_3(gpg: GPG) -> None: def test_repository_sign_args_skip_no_key(gpg: GPG) -> None:
""" """
must return empty args if it is not set must return empty args if it is not set if no key set
""" """
gpg.targets = {SignSettings.Repository} gpg.targets = {SignSettings.Repository}
assert not gpg.repository_sign_args assert not gpg.repository_sign_args
def test_repository_sign_args_skip_4(gpg: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg.targets = {SignSettings.Packages, SignSettings.Repository} gpg.targets = {SignSettings.Packages, SignSettings.Repository}
assert not gpg.repository_sign_args assert not gpg.repository_sign_args
@ -188,7 +173,7 @@ def test_process_sign_package_3(gpg_with_key: GPG, mocker: MockerFixture) -> Non
def test_process_sign_package_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None: def test_process_sign_package_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
""" """
must not sign package if it is not set must not sign package on empty target list
""" """
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process") process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {} gpg_with_key.targets = {}
@ -198,7 +183,7 @@ def test_process_sign_package_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -
def test_process_sign_package_skip_2(gpg_with_key: GPG, mocker: MockerFixture) -> None: def test_process_sign_package_skip_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
""" """
must not sign package if it is not set must not sign package if repository only is enabled
""" """
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process") process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {SignSettings.Repository} gpg_with_key.targets = {SignSettings.Repository}
@ -208,7 +193,7 @@ def test_process_sign_package_skip_2(gpg_with_key: GPG, mocker: MockerFixture) -
def test_process_sign_package_skip_3(gpg: GPG, mocker: MockerFixture) -> None: def test_process_sign_package_skip_3(gpg: GPG, mocker: MockerFixture) -> None:
""" """
must not sign package if it is not set must not sign package if key is not set
""" """
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process") process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Packages} gpg.targets = {SignSettings.Packages}
@ -216,16 +201,6 @@ def test_process_sign_package_skip_3(gpg: GPG, mocker: MockerFixture) -> None:
process_mock.assert_not_called() process_mock.assert_not_called()
def test_process_sign_package_skip_4(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Packages, SignSettings.Repository}
gpg.process_sign_package(Path("a"), None)
process_mock.assert_not_called()
def test_process_sign_package_skip_already_signed(gpg_with_key: GPG, mocker: MockerFixture) -> None: def test_process_sign_package_skip_already_signed(gpg_with_key: GPG, mocker: MockerFixture) -> None:
""" """
must not sign package if it was already signed must not sign package if it was already signed
@ -264,7 +239,7 @@ def test_process_sign_repository_2(gpg_with_key: GPG, mocker: MockerFixture) ->
def test_process_sign_repository_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None: def test_process_sign_repository_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
""" """
must not sign repository if it is not set must not sign repository if no targets set
""" """
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process") process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {} gpg_with_key.targets = {}
@ -274,7 +249,7 @@ def test_process_sign_repository_skip_1(gpg_with_key: GPG, mocker: MockerFixture
def test_process_sign_repository_skip_2(gpg_with_key: GPG, mocker: MockerFixture) -> None: def test_process_sign_repository_skip_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
""" """
must not sign repository if it is not set must not sign repository if repository target is not set
""" """
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process") process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {SignSettings.Packages} gpg_with_key.targets = {SignSettings.Packages}
@ -284,19 +259,9 @@ def test_process_sign_repository_skip_2(gpg_with_key: GPG, mocker: MockerFixture
def test_process_sign_repository_skip_3(gpg: GPG, mocker: MockerFixture) -> None: def test_process_sign_repository_skip_3(gpg: GPG, mocker: MockerFixture) -> None:
""" """
must not sign repository if it is not set must not sign repository if key is not set
""" """
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process") process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Repository} gpg.targets = {SignSettings.Repository}
gpg.process_sign_repository(Path("a")) gpg.process_sign_repository(Path("a"))
process_mock.assert_not_called() process_mock.assert_not_called()
def test_process_sign_repository_skip_4(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.Packages, SignSettings.Repository}
gpg.process_sign_repository(Path("a"))
process_mock.assert_not_called()

View File

@ -245,7 +245,7 @@ def test_set_failed(client: Client, package_ahriman: Package, mocker: MockerFixt
def test_set_pending(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None: def test_set_pending(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must set building status to the package must set pending status to the package
""" """
update_mock = mocker.patch("ahriman.core.status.Client.package_status_update") update_mock = mocker.patch("ahriman.core.status.Client.package_status_update")
client.set_pending(package_ahriman.base) client.set_pending(package_ahriman.base)

View File

@ -46,7 +46,7 @@ def test_changes_url(web_client: WebClient, package_ahriman: Package) -> None:
def test_dependencies_url(web_client: WebClient, package_ahriman: Package) -> None: def test_dependencies_url(web_client: WebClient, package_ahriman: Package) -> None:
""" """
must generate changes url correctly must generate dependencies url correctly
""" """
assert web_client._dependencies_url(package_ahriman.base).startswith(web_client.address) assert web_client._dependencies_url(package_ahriman.base).startswith(web_client.address)
assert web_client._dependencies_url(package_ahriman.base).endswith( assert web_client._dependencies_url(package_ahriman.base).endswith(
@ -74,7 +74,7 @@ def test_logs_url(web_client: WebClient, package_ahriman: Package) -> None:
def test_package_url(web_client: WebClient, package_ahriman: Package) -> None: def test_package_url(web_client: WebClient, package_ahriman: Package) -> None:
""" """
must generate package status url correctly must generate package url correctly
""" """
assert web_client._package_url("").startswith(web_client.address) assert web_client._package_url("").startswith(web_client.address)
assert web_client._package_url("").endswith("/api/v1/packages") assert web_client._package_url("").endswith("/api/v1/packages")
@ -86,7 +86,7 @@ def test_package_url(web_client: WebClient, package_ahriman: Package) -> None:
def test_patches_url(web_client: WebClient, package_ahriman: Package) -> None: def test_patches_url(web_client: WebClient, package_ahriman: Package) -> None:
""" """
must generate changes url correctly must generate patches url correctly
""" """
assert web_client._patches_url(package_ahriman.base).startswith(web_client.address) assert web_client._patches_url(package_ahriman.base).startswith(web_client.address)
assert web_client._patches_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}/patches") assert web_client._patches_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}/patches")
@ -575,7 +575,7 @@ def test_package_logs_add_failed(web_client: WebClient, log_record: logging.LogR
def test_package_logs_add_failed_http_error(web_client: WebClient, log_record: logging.LogRecord, def test_package_logs_add_failed_http_error(web_client: WebClient, log_record: logging.LogRecord,
package_ahriman: Package, mocker: MockerFixture) -> None: package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must pass exception during log post must pass HTTP exception during log post
""" """
mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
log_record.package_base = package_ahriman.base log_record.package_base = package_ahriman.base
@ -725,7 +725,7 @@ def test_package_patches_get_failed(web_client: WebClient, package_ahriman: Pack
def test_package_patches_get_failed_http_error(web_client: WebClient, package_ahriman: Package, def test_package_patches_get_failed_http_error(web_client: WebClient, package_ahriman: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must suppress HTTP exception happened during dependencies fetch must suppress HTTP exception happened during patches fetch
""" """
mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
web_client.package_patches_get(package_ahriman.base, None) web_client.package_patches_get(package_ahriman.base, None)

View File

@ -13,7 +13,7 @@ from ahriman.models.result import Result
def test_known_triggers(configuration: Configuration) -> None: def test_known_triggers(configuration: Configuration) -> None:
""" """
must return used triggers must return known triggers
""" """
configuration.set_option("build", "triggers_known", "a b c") configuration.set_option("build", "triggers_known", "a b c")
assert TriggerLoader.known_triggers(configuration) == ["a", "b", "c"] assert TriggerLoader.known_triggers(configuration) == ["a", "b", "c"]

View File

@ -255,14 +255,14 @@ def test_from_json_view_1(package_ahriman: Package) -> None:
def test_from_json_view_2(package_python_schedule: Package) -> None: def test_from_json_view_2(package_python_schedule: Package) -> None:
""" """
must construct same object from json must construct same object from json (double package)
""" """
assert Package.from_json(package_python_schedule.view()) == package_python_schedule assert Package.from_json(package_python_schedule.view()) == package_python_schedule
def test_from_json_view_3(package_tpacpi_bat_git: Package) -> None: def test_from_json_view_3(package_tpacpi_bat_git: Package) -> None:
""" """
must construct same object from json must construct same object from json (git package)
""" """
assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git

View File

@ -86,7 +86,7 @@ def test_resolve_local(repository_paths: RepositoryPaths, mocker: MockerFixture)
def test_resolve_local_cache(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: def test_resolve_local_cache(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
""" """
must resolve auto type into the local sources must resolve auto type into the local sources with cache
""" """
cache_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.cache_for", return_value=Path("cache")) cache_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.cache_for", return_value=Path("cache"))
mocker.patch("pathlib.Path.is_dir", autospec=True, side_effect=lambda p: p == Path("cache")) mocker.patch("pathlib.Path.is_dir", autospec=True, side_effect=lambda p: p == Path("cache"))

View File

@ -27,7 +27,7 @@ def test_is_function() -> None:
def test_is_plain_diff() -> None: def test_is_plain_diff() -> None:
""" """
must correctly define key as function must correctly define key as a plain diff
""" """
assert not PkgbuildPatch("key", "value").is_plain_diff assert not PkgbuildPatch("key", "value").is_plain_diff
assert PkgbuildPatch(None, "value").is_plain_diff assert PkgbuildPatch(None, "value").is_plain_diff
@ -112,7 +112,7 @@ def test_from_env_serialize() -> None:
def test_serialize_plain_diff() -> None: def test_serialize_plain_diff() -> None:
""" """
must correctly serialize function values must correctly serialize plain diff values
""" """
assert PkgbuildPatch(None, "{ value }").serialize() == "{ value }" assert PkgbuildPatch(None, "{ value }").serialize() == "{ value }"

View File

@ -98,7 +98,7 @@ async def test_exception_handler_unauthorized(mocker: MockerFixture) -> None:
async def test_exception_handler_unauthorized_templated(mocker: MockerFixture) -> None: async def test_exception_handler_unauthorized_templated(mocker: MockerFixture) -> None:
""" """
must handle unauthorized exception as json response must handle unauthorized exception as json response in html context
""" """
request = pytest.helpers.request("", "", "") request = pytest.helpers.request("", "", "")
request_handler = AsyncMock(side_effect=HTTPUnauthorized()) request_handler = AsyncMock(side_effect=HTTPUnauthorized())

View File

@ -124,7 +124,7 @@ def test_run_with_auth(application_with_auth: Application, mocker: MockerFixture
def test_run_with_socket(application: Application, mocker: MockerFixture) -> None: def test_run_with_socket(application: Application, mocker: MockerFixture) -> None:
""" """
must run application must run application with socket
""" """
port = 8080 port = 8080
application[ConfigurationKey].set_option("web", "port", str(port)) application[ConfigurationKey].set_option("web", "port", str(port))

View File

@ -158,7 +158,7 @@ async def test_post_unauthorized(client_with_auth: TestClient, user: User, mocke
async def test_post_invalid_json(client_with_auth: TestClient, mocker: MockerFixture) -> None: async def test_post_invalid_json(client_with_auth: TestClient, mocker: MockerFixture) -> None:
""" """
must return unauthorized on invalid auth must return unauthorized on invalid payload
""" """
response_schema = pytest.helpers.schema_response(LoginView.post, code=400) response_schema = pytest.helpers.schema_response(LoginView.post, code=400)
remember_mock = mocker.patch("aiohttp_security.remember") remember_mock = mocker.patch("aiohttp_security.remember")