diff --git a/.gitignore b/.gitignore index 0d20b64..670777e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ *.pyc +.coverage +.tox diff --git a/test/README.md b/test/README.md index 824dbca..2cd4290 100644 --- a/test/README.md +++ b/test/README.md @@ -1,24 +1,41 @@ Testing ======= -Dotbot testing code uses [Vagrant][vagrant] to run all tests inside a virtual -machine to have tests be completely isolated from the host machine. The test -driver relies on the [Sahara][sahara] plugin to snapshot and roll back virtual -machine state. The tests are deterministic, and each test is run in a virtual -machine with fresh state, ensuring that tests that modify system state are -easily repeatable. +Testing is run against multiple Python using ``tox``. It is recommended to use ``pyenv`` to manage +your Python version. -Running the Tests ------------------ -Before running the tests, the virtual machine must be running. It can be -started by running `vagrant up`. +Setup +===== -The test suite can be run by running `./test`. Selected tests can be run by -passing paths to the tests as arguments to `./test`. +* Ensure git submodules are up to date +* Install pyenv +* Install Python versions -When finished with testing, it is good to shut down the virtual machine by -running `vagrant halt`. +``` +pyenv install 3.4.3 +pyenv install 3.3.6 +pyenv install 3.2.6 +pyenv install 2.7.10 +pyenv install 2.6.9 +``` + +* *cd* into the *dotbot* repository and set the local Python versions for pyenv + +``` +pyenv local 3.4.3 3.3.6 3.2.6 2.7.10 2.6.9 +``` + +* Install test requirements + +``` +pip install tox +pyenv rehash +``` + + +Running the Test Suite +====================== + +Once the environment has been setup, simply run the ``tox`` command in the ``dotbot`` directory -[vagrant]: https://www.vagrantup.com/ -[sahara]: https://github.com/jedi4ever/sahara diff --git a/test/Vagrantfile b/test/Vagrantfile deleted file mode 100644 index 4463f99..0000000 --- a/test/Vagrantfile +++ /dev/null @@ -1,10 +0,0 @@ -Vagrant.configure(2) do |config| - config.vm.box = 'ubuntu/trusty64' - - # sync by copying for isolation - config.vm.synced_folder "..", "/dotbot", type: "rsync", - rsync__exclude: ".git/" - - # disable default synced folder - config.vm.synced_folder ".", "/vagrant", disabled: true -end diff --git a/test/driver-lib.bash b/test/driver-lib.bash deleted file mode 100644 index f7b25ba..0000000 --- a/test/driver-lib.bash +++ /dev/null @@ -1,120 +0,0 @@ -MAXRETRY=5 -TIMEOUT=1 - -red() { - if [ -t 1 ]; then - printf "\033[31m%s\033[0m\n" "$*" - else - printf "%s\n" "$*" - fi -} - -green() { - if [ -t 1 ]; then - printf "\033[32m%s\033[0m\n" "$*" - else - printf "%s\n" "$*" - fi -} - -yellow() { - if [ -t 1 ]; then - printf "\033[33m%s\033[0m\n" "$*" - else - printf "%s\n" "$*" - fi -} - - -check_prereqs() { - if ! (vagrant ssh -c 'exit') >/dev/null 2>&1; then - >&2 echo "vagrant vm must be running." - return 1 - fi - if ! (vagrant plugin list | grep '^sahara\s\+') >/dev/null 2>&1; then - >&2 echo "vagrant plugin 'sahara' is not installed." - return 1 - fi -} - -until_success() { - local timeout=${TIMEOUT} - local attempt=0 - while [ $attempt -lt $MAXRETRY ]; do - if ($@) >/dev/null 2>&1; then - return 0 - fi - sleep $timeout - timeout=$((timeout * 2)) - attempt=$((attempt + 1)) - done - - return 1 -} - -wait_for_vagrant() { - until_success vagrant ssh -c 'exit' -} - -rollback() { - vagrant sandbox rollback >/dev/null 2>&1 && - wait_for_vagrant && - vagrant rsync >/dev/null 2>&1 -} - -initialize() { - echo "initializing." - vagrant sandbox on >/dev/null 2>&1 - tests_run=0 - tests_passed=0 - tests_failed=0 - tests_total="${1}" - local plural="" && [ "${tests_total}" -gt 1 ] && plural="s" - printf -- "running %d test%s...\n\n" "${tests_total}" "${plural}" -} - -pass() { - tests_passed=$((tests_passed + 1)) - green "-> ok." - echo -} - -fail() { - tests_failed=$((tests_failed + 1)) - yellow "-> fail!" - echo -} - -run_test() { - tests_run=$((tests_run + 1)) - printf '[%d/%d] (%s)\n' "${tests_run}" "${tests_total}" "${1}" - rollback || die "unable to rollback vm." # start with a clean slate - if vagrant ssh -c "cd /dotbot/test/tests && bash ${1}" 2>/dev/null; then - pass - else - fail - fi -} - -report() { - printf -- "test report\n" - printf -- "-----------\n" - printf -- "- %3d run\n" ${tests_run} - printf -- "- %3d passed\n" ${tests_passed} - if [ ${tests_failed} -gt 0 ]; then - printf -- "- %3d failed\n" ${tests_failed} - echo - red "==> not ok!" - return 1 - else - echo - green "==> all ok." - return 0 - fi -} - -die() { - >&2 echo $@ - >&2 echo "terminating..." - exit 1 -} diff --git a/test/test b/test/test deleted file mode 100755 index 7f4b8f1..0000000 --- a/test/test +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -set -e - -BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "${BASEDIR}" -. "./driver-lib.bash" - -start="$(date +%s)" - -check_prereqs || die "prerequisites unsatsfied." - -declare -a tests=() - -if [ $# -eq 0 ]; then - while read file; do - tests+=("${file}") - done < <(find tests -type f -name '*.bash') -else - tests=("$@") -fi - -initialize "${#tests[@]}" - -for file in "${tests[@]}"; do - run_test "$(basename "${file}")" -done - -if report; then - ret=0 -else - ret=1 -fi - -echo "(tests run in $(($(date +%s) - start)) seconds)" -exit ${ret} diff --git a/test/test-lib.bash b/test/test-lib.bash deleted file mode 100644 index 425ae56..0000000 --- a/test/test-lib.bash +++ /dev/null @@ -1,51 +0,0 @@ -DEBUG=false -DOTFILES='/home/vagrant/dotfiles' -INSTALL_CONF='install.conf.yaml' - -test_run_() { - if ! ${DEBUG}; then - (eval "$*") >/dev/null 2>&1 - else - (eval "$*") - fi -} - -test_expect_success() { - local tag=${1} && shift - if ! test_run_ "$@"; then - >&2 echo "- ${tag} failed." - exit 1 - fi -} - -test_expect_failure() { - local tag=${1} && shift - if test_run_ "$@"; then - >&2 echo "- ${tag} failed." - exit 1 - fi -} - -check_vm() { - if [ "$(whoami)" != "vagrant" ]; then - >&2 echo "test can't run outside vm!" - exit 1 - fi -} - -initialize() { - check_vm - echo "${test_description}" - mkdir -p "${DOTFILES}" - cd -} - -run_dotbot() { - ( - cd "${DOTFILES}" - cat > "${INSTALL_CONF}" - /dotbot/bin/dotbot -d . -c "${INSTALL_CONF}" "${@}" - ) -} - -initialize diff --git a/test/test_clean.py b/test/test_clean.py new file mode 100644 index 0000000..02060c5 --- /dev/null +++ b/test/test_clean.py @@ -0,0 +1,33 @@ +import os +from utils import DotbotTestCase + + +class MissingTestCase(DotbotTestCase): + def test_clean(self): + """ clean deletes links to missing files """ + self.add_file('f') + self.add_symlink('f') + self.add_symlink('g') + + self.run_dotbot(config='- clean: ["~"]') + + self.assertIsLinked('f') + self.assertDoesNotExist('g') + + def test_ignores_nonexistant(self): + """ clean ignores nonexistant directories """ + self.run_dotbot(config='- clean: ["~", "~/fake"]') + + def test_ignores_outside_linking(self): + """ clean ignores files linking outside dotfiles directory """ + self.add_symlink('f') + + with open(os.path.join(self.home_dir, 'g'), 'w') as g: + g.write('') + os.symlink(os.path.join(self.home_dir, 'g'), os.path.join(self.home_dir, '.g')) + + self.run_dotbot(config='- clean: ["~"]') + + self.assertDoesNotExist('f') + self.assertEqual(os.stat(os.path.join(self.home_dir, 'g')), + os.stat(os.path.join(self.home_dir, '.g'))) diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 0000000..7a25e97 --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,10 @@ +import os +from utils import DotbotTestCase + + +class ConfigTestCase(DotbotTestCase): + def test_blank_config_allowed(self): + self.run_dotbot(config='[]') + + def test_empty_config_not_allowed(self): + self.assertRaises(SystemExit, self.run_dotbot, skip_config=True) diff --git a/test/tests/clean-missing.bash b/test/tests/clean-missing.bash deleted file mode 100644 index dc6c84c..0000000 --- a/test/tests/clean-missing.bash +++ /dev/null @@ -1,19 +0,0 @@ -test_description='clean deletes links to missing files' -. '../test-lib.bash' - -test_expect_success 'setup' ' -touch ${DOTFILES}/f && -ln -s ${DOTFILES}/f ~/.f && -ln -s ${DOTFILES}/g ~/.g -' - -test_expect_success 'run' ' -run_dotbot < ${DOTFILES}/h -' - -test_expect_success 'run' ' -export APPLE="h" && -run_dotbot < ${DOTFILES}/f && -echo "grape" > ${DOTFILES}/h -' - -test_expect_success 'run' ' -export ORANGE=".config" && -export BANANA="g" && -unset PEAR && -run_dotbot < ${DOTFILES}/\$ORANGE -' - -test_expect_success 'run' ' -unset ORANGE && -run_dotbot < ${DOTFILES}/f && -echo "grape" > ~/.f -' - -test_expect_failure 'run' ' -run_dotbot < ${DOTFILES}/f && -echo "grape" > ~/.f -' - -test_expect_failure 'run' ' -run_dotbot < ${DOTFILES}/f && -echo "grape" > ~/f && -ln -s ~/f ~/.f -' - -test_expect_success 'run' ' -run_dotbot <= 3: + inject('pyyaml/lib3') +else: + inject('pyyaml/lib') + +if os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'dotbot')): + if PROJECT_ROOT_DIRECTORY not in sys.path: + sys.path.insert(0, PROJECT_ROOT_DIRECTORY) + os.putenv('PYTHONPATH', PROJECT_ROOT_DIRECTORY) + + +import shutil +import dotbot +import tempfile +import unittest +import shlex + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +# mock is built-in after py3.3, otherwise import the 3rd party mock +try: + from unittest import mock +except ImportError: + import mock + + +class DotbotTestCase(unittest.TestCase): + """ Dotbot specific TestCase that will take care of setting up temporary directories and a convenience + function for simulating running Dotbot from the CLI """ + + def setUp(self): + """ Creates a temporary directory to run in for every test """ + self.tempdir = tempfile.mkdtemp() + self.dotbot_dir = os.path.join(self.tempdir, 'dotbot') + self.home_dir = os.path.join(self.tempdir, 'home') + self.config_file = os.path.join(self.dotbot_dir, 'dotbot.config') + + if int(os.environ.get('NO_CLEAN', '0')): + print(self.tempdir) # If we're not going to clean up, print out where we're runnin + + os.mkdir(self.dotbot_dir) + os.mkdir(self.home_dir) + + def add_file(self, filename, contents=""): + """ Create a file in temporary dotbot_dir. Optionally with content """ + with open(os.path.join(self.dotbot_dir, filename), 'w') as f: + f.write(contents) + + def add_dirs(self, path): + """ Create directories within the temporary dotbot_dir. Acts like ``mkdir -p``. Path is relative to the + dotbot dir """ + os.makedirs(os.path.join(self.dotbot_dir, path)) + + def add_symlink(self, path): + """ Creates a symlink from ``self.home_dir``/path to ``self.dotbot_dir``/path """ + os.symlink(os.path.join(self.dotbot_dir, path), os.path.join(self.home_dir, path)) + + def assertIsLinked(self, path): + """ Asserts that the given ``path`` in self.home_dir is symlinked to the corresponding ``path`` + in self.dotbot_dir """ + self.assertTrue(os.path.islink(os.path.join(self.home_dir, path))) + self.assertEqual(os.stat(os.path.join(self.dotbot_dir, path)), + os.stat(os.path.join(self.home_dir, path))) + + def assertDoesNotExist(self, path): + """ Asserts the given ``path`` in self.home_dir does not exist """ + self.assertFalse(os.path.exists(os.path.join(self.home_dir, path)) or + os.path.lexists(os.path.join(self.home_dir, path))) + + def run_dotbot(self, config="", args="", skip_config=False): + """ Runs dotbot in a simulated way temporarily intercepting stdout, stderr, setting the HOME + environment variable to ``self.home_dir``, and setting sys.argv to the simulated command + line options. ``run_dotbot`` will automatically set the ``--base-directory`` and + ``--config-file`` command line arguments appropriately. + + The ``config`` argument is a string that is written out as the configuration file for dotbot + to use. The ``args`` argument is a string of extra command line arguments to pass to dotbot + just like they would be passed on the command line. + + If ``skip_config`` is True, a config file will not be written. + + Returns a tuple (out, err) of the stdout and stderr from dotbot. + """ + + if not skip_config: + with open(self.config_file, 'w') as f: + f.write(config) + + base_args = [ + 'dotbot', + '--base-directory', self.tempdir, + '--config-file', self.config_file, + ] + + old_home, os.environ['HOME'] = os.path.expanduser('~'), self.home_dir + old_stdout, sys.stdout = sys.stdout, StringIO() + old_stderr, sys.stderr = sys.stderr, StringIO() + old_argv, sys.argv = sys.argv, base_args + shlex.split(args) + + try: + dotbot.cli.main() + finally: + os.environ['HOME'] = old_home + out, sys.stdout = sys.stdout.getvalue(), old_stdout + err, sys.stderr = sys.stderr.getvalue(), old_stderr + sys.argv = old_argv + print("\nDotbot Output:") + print('out:\n', out) + print('err:\n', err) + + return out, err + + def tearDown(self): + """ Clean up the temporary directory that was created. Set NO_CLEAN=1 to disable cleanup """ + if not int(os.environ.get('NO_CLEAN', '0')): + shutil.rmtree(self.tempdir) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..877c01b --- /dev/null +++ b/tox.ini @@ -0,0 +1,15 @@ +[tox] +skipsdist=True +envlist = + {py26,py27,py32,py33,py34} + +[testenv] +deps= + {py26,py27,py32}: mock + nose + coverage + python-coveralls +commands= + nosetests {posargs:--with-coverage --cover-package=dotbot} + +