Merge branch 'kurtmckee/test-on-windows-issue-309'
This commit is contained in:
commit
3965e1a390
93 changed files with 2403 additions and 2115 deletions
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
|
@ -6,11 +6,22 @@ on:
|
|||
- cron: '0 8 * * 6'
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: 1
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python: ["2.7", "pypy2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy3.9"]
|
||||
name: "Test: Python ${{ matrix.python }}"
|
||||
os: ["ubuntu-20.04", "macos-latest"]
|
||||
python: ["2.7", "pypy-2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.9"]
|
||||
include:
|
||||
- os: "windows-latest"
|
||||
python: "3.8"
|
||||
- os: "windows-latest"
|
||||
python: "3.9"
|
||||
- os: "windows-latest"
|
||||
python: "3.10"
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Test: Python ${{ matrix.python }} on ${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
@ -18,7 +29,14 @@ jobs:
|
|||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- run: ./test/test
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools
|
||||
python -m pip install tox tox-gh-actions
|
||||
- name: "Run tests"
|
||||
run: |
|
||||
python -m tox
|
||||
|
||||
fmt:
|
||||
name: Format
|
||||
runs-on: ubuntu-22.04
|
||||
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,4 +1,10 @@
|
|||
*.egg-info
|
||||
*.pyc
|
||||
.coverage*
|
||||
.eggs/
|
||||
.idea/
|
||||
.tox/
|
||||
.venv/
|
||||
build/
|
||||
dist/
|
||||
htmlcov/
|
||||
|
|
|
@ -50,6 +50,31 @@ used in the rest of the project. The version history should be clean, and
|
|||
commit messages should be descriptive and [properly
|
||||
formatted][commit-messages].
|
||||
|
||||
When preparing a patch, it's recommended that you add unit tests
|
||||
that demonstrate the bug is fixed (or that the feature works).
|
||||
You can run the tests on your local machine by installing the `dev` extras.
|
||||
The steps below do this using a virtual environment:
|
||||
|
||||
```shell
|
||||
# Create a local virtual environment
|
||||
$ python -m venv .venv
|
||||
|
||||
# Activate the virtual environment
|
||||
# Cygwin, Linux, and MacOS:
|
||||
$ . .venv/bin/activate
|
||||
# Windows Powershell:
|
||||
$ & .venv\Scripts\Activate.ps1
|
||||
|
||||
# Update pip and setuptools
|
||||
(.venv) $ python -m pip install -U pip setuptools
|
||||
|
||||
# Install dotbot and its development dependencies
|
||||
(.venv) $ python -m pip install -e .[dev]
|
||||
|
||||
# Run the unit tests
|
||||
(.venv) $ tox
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
If you have any questions about anything, feel free to [ask][email]!
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import os, glob
|
||||
import sys
|
||||
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
from .config import ConfigReader, ReadingError
|
||||
from .dispatcher import Dispatcher, DispatchError
|
||||
from .messenger import Messenger
|
||||
from .messenger import Level
|
||||
from .util import module
|
||||
|
||||
import dotbot
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
|
||||
import dotbot
|
||||
|
||||
from .config import ConfigReader, ReadingError
|
||||
from .dispatcher import Dispatcher, DispatchError
|
||||
from .messenger import Level, Messenger
|
||||
from .plugins import Clean, Create, Link, Shell
|
||||
from .util import module
|
||||
|
||||
|
||||
def add_options(parser):
|
||||
|
@ -118,9 +118,10 @@ def main():
|
|||
else:
|
||||
log.use_color(sys.stdout.isatty())
|
||||
|
||||
plugins = []
|
||||
plugin_directories = list(options.plugin_dirs)
|
||||
if not options.disable_built_in_plugins:
|
||||
from .plugins import Clean, Create, Link, Shell
|
||||
plugins.extend([Clean, Create, Link, Shell])
|
||||
plugin_paths = []
|
||||
for directory in plugin_directories:
|
||||
for plugin_path in glob.glob(os.path.join(directory, "*.py")):
|
||||
|
@ -129,7 +130,7 @@ def main():
|
|||
plugin_paths.append(plugin_path)
|
||||
for plugin_path in plugin_paths:
|
||||
abspath = os.path.abspath(plugin_path)
|
||||
module.load(abspath)
|
||||
plugins.extend(module.load(abspath))
|
||||
if not options.config_file:
|
||||
log.error("No configuration file specified")
|
||||
exit(1)
|
||||
|
@ -151,6 +152,7 @@ def main():
|
|||
skip=options.skip,
|
||||
exit_on_failure=options.exit_on_failure,
|
||||
options=options,
|
||||
plugins=plugins,
|
||||
)
|
||||
success = dispatcher.dispatch(tasks)
|
||||
if success:
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import yaml
|
||||
import json
|
||||
import os.path
|
||||
|
||||
import yaml
|
||||
|
||||
from .util import string
|
||||
|
||||
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
import os
|
||||
from argparse import Namespace
|
||||
from .plugin import Plugin
|
||||
from .messenger import Messenger
|
||||
|
||||
from .context import Context
|
||||
from .messenger import Messenger
|
||||
from .plugin import Plugin
|
||||
|
||||
|
||||
class Dispatcher(object):
|
||||
def __init__(
|
||||
self, base_directory, only=None, skip=None, exit_on_failure=False, options=Namespace()
|
||||
self,
|
||||
base_directory,
|
||||
only=None,
|
||||
skip=None,
|
||||
exit_on_failure=False,
|
||||
options=Namespace(),
|
||||
plugins=None,
|
||||
):
|
||||
self._log = Messenger()
|
||||
self._setup_context(base_directory, options)
|
||||
self._load_plugins()
|
||||
plugins = plugins or []
|
||||
self._plugins = [plugin(self._context) for plugin in plugins]
|
||||
self._only = only
|
||||
self._skip = skip
|
||||
self._exit = exit_on_failure
|
||||
|
@ -65,9 +73,6 @@ class Dispatcher(object):
|
|||
return False
|
||||
return success
|
||||
|
||||
def _load_plugins(self):
|
||||
self._plugins = [plugin(self._context) for plugin in Plugin.__subclasses__()]
|
||||
|
||||
|
||||
class DispatchError(Exception):
|
||||
pass
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
from .messenger import Messenger
|
||||
from .level import Level
|
||||
from .messenger import Messenger
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from ..util.singleton import Singleton
|
||||
from ..util.compat import with_metaclass
|
||||
from ..util.singleton import Singleton
|
||||
from .color import Color
|
||||
from .level import Level
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from .messenger import Messenger
|
||||
from .context import Context
|
||||
from .messenger import Messenger
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import os
|
||||
import dotbot
|
||||
import sys
|
||||
|
||||
from ..plugin import Plugin
|
||||
|
||||
|
||||
class Clean(dotbot.Plugin):
|
||||
class Clean(Plugin):
|
||||
"""
|
||||
Cleans broken symbolic links.
|
||||
"""
|
||||
|
@ -42,7 +44,9 @@ class Clean(dotbot.Plugin):
|
|||
self._log.debug("Ignoring nonexistent directory %s" % target)
|
||||
return True
|
||||
for item in os.listdir(os.path.expandvars(os.path.expanduser(target))):
|
||||
path = os.path.join(os.path.expandvars(os.path.expanduser(target)), item)
|
||||
path = os.path.abspath(
|
||||
os.path.join(os.path.expandvars(os.path.expanduser(target)), item)
|
||||
)
|
||||
if recursive and os.path.isdir(path):
|
||||
# isdir implies not islink -- we don't want to descend into
|
||||
# symlinked directories. okay to do a recursive call here
|
||||
|
@ -50,6 +54,8 @@ class Clean(dotbot.Plugin):
|
|||
self._clean(path, force, recursive)
|
||||
if not os.path.exists(path) and os.path.islink(path):
|
||||
points_at = os.path.join(os.path.dirname(path), os.readlink(path))
|
||||
if sys.platform[:5] == "win32" and points_at.startswith("\\\\?\\"):
|
||||
points_at = points_at[4:]
|
||||
if self._in_directory(path, self._context.base_directory()) or force:
|
||||
self._log.lowinfo("Removing invalid link %s -> %s" % (path, points_at))
|
||||
os.remove(path)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import os
|
||||
import dotbot
|
||||
|
||||
from ..plugin import Plugin
|
||||
|
||||
|
||||
class Create(dotbot.Plugin):
|
||||
class Create(Plugin):
|
||||
"""
|
||||
Create empty paths.
|
||||
"""
|
||||
|
@ -21,7 +22,7 @@ class Create(dotbot.Plugin):
|
|||
success = True
|
||||
defaults = self._context.defaults().get("create", {})
|
||||
for key in paths:
|
||||
path = os.path.expandvars(os.path.expanduser(key))
|
||||
path = os.path.abspath(os.path.expandvars(os.path.expanduser(key)))
|
||||
mode = defaults.get("mode", 0o777) # same as the default for os.makedirs
|
||||
if isinstance(paths, dict):
|
||||
options = paths[key]
|
||||
|
@ -48,6 +49,9 @@ class Create(dotbot.Plugin):
|
|||
try:
|
||||
self._log.lowinfo("Creating path %s" % path)
|
||||
os.makedirs(path, mode)
|
||||
# On Windows, the *mode* argument to `os.makedirs()` is ignored.
|
||||
# The mode must be set explicitly in a follow-up call.
|
||||
os.chmod(path, mode)
|
||||
except OSError:
|
||||
self._log.warning("Failed to create path %s" % path)
|
||||
success = False
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import os
|
||||
import sys
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
import dotbot
|
||||
import dotbot.util
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from ..plugin import Plugin
|
||||
from ..util import shell_command
|
||||
|
||||
|
||||
class Link(dotbot.Plugin):
|
||||
class Link(Plugin):
|
||||
"""
|
||||
Symbolically links dotfiles.
|
||||
"""
|
||||
|
@ -58,7 +58,7 @@ class Link(dotbot.Plugin):
|
|||
if test is not None and not self._test_success(test):
|
||||
self._log.lowinfo("Skipping %s" % destination)
|
||||
continue
|
||||
path = os.path.expandvars(os.path.expanduser(path))
|
||||
path = os.path.normpath(os.path.expandvars(os.path.expanduser(path)))
|
||||
if use_glob:
|
||||
glob_results = self._create_glob_results(path, exclude_paths)
|
||||
if len(glob_results) == 0:
|
||||
|
@ -140,7 +140,7 @@ class Link(dotbot.Plugin):
|
|||
return success
|
||||
|
||||
def _test_success(self, command):
|
||||
ret = dotbot.util.shell_command(command, cwd=self._context.base_directory())
|
||||
ret = shell_command(command, cwd=self._context.base_directory())
|
||||
if ret != 0:
|
||||
self._log.debug("Test '%s' returned false" % command)
|
||||
return ret == 0
|
||||
|
@ -166,6 +166,8 @@ class Link(dotbot.Plugin):
|
|||
return []
|
||||
# call glob.glob; only python >= 3.5 supports recursive globs
|
||||
found = glob.glob(path) if (sys.version_info < (3, 5)) else glob.glob(path, recursive=True)
|
||||
# normalize paths to ensure cross-platform compatibility
|
||||
found = [os.path.normpath(p) for p in found]
|
||||
# if using recursive glob (`**`), filter results to return only files:
|
||||
if "**" in path and not path.endswith(str(os.sep)):
|
||||
self._log.debug("Excluding directories from recursive glob: " + str(path))
|
||||
|
@ -197,7 +199,10 @@ class Link(dotbot.Plugin):
|
|||
Returns the destination of the symbolic link.
|
||||
"""
|
||||
path = os.path.expanduser(path)
|
||||
return os.readlink(path)
|
||||
path = os.readlink(path)
|
||||
if sys.platform[:5] == "win32" and path.startswith("\\\\?\\"):
|
||||
path = path[4:]
|
||||
return path
|
||||
|
||||
def _exists(self, path):
|
||||
"""
|
||||
|
@ -223,7 +228,7 @@ class Link(dotbot.Plugin):
|
|||
def _delete(self, source, path, relative, canonical_path, force):
|
||||
success = True
|
||||
source = os.path.join(self._context.base_directory(canonical_path=canonical_path), source)
|
||||
fullpath = os.path.expanduser(path)
|
||||
fullpath = os.path.abspath(os.path.expanduser(path))
|
||||
if relative:
|
||||
source = self._relative_path(source, fullpath)
|
||||
if (self._is_link(path) and self._link_destination(path) != source) or (
|
||||
|
@ -264,9 +269,10 @@ class Link(dotbot.Plugin):
|
|||
Returns true if successfully linked files.
|
||||
"""
|
||||
success = False
|
||||
destination = os.path.expanduser(link_name)
|
||||
destination = os.path.abspath(os.path.expanduser(link_name))
|
||||
base_directory = self._context.base_directory(canonical_path=canonical_path)
|
||||
absolute_source = os.path.join(base_directory, source)
|
||||
link_name = os.path.normpath(link_name)
|
||||
if relative:
|
||||
source = self._relative_path(absolute_source, destination)
|
||||
else:
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import os
|
||||
import subprocess
|
||||
import dotbot
|
||||
import dotbot.util
|
||||
from ..plugin import Plugin
|
||||
from ..util import shell_command
|
||||
|
||||
|
||||
class Shell(dotbot.Plugin):
|
||||
class Shell(Plugin):
|
||||
"""
|
||||
Run arbitrary shell commands.
|
||||
"""
|
||||
|
@ -51,7 +49,7 @@ class Shell(dotbot.Plugin):
|
|||
self._log.lowinfo("%s [%s]" % (msg, cmd))
|
||||
stdout = options.get("stdout", stdout)
|
||||
stderr = options.get("stderr", stderr)
|
||||
ret = dotbot.util.shell_command(
|
||||
ret = shell_command(
|
||||
cmd,
|
||||
cwd=self._context.base_directory(),
|
||||
enable_stdin=stdin,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
import subprocess
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
|
||||
def shell_command(command, cwd=None, enable_stdin=False, enable_stdout=False, enable_stderr=False):
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import sys, os.path
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dotbot.plugin import Plugin
|
||||
|
||||
# We keep references to loaded modules so they don't get garbage collected.
|
||||
loaded_modules = []
|
||||
|
@ -7,8 +10,17 @@ loaded_modules = []
|
|||
def load(path):
|
||||
basename = os.path.basename(path)
|
||||
module_name, extension = os.path.splitext(basename)
|
||||
plugin = load_module(module_name, path)
|
||||
loaded_modules.append(plugin)
|
||||
loaded_module = load_module(module_name, path)
|
||||
plugins = []
|
||||
for name in dir(loaded_module):
|
||||
possible_plugin = getattr(loaded_module, name)
|
||||
try:
|
||||
if issubclass(possible_plugin, Plugin) and possible_plugin is not Plugin:
|
||||
plugins.append(possible_plugin)
|
||||
except TypeError:
|
||||
pass
|
||||
loaded_modules.append(loaded_module)
|
||||
return plugins
|
||||
|
||||
|
||||
if sys.version_info >= (3, 5):
|
||||
|
|
6
setup.py
6
setup.py
|
@ -58,6 +58,12 @@ setup(
|
|||
install_requires=[
|
||||
"PyYAML>=5.3,<6",
|
||||
],
|
||||
extras_require={
|
||||
"dev": {
|
||||
"pytest",
|
||||
"tox",
|
||||
}
|
||||
},
|
||||
# To provide executable scripts, use entry points in preference to the
|
||||
# "scripts" keyword. Entry points provide cross-platform support and allow
|
||||
# pip to create the appropriate form of executable for the target platform.
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
[Vagrantfile]
|
||||
indent_size = 2
|
||||
|
||||
[{test,test_travis}]
|
||||
indent_size = 4
|
2
test/.gitignore
vendored
2
test/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
.vagrant/
|
||||
*.log
|
|
@ -1,86 +0,0 @@
|
|||
Testing
|
||||
=======
|
||||
|
||||
Dotbot testing code uses [Vagrant] to run all tests inside a virtual machine to
|
||||
have tests be completely isolated from the host machine. Specifically, you
|
||||
will need both:
|
||||
|
||||
- [VirtualBox]
|
||||
- [Vagrant]
|
||||
|
||||
Install Dotbot dependencies
|
||||
---------------------------
|
||||
|
||||
Ensure you have updated the `dotbot` submodule dependencies, on the host machine:
|
||||
|
||||
```bash
|
||||
git submodule sync --quiet --recursive
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
Install Vagrant
|
||||
---------------
|
||||
|
||||
### Debian-based distributions
|
||||
|
||||
```bash
|
||||
sudo apt install vagrant virtualbox
|
||||
```
|
||||
|
||||
### macOS
|
||||
|
||||
You can download those directly from the above URLs, or via some MacOS package managers.
|
||||
e.g. using [HomeBrew](https://brew.sh/):
|
||||
|
||||
```bash
|
||||
brew cask install virtualbox
|
||||
brew cask install vagrant
|
||||
# optional, adding menu-bar support:
|
||||
brew cask install vagrant-manager
|
||||
```
|
||||
|
||||
Running the Tests
|
||||
-----------------
|
||||
|
||||
Before running the tests, you must start and `ssh` into the VM:
|
||||
|
||||
```bash
|
||||
vagrant up
|
||||
vagrant ssh
|
||||
```
|
||||
|
||||
All remaining commands are run inside the VM.
|
||||
|
||||
First, you must install a version of Python to test against, using:
|
||||
|
||||
pyenv install -s {version}
|
||||
|
||||
You can choose any version you like, e.g. `3.8.1`. It isn't particularly
|
||||
important to test against all supported versions of Python in the VM, because
|
||||
they will be tested by CI. Once you've installed a specific version of Python,
|
||||
activate it with:
|
||||
|
||||
pyenv global {version}
|
||||
|
||||
The VM mounts your host's Dotbot directory in `/dotbot` as read-only, allowing
|
||||
you to make edits on your host machine. Run the entire test suite by:
|
||||
|
||||
```bash
|
||||
cd /dotbot/test
|
||||
./test
|
||||
```
|
||||
|
||||
Selected tests can be run by passing paths to the tests as arguments, e.g.:
|
||||
|
||||
```bash
|
||||
./test tests/create.bash tests/defaults.bash
|
||||
```
|
||||
|
||||
To debug tests, you can run the test driver with the `--debug` (or `-d` short
|
||||
form) flag, e.g. `./test --debug tests/link-if.bash`. This will enable printing
|
||||
stdout/stderr.
|
||||
|
||||
When finished with testing, it is good to shut down the virtual machine by
|
||||
running `vagrant halt`.
|
||||
|
||||
[VirtualBox]: https://www.virtualbox.org/
|
||||
[Vagrant]: https://www.vagrantup.com/
|
28
test/Vagrantfile
vendored
28
test/Vagrantfile
vendored
|
@ -1,28 +0,0 @@
|
|||
Vagrant.configure(2) do |config|
|
||||
config.vm.box = 'ubuntu/jammy64'
|
||||
|
||||
config.vm.synced_folder "..", "/dotbot", mount_options: ["ro"]
|
||||
|
||||
# disable default synced folder
|
||||
config.vm.synced_folder ".", "/vagrant", disabled: true
|
||||
|
||||
# install packages
|
||||
config.vm.provision "shell", inline: <<-EOS
|
||||
apt-get -y update
|
||||
apt-get install -y git make build-essential libssl-dev zlib1g-dev \
|
||||
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
|
||||
libncurses5-dev
|
||||
EOS
|
||||
|
||||
# install pyenv
|
||||
config.vm.provision "shell", privileged: false, inline: <<-EOS
|
||||
rm -rf ~/.pyenv
|
||||
git clone https://github.com/pyenv/pyenv.git ~/.pyenv
|
||||
cat <<-'PYENV' > ~/.bashrc
|
||||
export PYENV_ROOT="$HOME/.pyenv"
|
||||
export PATH="$PYENV_ROOT/bin:$PATH"
|
||||
eval "$(pyenv init --path)"
|
||||
eval "$(pyenv init -)"
|
||||
PYENV
|
||||
EOS
|
||||
end
|
|
@ -1,99 +0,0 @@
|
|||
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_env() {
|
||||
if [[ "$(whoami)" != "vagrant" && "${CI}" != true ]]; then
|
||||
die "tests must be run inside Vagrant or CI"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -rf ~/fakehome
|
||||
mkdir -p ~/fakehome
|
||||
}
|
||||
|
||||
initialize() {
|
||||
echo "initializing."
|
||||
tests_run=0
|
||||
tests_passed=0
|
||||
tests_failed=0
|
||||
tests_skipped=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))
|
||||
red "-> fail!"
|
||||
echo
|
||||
}
|
||||
|
||||
skip() {
|
||||
tests_skipped=$((tests_skipped + 1))
|
||||
yellow "-> skipped."
|
||||
echo
|
||||
}
|
||||
|
||||
run_test() {
|
||||
tests_run=$((tests_run + 1))
|
||||
printf '[%d/%d] (%s)\n' "${tests_run}" "${tests_total}" "${1}"
|
||||
cleanup
|
||||
if (cd "${BASEDIR}/test/tests" && HOME=~/fakehome DEBUG=${2} DOTBOT_TEST=true bash "${1}"); then
|
||||
pass
|
||||
elif [ $? -eq 42 ]; then
|
||||
skip
|
||||
else
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
report() {
|
||||
printf -- "test report\n"
|
||||
printf -- "-----------\n"
|
||||
printf -- "- %3d run\n" ${tests_run}
|
||||
printf -- "- %3d passed\n" ${tests_passed}
|
||||
printf -- "- %3d skipped\n" ${tests_skipped}
|
||||
printf -- "- %3d failed\n" ${tests_failed}
|
||||
if [ ${tests_failed} -gt 0 ]; then
|
||||
red "==> FAIL! "
|
||||
return 1
|
||||
else
|
||||
green "==> PASS. "
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
die() {
|
||||
>&2 echo $@
|
||||
>&2 echo "terminating..."
|
||||
exit 1
|
||||
}
|
53
test/test
53
test/test
|
@ -1,53 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
export BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${BASEDIR}/test"
|
||||
. "./driver-lib.bash"
|
||||
|
||||
date_stamp="$(date --rfc-3339=ns)"
|
||||
start="$(date +%s)"
|
||||
|
||||
check_env
|
||||
|
||||
# parse flags; must come before positional arguments
|
||||
POSITIONAL=()
|
||||
DEBUG=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-d|--debug)
|
||||
DEBUG=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
POSITIONAL+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}" # restore positional arguments
|
||||
|
||||
declare -a tests=()
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
while read file; do
|
||||
tests+=("${file}")
|
||||
done < <(find tests -type f -name '*.bash' | sort)
|
||||
else
|
||||
tests=("$@")
|
||||
fi
|
||||
|
||||
initialize "${#tests[@]}"
|
||||
|
||||
for file in "${tests[@]}"; do
|
||||
run_test "$(basename "${file}")" "${DEBUG}"
|
||||
done
|
||||
|
||||
if report; then
|
||||
ret=0
|
||||
else
|
||||
ret=1
|
||||
fi
|
||||
|
||||
echo "(tests run in $(($(date +%s) - start)) seconds)"
|
||||
exit ${ret}
|
|
@ -1,76 +0,0 @@
|
|||
DOTBOT_EXEC="${BASEDIR}/bin/dotbot"
|
||||
DOTFILES="${HOME}/dotfiles"
|
||||
INSTALL_CONF='install.conf.yaml'
|
||||
INSTALL_CONF_JSON='install.conf.json'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
skip_tests() {
|
||||
# exit with special exit code picked up by driver-lib.bash
|
||||
exit 42
|
||||
}
|
||||
|
||||
check_env() {
|
||||
if [ "${DOTBOT_TEST}" != "true" ]; then
|
||||
>&2 echo "test must be run by test driver"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# run comparison check on python version; args:
|
||||
# $1 - comparison operator (e.g. '>=')
|
||||
# $2 - version number, to be passed to python (e.g. '3', '3.5', '3.6.4')
|
||||
# status code will reflect if comparison is true/false
|
||||
# e.g. `check_python_version '>=' 3.5`
|
||||
check_python_version() {
|
||||
check="$1"
|
||||
version="$(echo "$2" | tr . , )"
|
||||
# this call to just `python` will work in the Vagrant-based testing VM
|
||||
# because `pyenv` will always create a link to the "right" version.
|
||||
python -c "import sys; exit( not (sys.version_info ${check} (${version})) )"
|
||||
}
|
||||
|
||||
initialize() {
|
||||
check_env
|
||||
echo "${test_description}"
|
||||
mkdir -p "${DOTFILES}"
|
||||
cd
|
||||
}
|
||||
|
||||
run_dotbot() {
|
||||
(
|
||||
cat > "${DOTFILES}/${INSTALL_CONF}"
|
||||
${DOTBOT_EXEC} -c "${DOTFILES}/${INSTALL_CONF}" "${@}"
|
||||
)
|
||||
}
|
||||
|
||||
run_dotbot_json() {
|
||||
(
|
||||
cat > "${DOTFILES}/${INSTALL_CONF_JSON}"
|
||||
${DOTBOT_EXEC} -c "${DOTFILES}/${INSTALL_CONF_JSON}" "${@}"
|
||||
)
|
||||
}
|
||||
|
||||
initialize
|
|
@ -1,19 +0,0 @@
|
|||
test_description='clean uses default unless overridden'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
ln -s /nowhere ~/.g
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
- clean:
|
||||
~/nonexistent:
|
||||
force: true
|
||||
~/:
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
test -h ~/.g
|
||||
'
|
|
@ -1,16 +0,0 @@
|
|||
test_description='clean expands environment variables'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
ln -s ${DOTFILES}/f ~/.f
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
- clean: ["\$HOME"]
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
! test -h ~/.f
|
||||
'
|
|
@ -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 <<EOF
|
||||
- clean: ["~"]
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
test -f ~/.f &&
|
||||
! test -h ~/.g
|
||||
'
|
|
@ -1,8 +0,0 @@
|
|||
test_description='clean ignores nonexistent directories'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
- clean: ["~", "~/fake"]
|
||||
EOF
|
||||
'
|
|
@ -1,18 +0,0 @@
|
|||
test_description='clean forced to remove files linking outside dotfiles directory'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
ln -s /nowhere ~/.g
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
- clean:
|
||||
~/:
|
||||
force: true
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
! test -h ~/.g
|
||||
'
|
|
@ -1,18 +0,0 @@
|
|||
test_description='clean ignores files linking outside dotfiles directory'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
ln -s ${DOTFILES}/f ~/.f &&
|
||||
ln -s ~/g ~/.g
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
- clean: ["~"]
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
! test -h ~/.f &&
|
||||
test -h ~/.g
|
||||
'
|
|
@ -1,34 +0,0 @@
|
|||
test_description='clean removes recursively'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
mkdir -p ~/a/b
|
||||
ln -s /nowhere ~/c
|
||||
ln -s /nowhere ~/a/d
|
||||
ln -s /nowhere ~/a/b/e
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
- clean:
|
||||
~/:
|
||||
force: true
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
! test -h ~/c && test -h ~/a/d && test -h ~/a/b/e
|
||||
'
|
||||
|
||||
test_expect_success 'run 2' '
|
||||
run_dotbot <<EOF
|
||||
- clean:
|
||||
~/:
|
||||
force: true
|
||||
recursive: true
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test 2' '
|
||||
! test -h ~/a/d && ! test -h ~/a/b/e
|
||||
'
|
|
@ -1,8 +0,0 @@
|
|||
test_description='blank config allowed'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
[]
|
||||
EOF
|
||||
'
|
|
@ -1,7 +0,0 @@
|
|||
test_description='empty config allowed'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
EOF
|
||||
'
|
|
@ -1,20 +0,0 @@
|
|||
test_description='json config with tabs allowed'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "grape" > ${DOTFILES}/h
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot_json <<EOF
|
||||
[{
|
||||
"link": {
|
||||
"~/.i": "h"
|
||||
}
|
||||
}]
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "grape" ~/.i
|
||||
'
|
|
@ -1,20 +0,0 @@
|
|||
test_description='json config allowed'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "grape" > ${DOTFILES}/h
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot_json <<EOF
|
||||
[{
|
||||
"link": {
|
||||
"~/.i": "h"
|
||||
}
|
||||
}]
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "grape" ~/.i
|
||||
'
|
|
@ -1,26 +0,0 @@
|
|||
test_description='create mode'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot -v <<EOF
|
||||
- defaults:
|
||||
create:
|
||||
mode: 0755
|
||||
- create:
|
||||
- ~/downloads
|
||||
- ~/.vim/undo-history
|
||||
- create:
|
||||
~/.ssh:
|
||||
mode: 0700
|
||||
~/projects:
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
[ -d ~/downloads ] &&
|
||||
[ -d ~/.vim/undo-history ] &&
|
||||
[ -d ~/.ssh ] &&
|
||||
[ -d ~/projects ] &&
|
||||
[ "$(stat -c %a ~/.ssh)" = "700" ] &&
|
||||
[ "$(stat -c %a ~/downloads)" = "755" ]
|
||||
'
|
|
@ -1,23 +0,0 @@
|
|||
test_description='create folders'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
- create:
|
||||
- ~/somedir
|
||||
- ~/nested/somedir
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
[ -d ~/somedir ] &&
|
||||
[ -d ~/nested/somedir ]
|
||||
'
|
||||
|
||||
test_expect_success 'run 2' '
|
||||
run_dotbot <<EOF
|
||||
- create:
|
||||
- ~/somedir
|
||||
- ~/nested/somedir
|
||||
EOF
|
||||
'
|
|
@ -1,59 +0,0 @@
|
|||
test_description='defaults setting works'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "apple" > ${DOTFILES}/f &&
|
||||
echo "grape" > ~/f &&
|
||||
ln -s ~/f ~/.f &&
|
||||
ln -s /nowhere ~/.g
|
||||
'
|
||||
|
||||
test_expect_failure 'run-fail' '
|
||||
run_dotbot <<EOF
|
||||
- link:
|
||||
~/.f: f
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_failure 'test-fail' '
|
||||
grep "apple" ~/.f
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
- defaults:
|
||||
link:
|
||||
relink: true
|
||||
|
||||
- link:
|
||||
~/.f: f
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "apple" ~/.f
|
||||
'
|
||||
|
||||
test_expect_success 'run-fail 2' '
|
||||
run_dotbot <<EOF
|
||||
- clean: ["~"]
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_failure 'test-fail 2' '
|
||||
! test -h ~/.g
|
||||
'
|
||||
|
||||
test_expect_success 'run 2' '
|
||||
run_dotbot <<EOF
|
||||
- defaults:
|
||||
clean:
|
||||
force: true
|
||||
|
||||
- clean: ["~"]
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test 2' '
|
||||
! test -h ~/.g
|
||||
'
|
|
@ -1,21 +0,0 @@
|
|||
test_description='--except with multiple arguments'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
ln -s ${DOTFILES}/nonexistent ~/bad && touch ${DOTFILES}/y
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot --except clean shell <<EOF
|
||||
- clean: ["~"]
|
||||
- shell:
|
||||
- echo "x" > ~/x
|
||||
- link:
|
||||
~/y: y
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
[ "$(readlink ~/bad | cut -d/ -f5-)" = "dotfiles/nonexistent" ] &&
|
||||
! test -f ~/x && test -f ~/y
|
||||
'
|
|
@ -1,32 +0,0 @@
|
|||
test_description='--except'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "apple" > ${DOTFILES}/x
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot --except link <<EOF
|
||||
- shell:
|
||||
- echo "pear" > ~/y
|
||||
- link:
|
||||
~/x: x
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "pear" ~/y && ! test -f ~/x
|
||||
'
|
||||
|
||||
test_expect_success 'run 2' '
|
||||
run_dotbot --except shell <<EOF
|
||||
- shell:
|
||||
- echo "pear" > ~/z
|
||||
- link:
|
||||
~/x: x
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "apple" ~/x && ! test -f ~/z
|
||||
'
|
|
@ -1,32 +0,0 @@
|
|||
test_description='test exit on failure'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "apple" > ${DOTFILES}/f1 &&
|
||||
echo "orange" > ${DOTFILES}/f2 &&
|
||||
echo "pineapple" > ${DOTFILES}/f3
|
||||
'
|
||||
|
||||
test_expect_failure 'run_case1' '
|
||||
run_dotbot -x <<EOF
|
||||
- shell:
|
||||
- "this_is_not_a_command"
|
||||
- link:
|
||||
~/f1:
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_failure 'run_case2' '
|
||||
run_dotbot -x <<EOF
|
||||
- link:
|
||||
~/f2:
|
||||
- shell:
|
||||
- "this_is_not_a_command"
|
||||
- link:
|
||||
~/f3:
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
[[ ! -f ~/f1 ]] && [[ -f ~/f2 ]] && [[ ! -f ~/f3 ]]
|
||||
'
|
|
@ -1,60 +0,0 @@
|
|||
test_description='can find python executable with different names'
|
||||
. '../test-lib.bash'
|
||||
|
||||
# the test machine needs to have a binary named `python`
|
||||
test_expect_success 'setup' '
|
||||
mkdir ~/tmp_bin &&
|
||||
(
|
||||
IFS=:
|
||||
for p in $PATH; do
|
||||
if [ -d $p ]; then
|
||||
find $p -maxdepth 1 -mindepth 1 -exec sh -c \
|
||||
'"'"'ln -sf {} $HOME/tmp_bin/$(basename {})'"'"' \;
|
||||
fi
|
||||
done
|
||||
) &&
|
||||
rm -f ~/tmp_bin/python &&
|
||||
rm -f ~/tmp_bin/python2 &&
|
||||
rm -f ~/tmp_bin/python3
|
||||
'
|
||||
|
||||
test_expect_failure 'run' '
|
||||
PATH="$HOME/tmp_bin" run_dotbot <<EOF
|
||||
[]
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'setup 2' '
|
||||
touch ~/tmp_bin/python &&
|
||||
chmod +x ~/tmp_bin/python &&
|
||||
cat >> ~/tmp_bin/python <<EOF
|
||||
#!$HOME/tmp_bin/bash
|
||||
exec $(command -v python)
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'run 2' '
|
||||
PATH="$HOME/tmp_bin" run_dotbot <<EOF
|
||||
[]
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'setup 3' '
|
||||
mv ~/tmp_bin/python ~/tmp_bin/python2
|
||||
'
|
||||
|
||||
test_expect_success 'run 3' '
|
||||
PATH="$HOME/tmp_bin" run_dotbot <<EOF
|
||||
[]
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'setup 4' '
|
||||
mv ~/tmp_bin/python2 ~/tmp_bin/python3
|
||||
'
|
||||
|
||||
test_expect_success 'run 4' '
|
||||
PATH="$HOME/tmp_bin" run_dotbot <<EOF
|
||||
[]
|
||||
EOF
|
||||
'
|
|
@ -1,20 +0,0 @@
|
|||
test_description='linking canonicalizes path by default'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "apple" > ${DOTFILES}/f &&
|
||||
ln -s dotfiles dotfiles-symlink
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
cat > "${DOTFILES}/${INSTALL_CONF}" <<EOF
|
||||
- link:
|
||||
~/.f:
|
||||
path: f
|
||||
EOF
|
||||
${DOTBOT_EXEC} -c dotfiles-symlink/${INSTALL_CONF}
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
[ "$(readlink ~/.f | cut -d/ -f5-)" = "dotfiles/f" ]
|
||||
'
|
|
@ -1,26 +0,0 @@
|
|||
test_description='link uses destination if source is null'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "apple" > ${DOTFILES}/f &&
|
||||
echo "grape" > ${DOTFILES}/fd
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
- link:
|
||||
~/f:
|
||||
~/.f:
|
||||
~/fd:
|
||||
force: false
|
||||
~/.fd:
|
||||
force: false
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "apple" ~/f &&
|
||||
grep "apple" ~/.f &&
|
||||
grep "grape" ~/fd &&
|
||||
grep "grape" ~/.fd
|
||||
'
|
|
@ -1,17 +0,0 @@
|
|||
test_description='link expands user in target'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "apple" > ~/f
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot <<EOF
|
||||
- link:
|
||||
~/g: ~/f
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "apple" ~/g
|
||||
'
|
|
@ -1,20 +0,0 @@
|
|||
test_description='link expands environment variables in extended config syntax'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "grape" > ${DOTFILES}/h
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
export APPLE="h" &&
|
||||
run_dotbot <<EOF
|
||||
- link:
|
||||
~/.i:
|
||||
path: \$APPLE
|
||||
relink: true
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "grape" ~/.i
|
||||
'
|
|
@ -1,18 +0,0 @@
|
|||
test_description='link expands environment variables in source'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "grape" > ${DOTFILES}/h
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
export APPLE |