-
Notifications
You must be signed in to change notification settings - Fork 126
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
286 additions
and
168 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import queue | ||
import re | ||
import textwrap | ||
import threading | ||
import unittest | ||
import unittest.mock | ||
from unittest.mock import MagicMock | ||
|
||
import fmf | ||
import pytest | ||
|
||
import tmt.config | ||
from tmt.utils import Path | ||
|
||
|
||
@pytest.fixture | ||
def config_path(tmppath: Path, monkeypatch) -> Path: | ||
config_path = tmppath / 'config' | ||
config_path.mkdir() | ||
monkeypatch.setattr(tmt.config, 'effective_config_dir', MagicMock(return_value=config_path)) | ||
return config_path | ||
|
||
|
||
def test_config(config_path: Path): | ||
""" Config smoke test """ | ||
run = Path('/var/tmp/tmt/test') | ||
config1 = tmt.config.Config() | ||
config1.last_run = run | ||
config2 = tmt.config.Config() | ||
assert config2.last_run.resolve() == run.resolve() | ||
|
||
|
||
def test_last_run_race(tmppath: Path, monkeypatch): | ||
""" Race in last run symlink shouldn't be fatal """ | ||
config_path = tmppath / 'config' | ||
config_path.mkdir() | ||
monkeypatch.setattr(tmt.config, 'effective_config_dir', MagicMock(return_value=config_path)) | ||
mock_logger = unittest.mock.MagicMock() | ||
monkeypatch.setattr(tmt.utils.log, 'warning', mock_logger) | ||
config = tmt.config.Config() | ||
results = queue.Queue() | ||
threads = [] | ||
|
||
def create_last_run(config, counter): | ||
try: | ||
last_run_path = tmppath / f"run-{counter}" | ||
last_run_path.mkdir() | ||
val = config.last_run = last_run_path | ||
results.put(val) | ||
except Exception as err: | ||
results.put(err) | ||
|
||
total = 20 | ||
for i in range(total): | ||
threads.append(threading.Thread(target=create_last_run, args=(config, i))) | ||
for t in threads: | ||
t.start() | ||
for t in threads: | ||
t.join() | ||
|
||
all_good = True | ||
for _ in threads: | ||
value = results.get() | ||
if isinstance(value, Exception): | ||
# Print exception for logging | ||
print(value) | ||
all_good = False | ||
assert all_good | ||
# Getting into race is not certain, do not assert | ||
# assert mock_logger.called | ||
assert config.last_run, "Some run was stored as last run" | ||
|
||
|
||
def test_link_config_invalid(config_path: Path): | ||
config_yaml = textwrap.dedent(""" | ||
issue-tracker: | ||
- type: jiRA | ||
url: invalid_url | ||
tmt-web-url: https:// | ||
unknown: value | ||
additional_key: | ||
foo: bar | ||
""").strip() | ||
fmf.Tree.init(path=config_path) | ||
(config_path / 'link.fmf').write_text(config_yaml) | ||
|
||
with pytest.raises(tmt.utils.MetadataError) as error: | ||
_ = tmt.config.Config().link | ||
|
||
cause = str(error.value.__cause__) | ||
assert '6 validation errors for LinkConfig' in cause | ||
assert re.search(r'type\s*value is not a valid enumeration member', cause) | ||
assert re.search(r'url\s*invalid or missing URL scheme', cause) | ||
assert re.search(r'tmt-web-url\s*URL host invalid', cause) | ||
assert re.search(r'unknown\s*extra fields not permitted', cause) | ||
assert re.search(r'token\s*field required', cause) | ||
assert re.search(r'additional_key\s*extra fields not permitted', cause) | ||
|
||
|
||
def test_link_config_valid(config_path: Path): | ||
config_yaml = textwrap.dedent(""" | ||
issue-tracker: | ||
- type: jira | ||
url: https://issues.redhat.com | ||
tmt-web-url: https://tmt-web-url.com | ||
token: secret | ||
""").strip() | ||
fmf.Tree.init(path=config_path) | ||
(config_path / 'link.fmf').write_text(config_yaml) | ||
|
||
link = tmt.config.Config().link | ||
|
||
assert link.issue_tracker[0].type == 'jira' | ||
assert link.issue_tracker[0].url == 'https://issues.redhat.com' | ||
assert link.issue_tracker[0].tmt_web_url == 'https://tmt-web-url.com' | ||
assert link.issue_tracker[0].token == 'secret' | ||
|
||
|
||
def test_link_config_missing(config_path: Path): | ||
fmf.Tree.init(path=config_path) | ||
|
||
assert tmt.config.Config().link is None | ||
|
||
|
||
def test_link_config_empty(config_path: Path): | ||
fmf.Tree.init(path=config_path) | ||
(config_path / 'link.fmf').touch() | ||
|
||
with pytest.raises(tmt.utils.SpecificationError) as error: | ||
_ = tmt.config.Config().link | ||
|
||
cause = str(error.value.__cause__) | ||
assert '1 validation error for LinkConfig' in cause | ||
assert re.search(r'issue-tracker\s*field required', cause) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import functools | ||
import os | ||
from contextlib import suppress | ||
from typing import Optional, cast | ||
|
||
import fmf | ||
import fmf.utils | ||
from pydantic import ValidationError | ||
|
||
import tmt.utils | ||
from tmt._compat.pathlib import Path | ||
from tmt.config.models.link import LinkConfig | ||
|
||
# Config directory | ||
DEFAULT_CONFIG_DIR = Path('~/.config/tmt') | ||
|
||
|
||
def effective_config_dir() -> Path: | ||
""" | ||
Find out what the actual config directory is. | ||
If ``TMT_CONFIG_DIR`` variable is set, it is used. Otherwise, | ||
:py:const:`DEFAULT_CONFIG_DIR` is picked. | ||
""" | ||
|
||
if 'TMT_CONFIG_DIR' in os.environ: | ||
return Path(os.environ['TMT_CONFIG_DIR']).expanduser() | ||
|
||
return DEFAULT_CONFIG_DIR.expanduser() | ||
|
||
|
||
class Config: | ||
""" User configuration """ | ||
|
||
def __init__(self) -> None: | ||
""" Initialize config directory path """ | ||
self.path = effective_config_dir() | ||
self.logger = tmt.utils.log | ||
|
||
try: | ||
self.path.mkdir(parents=True, exist_ok=True) | ||
except OSError as error: | ||
raise tmt.utils.GeneralError( | ||
f"Failed to create config '{self.path}'.") from error | ||
|
||
@property | ||
def _last_run_symlink(self) -> Path: | ||
return self.path / 'last-run' | ||
|
||
@property | ||
def last_run(self) -> Optional[Path]: | ||
""" Get the last run workdir path """ | ||
return self._last_run_symlink.resolve() if self._last_run_symlink.is_symlink() else None | ||
|
||
@last_run.setter | ||
def last_run(self, workdir: Path) -> None: | ||
""" Set the last run to the given run workdir """ | ||
|
||
with suppress(OSError): | ||
self._last_run_symlink.unlink() | ||
|
||
try: | ||
self._last_run_symlink.symlink_to(workdir) | ||
except FileExistsError: | ||
# Race when tmt runs in parallel | ||
self.logger.warning( | ||
f"Unable to mark '{workdir}' as the last run, " | ||
"'tmt run --last' might not pick the right run directory.") | ||
except OSError as error: | ||
raise tmt.utils.GeneralError( | ||
f"Unable to save last run '{self.path}'.\n{error}") | ||
|
||
@functools.cached_property | ||
def fmf_tree(self) -> fmf.Tree: | ||
""" Return the configuration tree """ | ||
try: | ||
return fmf.Tree(self.path) | ||
except fmf.utils.RootError as error: | ||
raise tmt.utils.MetadataError(f"Config tree not found in '{self.path}'.") from error | ||
|
||
@property | ||
def link(self) -> Optional[LinkConfig]: | ||
""" Return the link configuration, if present. """ | ||
link_config = cast(Optional[fmf.Tree], self.fmf_tree.find('/link')) | ||
if not link_config: | ||
return None | ||
try: | ||
return LinkConfig.parse_obj(link_config.data) | ||
except ValidationError as error: | ||
raise tmt.utils.SpecificationError( | ||
f"Invalid link configuration in '{link_config.name}'.") from error |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from pydantic import BaseModel, Extra | ||
|
||
|
||
def create_alias(name: str) -> str: | ||
return name.replace('_', '-') | ||
|
||
|
||
class BaseConfig(BaseModel): | ||
class Config: | ||
# Accept only keys with dashes instead of underscores | ||
alias_generator = create_alias | ||
extra = Extra.forbid | ||
validate_assignment = True |
Oops, something went wrong.