diff --git a/.travis.yml b/.travis.yml index 643cd55..1cbb8f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,20 @@ +dist: bionic language: rust cache: cargo rust: nightly +sudo: true + before_script: + - sudo add-apt-repository -y ppa:deadsnakes/ppa + - sudo apt-get update + - sudo apt-get install python3.7 python3.7-distutils + - curl https://bootstrap.pypa.io/get-pip.py | sudo python3.7 + - rustup toolchain install nightly-2019-07-25 + - rustup default nightly-2019-07-25 - rustup component add rustfmt-preview - rustup update - cargo update + - sudo pip3.7 install -r ./tests/requirements.txt script: - cargo fmt --all -- --check diff --git a/tests/nesttests.py b/tests/nesttests.py new file mode 100644 index 0000000..81d551e --- /dev/null +++ b/tests/nesttests.py @@ -0,0 +1,224 @@ +import json +import os +import shutil +import subprocess +import tarfile +import tempfile +import toml +from typing import Any, Dict, List +from contextlib import contextmanager +from time import sleep + + +class Package: + def __init__( + self, + name: str, + category: str, + version: str, + kind: str, + description: str = "A package", + tags: List[str] = None, + maintainer: str = "nest-tests@raven-os.org", + licenses: List[str] = None, + upstream_url: str = None, + ): + self.name = name + self.category = category + self.version = version + self.kind = kind + self.description = description + self.tags = tags or [] + self.maintainer = maintainer + self.licenses = licenses or ["gpl_v3"] + self.upstream_url = upstream_url or "https://google.com" + self.dependencies = {} + self.files = {} + + def full_name(self) -> str: + return f"tests::{self.category}/{self.name}" + + def package_id(self) -> str: + return f"tests::{self.category}/{self.name}#{self.version}" + + def add_dependency(self, dependency: 'Package', version_requirement: str) -> 'Package': + self.dependencies[dependency.full_name()] = version_requirement + return self + + def add_file(self, path, with_content=None, from_reader=None) -> 'Package': + if not (with_content ^ from_reader): + raise ValueError("Invalid arguments: exactly one of 'with_content' and 'from_reader' must be used") + # self.files[path] = + return self + + def add_symlink(self, path: str, target: str) -> 'Package': + return self + + def add_directory(self, path: str) -> 'Package': + return self + + def _create_in(self, directory: str): + directory = f"{directory}/{self.category}/{self.name}" + os.makedirs(directory, exist_ok=True) + manifest = { + "name": self.name, + "category": self.category, + "version": self.version, + "kind": self.kind, + "wrap_date": "2019-05-27T16:34:15Z", + "metadata": { + "description": self.description, + "tags": self.tags, + "maintainer": self.maintainer, + "licenses": self.licenses, + "upstream_url": self.upstream_url + }, + "dependencies": self.dependencies + } + manifest_path = f"{directory}/manifest.toml" + with open(manifest_path, 'x') as f: + toml.dump(manifest, f) + + files = [(manifest_path, "manifest.toml")] + + if self.kind == "effective": + with tarfile.open(f"{directory}/data.tar.gz", "w:gz") as tar: + pass + files.append((f"{directory}/data.tar.gz", "data.tar.gz")) + + with tarfile.open(f"{directory}/{self.name}-{self.version}.nest", "x") as tar: + for name, arcname in files: + tar.add(name, arcname=arcname) + os.remove(name) + + +def _create_packages(packages: List[Package]): + for package in packages: + package._create_in("/tmp/nest-server/packages") + + +def _create_configuration_file(): + configuration = { + 'name': 'tests', + 'pretty_name': 'Tests', + 'package_dir': './packages/', + 'cache_dir': './cache/', + 'auth_token': 'a_very_strong_password', 'links': [ + {'name': 'Tests', 'url': '/', 'active': True}, + {'name': 'Stable', + 'url': 'https://stable.raven-os.org'}, + {'name': 'Beta', + 'url': 'https://beta.raven-os.org'}, + {'name': 'Unstable', + 'url': 'https://unstable.raven-os.org'} + ] + } + with open("/tmp/nest-server/Repository.toml", "w") as f: + toml.dump(configuration, f) + + +@contextmanager +def nest_server(packages: List[Package] = None): + _create_packages(packages or []) + _create_configuration_file() + nest_server_path = os.getenv("NEST_SERVER") + p = subprocess.Popen(["cargo", "run", "-q"], cwd=nest_server_path, stdout=subprocess.DEVNULL) + sleep(0.5) # Wait a bit so the server initializes properly + try: + yield + finally: + p.kill() + if os.path.exists(f"{nest_server_path}/packages"): + shutil.rmtree(f"{nest_server_path}/packages") + if os.path.exists(f"{nest_server_path}/cache"): + shutil.rmtree(f"{nest_server_path}/cache") + + +@contextmanager +def create_config(entries: Dict[str, Dict[str, Any]] = None): + entries = entries or {"repositories": {"tests": {"mirrors": ["http://localhost:8000"]}}} + path = tempfile.NamedTemporaryFile().name + with open(path, 'w') as f: + toml.dump(entries, f) + try: + yield path + finally: + os.remove(path) + + +class _Depgraph: + def __init__(self, path: str): + if os.path.exists(path): + self.data = json.load(open(path, 'r')) + else: + self.data = {"node_names": {}} + + def installed_packages(self): + return filter(lambda name: name[0] != '@', self.data["node_names"]) + + def groups(self): + return filter(lambda name: name[0] == '@', self.data["node_names"]) + + +class _Nest: + def __init__(self, config: str = None, chroot: str = None): + self.config = config + self.chroot = chroot + + def _run(self, *args: str, input_str: str = None): + cmd = ["sudo", f"PATH={os.getenv('PATH')}", "env", "cargo", "run", "-q", "--bin", "nest", "--"] + if self.config: + cmd += ("--config", self.config) + if self.chroot: + cmd += ("--chroot", self.chroot) + cmd += args + return subprocess.run(cmd, capture_output=True, input=input_str and input_str.encode()) + + def pull(self, confirm=True): + return self._run("pull", input_str="yes" if confirm else "no") + + def install(self, *packages: str, confirm=True): + return self._run("install", *packages, input_str="yes" if confirm else "no") + + def uninstall(self, *packages: str, confirm=True): + return self._run("uninstall", *packages, input_str="yes" if confirm else "no") + + def list(self): + pass + + def depgraph(self) -> _Depgraph: + return _Depgraph(f"{self.chroot}/var/nest/depgraph") + + def help(self): + return self._run("help") + + +def nest(config: str = None, chroot: str = None) -> _Nest: + chroot = chroot or os.getenv("NEST_CHROOT") + return _Nest(config, chroot) + + +class _Finest: + def __init__(self, config: str = None, chroot: str = None): + self.config = config + self.chroot = chroot + + def _run(self, *args: str, input_str: str = None): + cmd = ["sudo", f"PATH={os.getenv('PATH')}", "env", "cargo", "run", "-q", "--bin", "finest", "--"] + if self.config: + cmd += ("--config", self.config) + if self.chroot: + cmd += ("--chroot", self.chroot) + cmd += args + return subprocess.run(cmd, capture_output=True, input=input_str and input_str.encode()) + + def pull(self): + return self._run("pull", input_str="yes") + + def help(self): + return self._run("help") + + +def finest(config: str = None, chroot: str = None) -> _Finest: + chroot = chroot or os.getenv("NEST_CHROOT") + return _Finest(config, chroot) diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..089950d --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1 @@ +toml==0.10.0 diff --git a/tests/run.sh b/tests/run.sh index bf980b3..afdebbe 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -1,28 +1,50 @@ #!/usr/bin/env bash # Common variables shared among all tests -export NEST="cargo run --bin=nest" +export CARGO="env cargo" +export NEST_SERVER="/tmp/nest-server" +export PYTHONPATH=$(dirname "$0") +export NEST_CHROOT="/tmp/chroot/" # Colors export RED="\033[1;31m" export GREEN="\033[1;32m" export RESET="\033[0m" -declare -i nb_tests=1 +if [ ! -d $NEST_SERVER ]; then + echo "Cloning latest nest-server..." + git clone https://github.com/raven-os/nest-server $NEST_SERVER +elif [ ! -e $NEST_SERVER ]; then + echo "$NEST_SERVER already exists and is not a directory, aborting." + exit 1 +fi + +pushd $NEST_SERVER +$CARGO build +popd + +if [ -e $NEST_CHROOT ]; then + echo "$NEST_CHROOT already exists, aborting." + exit 1 +fi + +declare -i nb_tests=0 declare -i success=0 declare tests_dir=$(dirname "$0") # Run all tests -for ((i=1; i <= $nb_tests; i++)) do - $tests_dir/test_$i/run.sh > /dev/null 2> /dev/null - declare -i out_code=$? - - if [[ $out_code -eq 0 ]]; then - printf "[%02i] ${GREEN}OK${RESET}\n" $i - success=$(($success + 1)) - else - printf "[%02i] ${RED}KO${RESET}\n" $i - fi +for test in $tests_dir/test_*; do + $test/run.py + declare -i out_code=$? + + if [[ $out_code -eq 0 ]]; then + printf "[%02i] ${GREEN}OK${RESET}\n" $nb_tests + success=$(($success + 1)) + else + printf "[%02i] ${RED}KO${RESET}\n" $nb_tests + fi + nb_tests=$(($nb_tests + 1)) + sudo rm -rf $NEST_CHROOT done echo @@ -30,5 +52,5 @@ echo "$success/$nb_tests tests passed" # Exit 1 if any test failed to ensure the build fails on Travis if [[ $success -ne $nb_tests ]]; then - exit 1 + exit 1 fi diff --git a/tests/test_0/run.py b/tests/test_0/run.py new file mode 100755 index 0000000..1aa33b5 --- /dev/null +++ b/tests/test_0/run.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3.7 + +""" +Launching nest and finest with a valid configuration file should succeed +""" + +from nesttests import * + +assert nest().help().returncode == 0 +assert finest().help().returncode == 0 diff --git a/tests/test_1/run.py b/tests/test_1/run.py new file mode 100755 index 0000000..6c0ae7b --- /dev/null +++ b/tests/test_1/run.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3.7 + +""" +Launching nest and finest with the --help option should succeed, even with an invalid configuration file +""" + +from nesttests import * +import os + +assert nest(config="/non_existent/not_existing_either.toml").help().returncode == 0 +assert finest(config="/non_existent/not_existing_either.toml").help().returncode == 0 + +with create_config(entries={}) as config_path: + os.chmod(path=config_path, mode=0o222) # Make the configuration file write-only + assert nest(config=config_path).help().returncode == 0 + assert finest(config=config_path).help().returncode == 0 + +with create_config(entries={}) as config_path: + with open(config_path, 'w+') as f: + f.write("<(^v^)>") # Write invalid data + assert nest(config=config_path).help().returncode == 0 + assert finest(config=config_path).help().returncode == 0 diff --git a/tests/test_2/run.py b/tests/test_2/run.py new file mode 100755 index 0000000..25a8b9f --- /dev/null +++ b/tests/test_2/run.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3.7 + +""" +Pulling available repositories with a valid configuration file should succeed +""" + +from nesttests import * + +with nest_server(), create_config() as config_path: + assert nest(config=config_path).pull().returncode == 0 diff --git a/tests/test_3/run.py b/tests/test_3/run.py new file mode 100755 index 0000000..413b584 --- /dev/null +++ b/tests/test_3/run.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3.7 + +""" +Pulling available repositories with an invalid configuration file should fail +""" + +from nesttests import * +import os + +assert nest(config="/non_existent/not_existing_either.toml").pull().returncode == 1 +assert finest(config="/non_existent/not_existing_either.toml").pull().returncode == 1 + +with create_config(entries={}) as config_path: + os.chmod(path=config_path, mode=0o222) # Make the configuration file write-only + assert nest(config=config_path).pull().returncode == 1 + assert finest(config=config_path).pull().returncode == 1 + +with create_config(entries={}) as config_path: + with open(config_path, 'w+') as f: + f.write("<(^v^)>") # Write invalid data + assert nest(config=config_path).pull().returncode == 1 + assert finest(config=config_path).pull().returncode == 1 diff --git a/tests/test_4/run.py b/tests/test_4/run.py new file mode 100755 index 0000000..68b31e2 --- /dev/null +++ b/tests/test_4/run.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3.7 + +""" +Installation of an unavailable packages should trigger an error +""" + +from nesttests import * + +with nest_server(), create_config() as config_path: + nest = nest(config=config_path) + assert nest.pull().returncode == 0 + assert nest.install("unavailable-package").returncode == 1 diff --git a/tests/test_5/run.py b/tests/test_5/run.py new file mode 100755 index 0000000..3174b4d --- /dev/null +++ b/tests/test_5/run.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3.7 + +""" +Packages should be made available after a pull operation +""" + +from nesttests import * + +available_package = Package( + name="available-package", + category="sys-apps", + version="1.0.0", + kind="virtual", + description="A package", + tags=["test"] +) + +with nest_server(packages=[available_package]), create_config() as config_path: + nest = nest(config=config_path) + assert nest.install("available-package").returncode == 1 + assert nest.pull().returncode == 0 + assert nest.install("available-package", confirm=False).returncode == 0 + assert "tests::sys-apps/available-package" not in nest.depgraph().installed_packages() diff --git a/tests/test_6/run.py b/tests/test_6/run.py new file mode 100755 index 0000000..0c712ec --- /dev/null +++ b/tests/test_6/run.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3.7 + +""" +Standalone (without any dependencies) packages should be installable +""" + +from nesttests import * + +available_package = Package( + name="available-package", + category="sys-apps", + version="1.0.0", + kind="virtual", + description="A package", + tags=["test"] +) + +with nest_server(packages=[available_package]), create_config() as config_path: + nest = nest(config=config_path) + assert nest.pull().returncode == 0 + assert nest.install("available-package", confirm=True).returncode == 0 + assert "tests::sys-apps/available-package" in nest.depgraph().installed_packages() diff --git a/tests/test_7/run.py b/tests/test_7/run.py new file mode 100755 index 0000000..ec4ef12 --- /dev/null +++ b/tests/test_7/run.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3.7 + +""" +Packages with dependencies should be installable, and their dependencies should be installed with them +""" + +from nesttests import * + +some_library = Package( + name="some-library", + category="sys-libs", + version="1.0.0", + kind="virtual", +) + +some_package = Package( + name="some-package", + category="sys-apps", + version="1.0.0", + kind="virtual", +).add_dependency(some_library, "1.0.0") + +with nest_server(packages=[some_library, some_package]), create_config() as config_path: + nest = nest(config=config_path) + assert nest.pull().returncode == 0 + assert nest.install("some-package", confirm=True).returncode == 0 + assert "tests::sys-apps/some-package" in nest.depgraph().installed_packages() + assert "tests::sys-libs/some-library" in nest.depgraph().installed_packages() diff --git a/tests/test_8/run.py b/tests/test_8/run.py new file mode 100755 index 0000000..daafa3c --- /dev/null +++ b/tests/test_8/run.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3.7 + +""" +Removal of unknown packages should fail +""" + +from nesttests import * + +with nest_server(packages=[]), create_config() as config_path: + nest = nest(config=config_path) + assert nest.uninstall("some-package", confirm=True).returncode == 1