Skip to content
Open
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Reorganized CLI arguments into logical groups (Profile Management, Import & Export, Options, Miscellaneous) for better readability
- Improved all help messages with clearer and more descriptive text
- Return appropriate exit codes when ran from the CLI.

### Added
- Add 'py.test' framework with plug-ins.
- Create basic test fixtures for testing 'konsave' in a shell like environment.
- Add basic test cases for verifying CLI exit codes.

## [2.2.0] - 2023-01-31
### Added
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ maintclean: distclean
# mypy -p tests --no-strict-optional --ignore-missing-imports --install-types

tests:
python3 ./test.py
pytest tests
2 changes: 1 addition & 1 deletion konsave/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
__version__ = distribution(__name__).version
except PackageNotFoundError:
# Package is not installed
pass
__version__ = "unknown"
83 changes: 53 additions & 30 deletions konsave/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import argparse
import os
import sys
import shutil
from importlib.resources import files

Expand Down Expand Up @@ -41,30 +42,35 @@ def _get_parser() -> argparse.ArgumentParser:
"Profile Management", "Commands for managing configuration profiles"
)
profile_group.add_argument(
"-l", "--list",
"-l",
"--list",
action="store_true",
help="List all saved profiles",
)
profile_group.add_argument(
"-s", "--save",
"-s",
"--save",
type=str,
help="Save current configuration as a new profile",
metavar="<name>",
)
profile_group.add_argument(
"-a", "--apply",
"-a",
"--apply",
type=str,
help="Apply a saved profile to restore its configuration",
metavar="<name>",
)
profile_group.add_argument(
"-r", "--remove",
"-r",
"--remove",
type=str,
help="Delete a saved profile permanently",
metavar="<name>",
)
profile_group.add_argument(
"-w", "--wipe",
"-w",
"--wipe",
action="store_true",
help="Delete all saved profiles (use with caution!)",
)
Expand All @@ -75,13 +81,15 @@ def _get_parser() -> argparse.ArgumentParser:
"Commands for sharing profiles with others",
)
transfer_group.add_argument(
"-e", "--export-profile",
"-e",
"--export-profile",
type=str,
help="Export a profile as a shareable .knsv archive file",
metavar="<name>",
)
transfer_group.add_argument(
"-i", "--import-profile",
"-i",
"--import-profile",
type=str,
help="Import a profile from a .knsv archive file",
metavar="<path>",
Expand All @@ -92,17 +100,20 @@ def _get_parser() -> argparse.ArgumentParser:
"Options", "Additional options to modify command behavior"
)
options_group.add_argument(
"-f", "--force",
"-f",
"--force",
action="store_true",
help="Force overwrite when saving/exporting (skip confirmation prompts)",
)
options_group.add_argument(
"-d", "--export-directory",
"-d",
"--export-directory",
help="Specify custom directory for exported profile (default: current directory)",
metavar="<directory>",
)
options_group.add_argument(
"-n", "--export-name",
"-n",
"--export-name",
help="Specify custom filename for exported profile archive",
metavar="<archive-name>",
)
Expand All @@ -113,7 +124,8 @@ def _get_parser() -> argparse.ArgumentParser:
"-h", "--help", action="help", help="Show this help message and exit"
)
misc_group.add_argument(
"-v", "--version",
"-v",
"--version",
action="store_true",
help="Display the current version of Konsave",
)
Expand All @@ -135,25 +147,36 @@ def main():
parser = _get_parser()
args = parser.parse_args()

if args.list:
list_profiles(list_of_profiles, length_of_lop)
elif args.save:
save_profile(args.save, list_of_profiles, force=args.force)
elif args.remove:
remove_profile(args.remove, list_of_profiles, length_of_lop)
elif args.apply:
apply_profile(args.apply, list_of_profiles, length_of_lop)
elif args.export_profile:
export(args.export_profile, list_of_profiles, length_of_lop,
args.export_directory, args.export_name, args.force)
elif args.import_profile:
import_profile(args.import_profile)
elif args.version:
print(f"Konsave: {VERSION}")
elif args.wipe:
wipe()
else:
parser.print_help()
try:
if args.list:
list_profiles(list_of_profiles, length_of_lop)
elif args.save:
save_profile(args.save, list_of_profiles, force=args.force)
elif args.remove:
remove_profile(args.remove, list_of_profiles, length_of_lop)
elif args.apply:
apply_profile(args.apply, list_of_profiles, length_of_lop)
elif args.export_profile:
export(
args.export_profile,
list_of_profiles,
length_of_lop,
args.export_directory,
args.export_name,
args.force,
)
elif args.import_profile:
import_profile(args.import_profile)
elif args.version:
print(f"Konsave: {VERSION}")
elif args.wipe:
wipe()
else:
parser.print_help()
except RuntimeError:
sys.exit(1)

sys.exit(0)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion konsave/consts.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
This module contains all the variables for konsave
"""

import os
from konsave import __version__


HOME = os.path.expandvars("$HOME")
CONFIG_DIR = os.path.join(HOME, ".config")
SHARE_DIR = os.path.join(HOME, ".local/share")
Expand Down
3 changes: 1 addition & 2 deletions konsave/funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ def inner_func(*args, **kwargs):
print(
f"Konsave: {err}\nPlease check the log at {log_file} for more details."
)
return None

raise RuntimeError from err
return function

return inner_func
Expand Down
1 change: 1 addition & 0 deletions konsave/parse.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
This module parses conf.yaml
"""

import os
import re
from konsave.consts import HOME, CONFIG_DIR, SHARE_DIR, BIN_DIR
Expand Down
4 changes: 4 additions & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ aiohttp>=3.7.4
aiohttp_cors>=0.7.0
black>=20.8b1
pylint>=2.7.2
ipython==9.13.0
pytest==9.0.3
pytest-mock==3.15.1
pyfakefs==6.2.0
Empty file added tests/__init__.py
Empty file.
89 changes: 89 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import os
import sys
import pytest
import konsave.consts
import konsave.__main__


@pytest.fixture(scope="function")
def mock_cli(request, mocker):
"""Mock 'sys.argv' for testing the 'konsave' CLI interface."""

test_args = ["konsave", request.param]

mocker.patch.object(sys, "argv", test_args)


@pytest.fixture(scope="function")
def konsave_env_paths():
"""The 'konsave.consts' environment paths."""

knsv_env_paths = {
"HOME": konsave.consts.HOME,
"CONFIG_DIR": konsave.consts.CONFIG_DIR,
"SHARE_DIR": konsave.consts.SHARE_DIR,
"BIN_DIR": konsave.consts.BIN_DIR,
"KONSAVE_DIR": konsave.consts.KONSAVE_DIR,
"PROFILES_DIR": konsave.consts.PROFILES_DIR,
"CACHE_DIR": os.path.join(konsave.consts.HOME, ".cache"),
"CONFIG_FILE": konsave.consts.CONFIG_FILE,
}

return knsv_env_paths


@pytest.fixture(scope="function")
def konsave_test_env(fs, konsave_env_paths):
"""Construct the fake filesystem directory structure for testing 'konsave'."""

for p in [v for k, v in konsave_env_paths.items() if "FILE" not in k]:
fs.create_dir(p)

return fs


@pytest.fixture(scope="function")
def konsave_conf_kde(konsave_test_env):
"""Inject real 'kde' config file into the fake filesystem."""

konsave_test_env.add_real_file(
source_path="konsave/conf_kde.yaml",
target_path="/home/{user_name}/.config/konsave/conf.yaml".format(
user_name=os.getlogin()
),
)

return konsave_test_env


@pytest.fixture(scope="function")
def konsave_conf_other(konsave_test_env, konsave_env_paths):
"""Inject real 'other' config file into the fake filesystem."""

konsave_test_env.add_real_file(
source_path="konsave/conf_other.yaml",
target_path=os.path.join(konsave_env_paths["CONFIG_DIR"], "conf.yaml"),
)

return konsave_test_env


@pytest.fixture(scope="function")
def basic_kde_test_env(mocker, konsave_conf_kde, konsave_env_paths):
"""Configure the test directory structure for a basic two profile 'konsave' environment."""

konsave_conf_kde.create_dir(
os.path.join(konsave_env_paths["PROFILES_DIR"], "test_profile_1")
)
konsave_conf_kde.create_dir(
os.path.join(konsave_env_paths["PROFILES_DIR"], "test_profile_2")
)

# This must be patched because 'konsave.consts' was already loaded by this module.
mocker.patch.object(
konsave.__main__,
"list_of_profiles",
os.listdir(konsave_env_paths["PROFILES_DIR"]),
)

return konsave_conf_kde
63 changes: 63 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import pytest
from konsave.__main__ import main as konsave_main


class TestCli(object):
"""Test cases for CLI"""

@pytest.mark.parametrize("mock_cli", ["-h"], indirect=["mock_cli"])
def test_help(self, capsys, mock_cli, basic_kde_test_env):
"""Verify that the '-h' argument returns with exit code 0 and appropriate output."""

with pytest.raises(SystemExit) as e:
konsave_main()

assert e.value.code == 0
assert "usage: Konsave" in capsys.readouterr().out

@pytest.mark.parametrize("mock_cli", ["-l"], indirect=["mock_cli"])
def test_list(self, capsys, mock_cli, basic_kde_test_env):
"""
Verify that the '-l' argument returns with exit code 0 and lists two
profiles in output.
"""

with pytest.raises(SystemExit) as e:
konsave_main()

assert e.value.code == 0
assert "test_profile_1" and "test_profile_2" in capsys.readouterr().out


class TestCliNegative(object):
"""Negative test cases for CLI."""

@pytest.mark.parametrize("mock_cli", ["-r does_not_exist"], indirect=["mock_cli"])
def test_attempt_remove_non_existent_profile(
self, capsys, mock_cli, basic_kde_test_env
):
"""
Verify that the '-r' argument with non-existent profile returns with exit
code 1 and appropriate error message in output.
"""

with pytest.raises(SystemExit) as e:
konsave_main()

assert e.value.code == 1
assert "Konsave: Profile not found." in capsys.readouterr().out

@pytest.mark.parametrize("mock_cli", ["-i does_not_exist"], indirect=["mock_cli"])
def test_attempt_import_non_existent_profile(
self, capsys, mock_cli, basic_kde_test_env
):
"""
Verify that the '-i' argument with non-existent profile returns with exit
code 1 and appropriate error message in output.
"""

with pytest.raises(SystemExit) as e:
konsave_main()

assert e.value.code == 1
assert "Konsave: Not a valid konsave file" in capsys.readouterr().out