diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 392a24d..b98bc2c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Introduction -Thank you for considering contributing to **yadm**. I develop this project in my -limited spare time, so help is very appreciated. +Thank you for considering contributing to **yadm**. We develop this project in +our limited spare time, so help is very appreciated. All contributors must follow our [Code of Conduct][conduct]. Please make sure you are welcoming and friendly during your interactions, and report any @@ -17,7 +17,8 @@ To contribute, you can: * Star the yadm repo, the star count helps others discover yadm. * Report [bugs](#reporting-a-bug) * Request [features/enhancements](#suggesting-a-feature-or-enhancement) -* Contribute changes to [code, tests](#contributing-code), and [documentation](#improving-documentation) +* Contribute changes to [code, tests](#contributing-code), and + [documentation](#improving-documentation) * Maintain installation [packages](#maintaining-packages) * Help other users by [answering support questions](#answering-support-questions) @@ -70,14 +71,14 @@ Consider trying to reproduce the bug inside a docker container using the [yadm/testbed][] docker image. Doing so will greatly increase the likelihood of the problem being fixed. -The easiest way to start this container, is to clone the [TheLocehiliosan/yadm +The easiest way to start this container, is to clone the [yadm repo][yadm-repo], and use the `scripthost` make target. _(You will need `make` and `docker` installed.)_ For example: ```text -$ git clone https://github.com/TheLocehiliosan/yadm.git +$ git clone https://github.com/yadm-dev/yadm.git $ cd yadm $ make scripthost version=1.12.0 Starting scripthost version="1.12.0" (recording script) @@ -203,7 +204,7 @@ these principles when making changes. 3. Add the official repository (`upstream`) as a remote repository. ```text - $ git remote add upstream https://github.com/TheLocehiliosan/yadm.git + $ git remote add upstream https://github.com/yadm-dev/yadm.git ``` 4. Verify you can run the test harness. _(This will require dependencies: @@ -212,6 +213,11 @@ these principles when making changes. ```text $ make test ``` + If you don't use `docker` but an OCI engine akin to `podman`, you can set it through the `OCI` switch for every target + + ```text + $ make test OCI=podman + ``` 5. Create a feature branch, based off the `develop` branch. @@ -356,25 +362,24 @@ see if you can help. [attach-help]: https://help.github.com/en/articles/file-attachments-on-issues-and-pull-requests [commit-style]: https://chris.beams.io/posts/git-commit/#seven-rules [conduct]: CODE_OF_CONDUCT.md -[contrib-hooks]: https://github.com/TheLocehiliosan/yadm/tree/master/contrib/hooks +[contrib-hooks]: https://github.com/yadm-dev/yadm/tree/master/contrib/hooks [flake8]: https://pypi.org/project/flake8/ [groff-man]: https://www.gnu.org/software/groff/manual/html_node/man.html -[hooks-help]: https://github.com/TheLocehiliosan/yadm/blob/master/yadm.md#hooks +[hooks-help]: https://github.com/yadm-dev/yadm/blob/master/yadm.md#hooks [html-proofer]: https://github.com/gjtorikian/html-proofer [jekyll]: https://jekyllrb.com -[new-bug]: https://github.com/TheLocehiliosan/yadm/issues/new?template=BUG_REPORT.md -[new-feature]: https://github.com/TheLocehiliosan/yadm/issues/new?template=FEATURE_REQUEST.md -[open-issues]: https://github.com/TheLocehiliosan/yadm/issues +[new-bug]: https://github.com/yadm-dev/yadm/issues/new?template=BUG_REPORT.md +[new-feature]: https://github.com/yadm-dev/yadm/issues/new?template=FEATURE_REQUEST.md +[open-issues]: https://github.com/yadm-dev/yadm/issues [pr-help]: https://help.github.com/en/articles/creating-a-pull-request-from-a-fork [pylint]: https://pylint.org/ [pytest]: https://pytest.org/ -[questions]: https://github.com/TheLocehiliosan/yadm/labels/question -[refactor]: https://github.com/TheLocehiliosan/yadm/issues/146 +[questions]: https://github.com/yadm-dev/yadm/labels/question [shellcheck]: https://www.shellcheck.net [signing-commits]: https://help.github.com/en/articles/signing-commits [tpope-style]: https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html -[yadm-man]: https://github.com/TheLocehiliosan/yadm/blob/master/yadm.md -[yadm-repo]: https://github.com/TheLocehiliosan/yadm +[yadm-man]: https://github.com/yadm-dev/yadm/blob/master/yadm.md +[yadm-repo]: https://github.com/yadm-dev/yadm [yadm/jekyll]: https://hub.docker.com/r/yadm/jekyll [yadm/testbed]: https://hub.docker.com/r/yadm/testbed [yamllint]: https://github.com/adrienverge/yamllint diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 29dc730..ecfa5a2 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,6 @@ ### Is your feature request related to a problem? Please describe. diff --git a/.github/ISSUE_TEMPLATE/OTHER.md b/.github/ISSUE_TEMPLATE/OTHER.md index 936a4a4..43fa999 100644 --- a/.github/ISSUE_TEMPLATE/OTHER.md +++ b/.github/ISSUE_TEMPLATE/OTHER.md @@ -8,7 +8,7 @@ assignees: '' --- ### This issue is about diff --git a/.github/ISSUE_TEMPLATE/SUPPORT.md b/.github/ISSUE_TEMPLATE/SUPPORT.md index 22bd849..92dc298 100644 --- a/.github/ISSUE_TEMPLATE/SUPPORT.md +++ b/.github/ISSUE_TEMPLATE/SUPPORT.md @@ -8,11 +8,11 @@ assignees: '' --- ### This question is about diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d2f12b9..80061e4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -29,6 +29,6 @@ Be sure to preface the issue/PR numbers with a "#". Please review [yadm's Contributing Guide][3] for best practices. -[1]: https://github.com/TheLocehiliosan/yadm/blob/master/.github/CONTRIBUTING.md#test-conventions +[1]: https://github.com/yadm-dev/yadm/blob/master/.github/CONTRIBUTING.md#test-conventions [2]: https://help.github.com/en/articles/signing-commits -[3]: https://github.com/TheLocehiliosan/yadm/blob/master/.github/CONTRIBUTING.md +[3]: https://github.com/yadm-dev/yadm/blob/master/.github/CONTRIBUTING.md diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml index 6f1e267..29befa5 100644 --- a/.github/workflows/schedule.yml +++ b/.github/workflows/schedule.yml @@ -14,7 +14,7 @@ jobs: docker create -t --name yadm-website --entrypoint test/validate - yadm/jekyll:2019-10-17; + yadm/jekyll:2024-10-31; docker cp ./ yadm-website:/srv/jekyll - name: Test Site run: docker start yadm-website -a diff --git a/CHANGES b/CHANGES index baa12e5..b06d17a 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,17 @@ +3.3.0 + * Support nested ifs in default template (#436) + * Support include and ifs in default template includes (#406) + * Support environment variables in ifs in default template (#488) + * Support != in default template (#358, #477) + * Fix multiple classes in default template on macOS (#437) + +3.2.2 + * Support spaces in distro/distro-family (#432) + * Fix zsh hanging when tab completing add/checkout (#417) + * Add yadm-untracked script to contributed files (#418) + * Fix documentation typos (#425) + * Support docker-like OCI engines for dev testing (#431) + 3.2.1 * Fix Bash 3 bad array subscript bug (#411) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 2fc77ea..dc23b90 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -12,8 +12,12 @@ Espen Henriksen Cameron Eagans Klas Mellbourn James Clark +Glenn Waters +Nicolas signed-log FORMICHELLA Tomas Cernaj +Joshua Cold jonasc +Nicolas stig124 FORMICHELLA Chad Wade Day, Jr Sébastien Gross David Mandelberg @@ -21,28 +25,30 @@ Paulo Köch Oren Zipori Daniel Gray Paraplegic Racehorse -japm48 Siôn Le Roux Mateusz Piotrowski +japm48 Uroš Golja Satoshi Ohki -Nicolas stig124 FORMICHELLA Jonas Franciszek Madej Daniel Wagenknecht Stig Palmquist Patrick Hof -con-f-use Samisafool +LFdev +con-f-use Bram Ceulemans Travis A. Everett Sheng Yang Jared Smartt Adam Jimerson -dessert1 -addshore Tim Condit Thomas Luzat Russ Allbery +Patrick Roddy +dessert1 Brayden Banks Alexandre GV +addshore +Felipe S. S. Schneider diff --git a/Makefile b/Makefile index b8f5cca..5da0918 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ PYTESTS = $(wildcard test/test_*.py) -IMAGE = yadm/testbed:2022-01-07 +IMAGE = docker.io/yadm/testbed:2023-07-12 +OCI = docker .PHONY: all all: @@ -94,7 +95,7 @@ test: py.test -v $(testargs); \ else \ $(MAKE) -s require-docker && \ - docker run \ + $(OCI) run \ --rm -t$(shell test -t 0 && echo i) \ -v "$(CURDIR):/yadm:ro" \ $(IMAGE) \ @@ -117,7 +118,7 @@ test: .PHONY: testhost testhost: require-docker .testyadm @echo "Starting testhost" - @docker run \ + @$(OCI) run \ -w /root \ --hostname testhost \ --rm -it \ @@ -129,7 +130,7 @@ testhost: require-docker .testyadm scripthost: require-docker .testyadm @echo "Starting scripthost \(recording script\)" @printf '' > script.gz - @docker run \ + @$(OCI) run \ -w /root \ --hostname scripthost \ --rm -it \ @@ -159,7 +160,7 @@ testenv: .PHONY: image image: - @docker build -f test/Dockerfile . -t "$(IMAGE)" + @$(OCI) build -f test/Dockerfile . -t "$(IMAGE)" .PHONY: man @@ -175,7 +176,7 @@ man-ps: @groff -man -Tps ./yadm.1 > yadm.ps yadm.md: yadm.1 - @groff -man -Tascii ./yadm.1 | col -bx | sed 's/^[A-Z]/## &/g' | sed '/yadm(1)/d' > yadm.md + @groff -man -Tutf8 -Z ./yadm.1 | grotty -c | col -bx | sed 's/^[A-Z]/## &/g' | sed '/yadm(1)/d' > yadm.md .PHONY: contrib contrib: SHELL = /bin/bash @@ -204,11 +205,11 @@ install: .PHONY: sync-clock sync-clock: - docker run --rm --privileged alpine hwclock -s + $(OCI) run --rm --privileged alpine hwclock -s .PHONY: require-docker require-docker: - @if ! command -v "docker" > /dev/null 2>&1; then \ - echo "Sorry, this make target requires docker to be installed."; \ + @if ! command -v $(OCI) > /dev/null 2>&1; then \ + echo "Sorry, this make target requires docker to be installed, to use another docker-compatible engine, like podman, re-run the make command adding OCI=podman"; \ false; \ fi diff --git a/README.md b/README.md index 0c78766..41780d3 100644 --- a/README.md +++ b/README.md @@ -56,32 +56,31 @@ The star count helps others discover yadm. [Git]: https://git-scm.com/ [GnuPG]: https://gnupg.org/ [OpenSSL]: https://www.openssl.org/ -[arch-badge]: https://img.shields.io/archlinux/v/community/any/yadm -[arch-link]: https://archlinux.org/packages/community/any/yadm/ -[dev-pages-badge]: https://img.shields.io/github/workflow/status/TheLocehiliosan/yadm/Test%20Site/dev-pages?label=dev-pages -[develop-badge]: https://img.shields.io/github/workflow/status/TheLocehiliosan/yadm/Tests/develop?label=develop -[develop-commits]: https://github.com/TheLocehiliosan/yadm/commits/develop -[develop-date]: https://img.shields.io/github/last-commit/TheLocehiliosan/yadm/develop.svg?label=develop +[arch-badge]: https://img.shields.io/archlinux/v/extra/any/yadm +[arch-link]: https://archlinux.org/packages/extra/any/yadm/ +[dev-pages-badge]: https://img.shields.io/github/actions/workflow/status/yadm-dev/yadm/test.yml?branch=dev-pages +[develop-badge]: https://img.shields.io/github/actions/workflow/status/yadm-dev/yadm/test.yml?branch=develop +[develop-commits]: https://github.com/yadm-dev/yadm/commits/develop +[develop-date]: https://img.shields.io/github/last-commit/yadm-dev/yadm/develop.svg?label=develop [dotfiles]: https://en.wikipedia.org/wiki/Hidden_file_and_hidden_directory -[gh-pages-badge]: https://img.shields.io/github/workflow/status/TheLocehiliosan/yadm/Test%20Site/gh-pages?label=gh-pages +[gh-pages-badge]: https://img.shields.io/github/actions/workflow/status/yadm-dev/yadm/test.yml?branch=gh-pages [git-crypt]: https://github.com/AGWA/git-crypt [homebrew-badge]: https://img.shields.io/homebrew/v/yadm.svg [homebrew-link]: https://formulae.brew.sh/formula/yadm -[license-badge]: https://img.shields.io/github/license/TheLocehiliosan/yadm.svg -[license-link]: https://github.com/TheLocehiliosan/yadm/blob/master/LICENSE -[master-badge]: https://img.shields.io/github/workflow/status/TheLocehiliosan/yadm/Tests/master?label=master -[master-commits]: https://github.com/TheLocehiliosan/yadm/commits/master -[master-date]: https://img.shields.io/github/last-commit/TheLocehiliosan/yadm/master.svg?label=master -[obs-badge]: https://img.shields.io/badge/OBS-v3.2.1-blue +[license-badge]: https://img.shields.io/github/license/yadm-dev/yadm.svg +[license-link]: https://github.com/yadm-dev/yadm/blob/master/LICENSE +[master-badge]: https://img.shields.io/github/actions/workflow/status/yadm-dev/yadm/test.yml?branch=master +[master-commits]: https://github.com/yadm-dev/yadm/commits/master +[master-date]: https://img.shields.io/github/last-commit/yadm-dev/yadm/master.svg?label=master +[obs-badge]: https://img.shields.io/badge/OBS-v3.3.0-blue [obs-link]: https://software.opensuse.org//download.html?project=home%3ATheLocehiliosan%3Ayadm&package=yadm -[releases-badge]: https://img.shields.io/github/tag/TheLocehiliosan/yadm.svg?label=latest+release -[releases-link]: https://github.com/TheLocehiliosan/yadm/releases +[releases-badge]: https://img.shields.io/github/tag/yadm-dev/yadm.svg?label=latest+release +[releases-link]: https://github.com/yadm-dev/yadm/releases [transcrypt]: https://github.com/elasticdog/transcrypt -[travis-ci]: https://travis-ci.com/TheLocehiliosan/yadm/branches -[website-commits]: https://github.com/TheLocehiliosan/yadm/commits/gh-pages -[website-date]: https://img.shields.io/github/last-commit/TheLocehiliosan/yadm/gh-pages.svg?label=website +[website-commits]: https://github.com/yadm-dev/yadm/commits/gh-pages +[website-date]: https://img.shields.io/github/last-commit/yadm-dev/yadm/gh-pages.svg?label=website [website-link]: https://yadm.io/ -[workflow-dev-pages]: https://github.com/thelocehiliosan/yadm/actions?query=workflow%3a%22test+site%22+branch%3adev-pages -[workflow-develop]: https://github.com/TheLocehiliosan/yadm/actions?query=workflow%3ATests+branch%3Adevelop -[workflow-gh-pages]: https://github.com/thelocehiliosan/yadm/actions?query=workflow%3a%22test+site%22+branch%3agh-pages -[workflow-master]: https://github.com/TheLocehiliosan/yadm/actions?query=workflow%3ATests+branch%3Amaster +[workflow-dev-pages]: https://github.com/yadm-dev/yadm/actions?query=workflow%3a%22test+site%22+branch%3adev-pages +[workflow-develop]: https://github.com/yadm-dev/yadm/actions?query=workflow%3ATests+branch%3Adevelop +[workflow-gh-pages]: https://github.com/yadm-dev/yadm/actions?query=workflow%3a%22test+site%22+branch%3agh-pages +[workflow-master]: https://github.com/yadm-dev/yadm/actions?query=workflow%3ATests+branch%3Amaster diff --git a/bootstrap b/bootstrap index ab62aa3..70a9687 100755 --- a/bootstrap +++ b/bootstrap @@ -29,7 +29,7 @@ # source <(curl -L bootstrap.yadm.io) # -YADM_REPO="https://github.com/TheLocehiliosan/yadm" +YADM_REPO="https://github.com/yadm-dev/yadm" YADM_RELEASE=${release:-master} REPO_URL="" diff --git a/completion/README.md b/completion/README.md index 1edd861..d84253a 100644 --- a/completion/README.md +++ b/completion/README.md @@ -39,7 +39,7 @@ Load `_yadm` as a plugin in your `.zshrc`: ```zsh fpath=("$ZPLUG_HOME/bin" $fpath) -zplug "TheLocehiliosan/yadm", use:"completion/zsh/_yadm", as:command, defer:2 +zplug "yadm-dev/yadm", use:"completion/zsh/_yadm", as:command, defer:2 ``` ## Fish (manual installation) diff --git a/contrib/hooks/README.md b/contrib/hooks/README.md index 551f6f0..c475ae7 100644 --- a/contrib/hooks/README.md +++ b/contrib/hooks/README.md @@ -11,4 +11,4 @@ this is a place to share it. I recommend *careful review* of any code from here before using it. No guarantees of code quality is assumed. -[hooks-help]: https://github.com/TheLocehiliosan/yadm/blob/master/yadm.md#hooks +[hooks-help]: https://github.com/yadm-dev/yadm/blob/master/yadm.md#hooks diff --git a/pylintrc b/pylintrc deleted file mode 100644 index ba41b74..0000000 --- a/pylintrc +++ /dev/null @@ -1,17 +0,0 @@ -[BASIC] -good-names=pytestmark - -[DESIGN] -max-args=14 -max-locals=28 -max-attributes=8 -max-statements=65 - -[SIMILARITIES] -min-similarity-lines=8 - -[MESSAGES CONTROL] -disable=redefined-outer-name - -[TYPECHECK] -ignored-modules=py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1e51d44 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[tool.pytest.ini_options] +cache_dir = "/tmp" +addopts = "-ra" +markers = [ + "deprecated", # marks tests for deprecated features (deselect with '-m "not deprecated"') +] + +[tool.pylint.design] +max-args = 14 +max-locals = 28 +max-attributes = 8 +max-statements = 65 + +[tool.pylint.format] +max-line-length = 120 + +[tool.pylint."messages control"] +disable = [ + "redefined-outer-name", +] + +[tool.pylint.similarities] +ignore-imports = "yes" +min-similarity-lines = 8 + +[tool.black] +line-length = 120 + +[tool.isort] +line_length = 120 +profile = "black" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index d032ea5..0000000 --- a/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -cache_dir = /tmp -addopts = -ra -markers = - deprecated: marks tests for deprecated features (deselect with '-m "not deprecated"') diff --git a/test/Dockerfile b/test/Dockerfile index e6a0a97..29d863e 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -1,9 +1,9 @@ -FROM ubuntu:18.04 +FROM ubuntu:23.04 MAINTAINER Tim Byrne # Shellcheck and esh versions -ARG SC_VER=0.8.0 -ARG ESH_VER=0.3.1 +ARG SC_VER=0.9.0 +ARG ESH_VER=0.3.2 # Install prerequisites and configure UTF-8 locale RUN \ @@ -41,8 +41,8 @@ RUN cd /opt \ # Upgrade pip3 and install requirements COPY test/requirements.txt /tmp/requirements.txt -RUN python3 -m pip install --upgrade pip setuptools \ - && python3 -m pip install --upgrade -r /tmp/requirements.txt \ +RUN python3 -m pip install --break-system-packages --upgrade pip setuptools \ + && python3 -m pip install --break-system-packages --upgrade -r /tmp/requirements.txt \ && rm -f /tmp/requirements # Install esh @@ -57,8 +57,8 @@ RUN mkdir /yadm \ && echo "\t@false" >> /yadm/Makefile # Include released versions of yadm to test upgrades -ADD https://raw.githubusercontent.com/TheLocehiliosan/yadm/1.12.0/yadm /usr/local/bin/yadm-1.12.0 -ADD https://raw.githubusercontent.com/TheLocehiliosan/yadm/2.5.0/yadm /usr/local/bin/yadm-2.5.0 +ADD https://raw.githubusercontent.com/yadm-dev/yadm/1.12.0/yadm /usr/local/bin/yadm-1.12.0 +ADD https://raw.githubusercontent.com/yadm-dev/yadm/2.5.0/yadm /usr/local/bin/yadm-2.5.0 RUN chmod +x /usr/local/bin/yadm-* # Configure git to make it easier to test yadm manually diff --git a/test/conftest.py b/test/conftest.py index d3bbb76..b18cae4 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -3,11 +3,12 @@ import collections import contextlib import copy -import distutils.dir_util # pylint: disable=no-name-in-module,import-error import os import platform import pwd -from subprocess import Popen, PIPE +import shutil +from subprocess import PIPE, Popen + import py import pytest @@ -22,157 +23,168 @@ def pytest_addoption(parser): ) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def shellcheck_version(): """Version of shellcheck supported""" - return '0.8.0' + return "0.9.0" -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def pylint_version(): """Version of pylint supported""" - return '2.6.0' + return "2.17.0" -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") +def isort_version(): + """Version of isort supported""" + return "5.12.0" + + +@pytest.fixture(scope="session") def flake8_version(): """Version of flake8 supported""" - return '3.8.4' + return "6.0.0" -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") +def black_version(): + """Version of black supported""" + return "23.1.0" + + +@pytest.fixture(scope="session") def yamllint_version(): """Version of yamllint supported""" - return '1.25.0' + return "1.30.0" -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def tst_user(): """Test session's user id""" return pwd.getpwuid(os.getuid()).pw_name -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def tst_host(): """Test session's short hostname value""" - return platform.node().split('.')[0] + return platform.node().split(".")[0] -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def tst_distro(runner): """Test session's distro""" - distro = '' + distro = "" with contextlib.suppress(Exception): - run = runner(command=['lsb_release', '-si'], report=False) + run = runner(command=["lsb_release", "-si"], report=False) distro = run.out.strip() return distro -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def tst_distro_family(runner): """Test session's distro_family""" - family = '' + family = "" with contextlib.suppress(Exception): - run = runner(command=[ - 'grep', '-oP', r'ID_LIKE=\K.+', '/etc/os-release'], report=False) + run = runner(command=["grep", "-oP", r"ID_LIKE=\K.+", "/etc/os-release"], report=False) family = run.out.strip() return family -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def tst_sys(): """Test session's uname value""" return platform.system() -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def tst_arch(): """Test session's uname value""" return platform.machine() -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def supported_commands(): """List of supported commands This list should be updated every time yadm learns a new command. """ return [ - 'alt', - 'bootstrap', - 'clean', - 'clone', - 'config', - 'decrypt', - 'encrypt', - 'enter', - 'git-crypt', - 'gitconfig', - 'help', - 'init', - 'introspect', - 'list', - 'perms', - 'transcrypt', - 'upgrade', - 'version', - ] + "alt", + "bootstrap", + "clean", + "clone", + "config", + "decrypt", + "encrypt", + "enter", + "git-crypt", + "gitconfig", + "help", + "init", + "introspect", + "list", + "perms", + "transcrypt", + "upgrade", + "version", + ] -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def supported_configs(): """List of supported config options This list should be updated every time yadm learns a new config. """ return [ - 'local.arch', - 'local.class', - 'local.hostname', - 'local.os', - 'local.user', - 'yadm.alt-copy', - 'yadm.auto-alt', - 'yadm.auto-exclude', - 'yadm.auto-perms', - 'yadm.auto-private-dirs', - 'yadm.cipher', - 'yadm.git-program', - 'yadm.gpg-perms', - 'yadm.gpg-program', - 'yadm.gpg-recipient', - 'yadm.openssl-ciphername', - 'yadm.openssl-old', - 'yadm.openssl-program', - 'yadm.ssh-perms', - ] + "local.arch", + "local.class", + "local.hostname", + "local.os", + "local.user", + "yadm.alt-copy", + "yadm.auto-alt", + "yadm.auto-exclude", + "yadm.auto-perms", + "yadm.auto-private-dirs", + "yadm.cipher", + "yadm.git-program", + "yadm.gpg-perms", + "yadm.gpg-program", + "yadm.gpg-recipient", + "yadm.openssl-ciphername", + "yadm.openssl-old", + "yadm.openssl-program", + "yadm.ssh-perms", + ] -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def supported_switches(): """List of supported switches This list should be updated every time yadm learns a new switch. """ return [ - '--yadm-archive', - '--yadm-bootstrap', - '--yadm-config', - '--yadm-data', - '--yadm-dir', - '--yadm-encrypt', - '--yadm-repo', - '-Y', - ] + "--yadm-archive", + "--yadm-bootstrap", + "--yadm-config", + "--yadm-data", + "--yadm-dir", + "--yadm-encrypt", + "--yadm-repo", + "-Y", + ] -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def supported_local_configs(supported_configs): """List of supported local config options""" - return [c for c in supported_configs if c.startswith('local.')] + return [c for c in supported_configs if c.startswith("local.")] -class Runner(): +class Runner: """Class for running commands Within yadm tests, this object should be used when running commands that @@ -185,17 +197,9 @@ class Runner(): Other instances of simply running commands should use os.system(). """ - def __init__( - self, - command, - inp=None, - shell=False, - cwd=None, - env=None, - expect=None, - report=True): + def __init__(self, command, inp=None, shell=False, cwd=None, env=None, expect=None, report=True): if shell: - self.command = ' '.join([str(cmd) for cmd in command]) + self.command = " ".join([str(cmd) for cmd in command]) else: self.command = command if env is None: @@ -204,7 +208,7 @@ class Runner(): merged_env.update(env) self.inp = inp self.wrap(expect) - process = Popen( + with Popen( self.command, stdin=PIPE, stdout=PIPE, @@ -212,67 +216,57 @@ class Runner(): shell=shell, cwd=cwd, env=merged_env, - ) - input_bytes = self.inp - if self.inp: - input_bytes = self.inp.encode() - (out_bstream, err_bstream) = process.communicate(input=input_bytes) - self.out = out_bstream.decode() - self.err = err_bstream.decode() - self.code = process.wait() + ) as process: + input_bytes = self.inp + if self.inp: + input_bytes = self.inp.encode() + (out_bstream, err_bstream) = process.communicate(input=input_bytes) + self.out = out_bstream.decode() + self.err = err_bstream.decode() + self.code = process.wait() self.success = self.code == 0 self.failure = self.code != 0 if report: self.report() def __repr__(self): - return f'Runner({self.command})' + return f"Runner({self.command})" def report(self): """Print code/stdout/stderr""" - print(f'{self}') - print(f' RUN: code:{self.code}') + print(f"{self}") + print(f" RUN: code:{self.code}") if self.inp: - print(f' RUN: input:\n{self.inp}') - print(f' RUN: stdout:\n{self.out}') - print(f' RUN: stderr:\n{self.err}') + print(f" RUN: input:\n{self.inp}") + print(f" RUN: stdout:\n{self.out}") + print(f" RUN: stderr:\n{self.err}") def wrap(self, expect): """Wrap command with expect""" if not expect: return - cmdline = ' '.join([f'"{w}"' for w in self.command]) - expect_script = f'set timeout 2\nspawn {cmdline}\n' + cmdline = " ".join([f'"{w}"' for w in self.command]) + expect_script = f"set timeout 2\nspawn {cmdline}\n" for question, answer in expect: - expect_script += ( - 'expect {\n' - f'"{question}" {{send "{answer}\\r"}}\n' - 'timeout {close;exit 128}\n' - '}\n') - expect_script += ( - 'expect eof\n' - 'foreach {pid spawnid os_error_flag value} [wait] break\n' - 'exit $value') + expect_script += "expect {\n" f'"{question}" {{send "{answer}\\r"}}\n' "timeout {close;exit 128}\n" "}\n" + expect_script += "expect eof\n" "foreach {pid spawnid os_error_flag value} [wait] break\n" "exit $value" self.inp = expect_script - print(f'EXPECT:{expect_script}') - self.command = ['expect'] + print(f"EXPECT:{expect_script}") + self.command = ["expect"] -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def runner(): """Class for running commands""" return Runner -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def config_git(): """Configure global git configuration, if missing""" - os.system( - 'git config user.name || ' - 'git config --global user.name "test"') - os.system( - 'git config user.email || ' - 'git config --global user.email "test@test.test"') + os.system("git config init.defaultBranch || git config --global init.defaultBranch master") + os.system('git config user.name || git config --global user.name "test"') + os.system('git config user.email || git config --global user.email "test@test.test"') @pytest.fixture() @@ -282,19 +276,19 @@ def repo_config(runner, paths): def query_func(key): """Query a yadm repo configuration value""" run = runner( - command=('git', 'config', '--local', key), - env={'GIT_DIR': paths.repo}, + command=("git", "config", "--local", key), + env={"GIT_DIR": paths.repo}, report=False, - ) + ) return run.out.rstrip() return query_func -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def yadm(): """Path to yadm program to be tested""" - full_path = os.path.realpath('yadm') + full_path = os.path.realpath("yadm") assert os.path.isfile(full_path), "yadm program file isn't present" return full_path @@ -303,38 +297,40 @@ def yadm(): def paths(tmpdir, yadm): """Function scoped test paths""" - dir_root = tmpdir.mkdir('root') - dir_remote = dir_root.mkdir('remote') - dir_work = dir_root.mkdir('work') - dir_xdg_data = dir_root.mkdir('xdg_data') - dir_xdg_home = dir_root.mkdir('xdg_home') - dir_data = dir_xdg_data.mkdir('yadm') - dir_yadm = dir_xdg_home.mkdir('yadm') - dir_hooks = dir_yadm.mkdir('hooks') - dir_repo = dir_data.mkdir('repo.git') - file_archive = dir_data.join('archive') - file_bootstrap = dir_yadm.join('bootstrap') - file_config = dir_yadm.join('config') - file_encrypt = dir_yadm.join('encrypt') + dir_root = tmpdir.mkdir("root") + dir_remote = dir_root.mkdir("remote") + dir_work = dir_root.mkdir("work") + dir_xdg_data = dir_root.mkdir("xdg_data") + dir_xdg_home = dir_root.mkdir("xdg_home") + dir_data = dir_xdg_data.mkdir("yadm") + dir_yadm = dir_xdg_home.mkdir("yadm") + dir_hooks = dir_yadm.mkdir("hooks") + dir_repo = dir_data.mkdir("repo.git") + file_archive = dir_data.join("archive") + file_bootstrap = dir_yadm.join("bootstrap") + file_config = dir_yadm.join("config") + file_encrypt = dir_yadm.join("encrypt") paths = collections.namedtuple( - 'Paths', [ - 'pgm', - 'root', - 'remote', - 'work', - 'xdg_data', - 'xdg_home', - 'data', - 'yadm', - 'hooks', - 'repo', - 'archive', - 'bootstrap', - 'config', - 'encrypt', - ]) - os.environ['XDG_CONFIG_HOME'] = str(dir_xdg_home) - os.environ['XDG_DATA_HOME'] = str(dir_xdg_data) + "Paths", + [ + "pgm", + "root", + "remote", + "work", + "xdg_data", + "xdg_home", + "data", + "yadm", + "hooks", + "repo", + "archive", + "bootstrap", + "config", + "encrypt", + ], + ) + os.environ["XDG_CONFIG_HOME"] = str(dir_xdg_home) + os.environ["XDG_DATA_HOME"] = str(dir_xdg_data) return paths( yadm, dir_root, @@ -350,19 +346,25 @@ def paths(tmpdir, yadm): file_bootstrap, file_config, file_encrypt, - ) + ) @pytest.fixture() def yadm_cmd(paths): """Generate custom command_list function""" + def command_list(*args): """Produce params for running yadm with -Y""" return [paths.pgm] + list(args) + return command_list -class DataFile(): +class NoRelativePath(Exception): + """Exception when finding relative paths""" + + +class DataFile: """Datafile object""" def __init__(self, path, tracked=True, private=False): @@ -381,7 +383,7 @@ class DataFile(): """Relative path property""" if self.__parent: return self.__parent.join(self.path) - raise BaseException('Unable to provide relative path, no parent') + raise NoRelativePath("Unable to provide relative path, no parent") @property def tracked(self): @@ -398,22 +400,18 @@ class DataFile(): self.__parent = parent -class DataSet(): +class DataSet: """Dataset object""" def __init__(self): - self.__files = list() - self.__dirs = list() - self.__tracked_dirs = list() - self.__private_dirs = list() + self.__files = [] + self.__dirs = [] + self.__tracked_dirs = [] + self.__private_dirs = [] self.__relpath = None def __repr__(self): - return ( - f'[DS with {len(self)} files; ' - f'{len(self.tracked)} tracked, ' - f'{len(self.private)} private]' - ) + return f"[DS with {len(self)} files; " f"{len(self.tracked)} tracked, " f"{len(self.private)} private]" def __iter__(self): return iter(self.__files) @@ -451,17 +449,17 @@ class DataSet(): @property def plain_dirs(self): """List of directories in DataSet not starting with '.'""" - return [d for d in self.dirs if not d.startswith('.')] + return [d for d in self.dirs if not d.startswith(".")] @property def hidden_dirs(self): """List of directories in DataSet starting with '.'""" - return [d for d in self.dirs if d.startswith('.')] + return [d for d in self.dirs if d.startswith(".")] @property def tracked_dirs(self): """List of directories in DataSet not starting with '.'""" - return [d for d in self.__tracked_dirs if not d.startswith('.')] + return [d for d in self.__tracked_dirs if not d.startswith(".")] @property def private_dirs(self): @@ -491,23 +489,23 @@ class DataSet(): datafile.relative_to(self.__relpath) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def ds1_dset(tst_sys): """Meta-data for dataset one files""" dset = DataSet() - dset.add_file('t1') - dset.add_file('d1/t2') - dset.add_file(f'test_alt_copy##os.{tst_sys}') - dset.add_file('u1', tracked=False) - dset.add_file('d2/u2', tracked=False) - dset.add_file('.ssh/p1', tracked=False, private=True) - dset.add_file('.ssh/.p2', tracked=False, private=True) - dset.add_file('.gnupg/p3', tracked=False, private=True) - dset.add_file('.gnupg/.p4', tracked=False, private=True) + dset.add_file("t1") + dset.add_file("d1/t2") + dset.add_file(f"test_alt_copy##os.{tst_sys}") + dset.add_file("u1", tracked=False) + dset.add_file("d2/u2", tracked=False) + dset.add_file(".ssh/p1", tracked=False, private=True) + dset.add_file(".ssh/.p2", tracked=False, private=True) + dset.add_file(".gnupg/p3", tracked=False, private=True) + dset.add_file(".gnupg/.p4", tracked=False, private=True) return dset -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def ds1_data(tmpdir_factory, config_git, ds1_dset, runner): """A set of test data, worktree & repo""" # pylint: disable=unused-argument @@ -515,65 +513,40 @@ def ds1_data(tmpdir_factory, config_git, ds1_dset, runner): # @pytest.mark.usefixtures('config_git') # cannot be applied to another fixture. - data = tmpdir_factory.mktemp('ds1') + data = tmpdir_factory.mktemp("ds1") - work = data.mkdir('work') + work = data.mkdir("work") for datafile in ds1_dset: work.join(datafile.path).write(datafile.path, ensure=True) - repo = data.mkdir('repo.git') + repo = data.mkdir("repo.git") env = os.environ.copy() - env['GIT_DIR'] = str(repo) - runner( - command=['git', 'init', '--shared=0600', '--bare', str(repo)], - report=False) - runner( - command=['git', 'config', 'core.bare', 'false'], - env=env, - report=False) - runner( - command=['git', 'config', 'status.showUntrackedFiles', 'no'], - env=env, - report=False) - runner( - command=['git', 'config', 'yadm.managed', 'true'], - env=env, - report=False) - runner( - command=['git', 'config', 'core.worktree', str(work)], - env=env, - report=False) - runner( - command=['git', 'add'] + - [str(work.join(f.path)) for f in ds1_dset if f.tracked], - env=env) - runner( - command=['git', 'commit', '--allow-empty', '-m', 'Initial commit'], - env=env, - report=False) + env["GIT_DIR"] = str(repo) + runner(command=["git", "init", "--shared=0600", "--bare", str(repo)], report=False) + runner(command=["git", "config", "core.bare", "false"], env=env, report=False) + runner(command=["git", "config", "status.showUntrackedFiles", "no"], env=env, report=False) + runner(command=["git", "config", "yadm.managed", "true"], env=env, report=False) + runner(command=["git", "config", "core.worktree", str(work)], env=env, report=False) + runner(command=["git", "add"] + [str(work.join(f.path)) for f in ds1_dset if f.tracked], env=env) + runner(command=["git", "commit", "--allow-empty", "-m", "Initial commit"], env=env, report=False) - data = collections.namedtuple('Data', ['work', 'repo']) + data = collections.namedtuple("Data", ["work", "repo"]) return data(work, repo) @pytest.fixture() def ds1_work_copy(ds1_data, paths): """Function scoped copy of ds1_data.work""" - distutils.dir_util.copy_tree( # pylint: disable=no-member - str(ds1_data.work), str(paths.work)) + shutil.copytree(str(ds1_data.work), str(paths.work), dirs_exist_ok=True) @pytest.fixture() def ds1_repo_copy(runner, ds1_data, paths): """Function scoped copy of ds1_data.repo""" - distutils.dir_util.copy_tree( # pylint: disable=no-member - str(ds1_data.repo), str(paths.repo)) + shutil.copytree(str(ds1_data.repo), str(paths.repo), dirs_exist_ok=True) env = os.environ.copy() - env['GIT_DIR'] = str(paths.repo) - runner( - command=['git', 'config', 'core.worktree', str(paths.work)], - env=env, - report=False) + env["GIT_DIR"] = str(paths.repo) + runner(command=["git", "config", "core.worktree", str(paths.work)], env=env, report=False) @pytest.fixture() @@ -598,30 +571,27 @@ def ds1(ds1_work_copy, paths, ds1_dset): return dscopy -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def gnupg(tmpdir_factory, runner): """Location of GNUPGHOME""" def register_gpg_password(password): """Publish a new GPG mock password""" - py.path.local('/tmp/mock-password').write(password) + py.path.local("/tmp/mock-password").write(password) - home = tmpdir_factory.mktemp('gnupghome') + home = tmpdir_factory.mktemp("gnupghome") home.chmod(0o700) - conf = home.join('gpg.conf') - conf.write('no-secmem-warning\n') + conf = home.join("gpg.conf") + conf.write("no-secmem-warning\n") conf.chmod(0o600) - agentconf = home.join('gpg-agent.conf') - agentconf.write( - f'pinentry-program {os.path.abspath("test/pinentry-mock")}\n' - 'max-cache-ttl 0\n' - ) + agentconf = home.join("gpg-agent.conf") + agentconf.write(f'pinentry-program {os.path.abspath("test/pinentry-mock")}\n' "max-cache-ttl 0\n") agentconf.chmod(0o600) - data = collections.namedtuple('GNUPG', ['home', 'pw']) + data = collections.namedtuple("GNUPG", ["home", "pw"]) env = os.environ.copy() - env['GNUPGHOME'] = home + env["GNUPGHOME"] = home # this pre-populates std files in the GNUPGHOME - runner(['gpg', '-k'], env=env) + runner(["gpg", "-k"], env=env) return data(home, register_gpg_password) diff --git a/test/pinentry-mock b/test/pinentry-mock index d40033b..39da043 100755 --- a/test/pinentry-mock +++ b/test/pinentry-mock @@ -1,12 +1,20 @@ #!/bin/bash # This program is a custom mock pinentry program -# It always uses whatever password is found in the /tmp directory -password="$(cat /tmp/mock-password 2>/dev/null)" +# It uses whatever password is found in the /tmp directory +# If the password is empty, replies CANCEL causing an error to similate invalid +# credentials echo "OK Pleased to meet you" while read -r line; do if [[ $line =~ GETPIN ]]; then - echo -n "D " - echo "$password" + password="$(cat /tmp/mock-password 2>/dev/null)" + if [ -n "$password" ]; then + echo -n "D " + echo "$password" + echo "OK"; + else + echo "CANCEL"; + fi + else + echo "OK"; fi - echo "OK"; done diff --git a/test/pylintrc b/test/pylintrc deleted file mode 120000 index 05334af..0000000 --- a/test/pylintrc +++ /dev/null @@ -1 +0,0 @@ -../pylintrc \ No newline at end of file diff --git a/test/requirements.txt b/test/requirements.txt index 30da6ae..e71b349 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,6 +1,8 @@ +black==23.1.0 envtpl -flake8==3.8.4 +flake8==6.0.0 +isort==5.12.0 j2cli -pylint==2.6.0 -pytest==6.2.1 -yamllint==1.25.0 +pylint==2.17.0 +pytest==7.2.2 +yamllint==1.30.0 diff --git a/test/test_alt.py b/test/test_alt.py index ddaf374..c429ad4 100644 --- a/test/test_alt.py +++ b/test/test_alt.py @@ -1,6 +1,7 @@ """Test alt""" import os import string + import py import pytest import utils @@ -8,34 +9,34 @@ import utils TEST_PATHS = [utils.ALT_FILE1, utils.ALT_FILE2, utils.ALT_DIR] -@pytest.mark.usefixtures('ds1_copy') -@pytest.mark.parametrize('yadm_alt', [True, False], ids=['alt', 'worktree']) +@pytest.mark.usefixtures("ds1_copy") +@pytest.mark.parametrize("yadm_alt", [True, False], ids=["alt", "worktree"]) @pytest.mark.parametrize( - 'tracked,encrypt,exclude', [ + "tracked,encrypt,exclude", + [ (False, False, False), (True, False, False), (False, True, False), (False, True, True), - ], ids=['untracked', 'tracked', 'encrypted', 'excluded']) -def test_alt_source( - runner, paths, - tracked, encrypt, exclude, - yadm_alt): + ], + ids=["untracked", "tracked", "encrypted", "excluded"], +) +def test_alt_source(runner, paths, tracked, encrypt, exclude, yadm_alt): """Test yadm alt operates on all expected sources of alternates""" yadm_dir, yadm_data = setup_standard_yadm_dir(paths) utils.create_alt_files( - paths, '##default', tracked=tracked, encrypt=encrypt, exclude=exclude, - yadm_alt=yadm_alt, yadm_dir=yadm_dir) - run = runner([paths.pgm, '-Y', yadm_dir, '--yadm-data', yadm_data, 'alt']) + paths, "##default", tracked=tracked, encrypt=encrypt, exclude=exclude, yadm_alt=yadm_alt, yadm_dir=yadm_dir + ) + run = runner([paths.pgm, "-Y", yadm_dir, "--yadm-data", yadm_data, "alt"]) assert run.success - assert run.err == '' + assert run.err == "" linked = utils.parse_alt_output(run.out) - basepath = yadm_dir.join('alt') if yadm_alt else paths.work + basepath = yadm_dir.join("alt") if yadm_alt else paths.work for link_path in TEST_PATHS: - source_file_content = link_path + '##default' + source_file_content = link_path + "##default" source_file = basepath.join(source_file_content) link_file = paths.work.join(link_path) if tracked or (encrypt and not exclude): @@ -45,63 +46,69 @@ def test_alt_source( assert link_file.read() == source_file_content assert str(source_file) in linked else: - assert link_file.join( - utils.CONTAINED).read() == source_file_content + assert link_file.join(utils.CONTAINED).read() == source_file_content assert str(source_file) in linked else: assert not link_file.exists() assert str(source_file) not in linked -@pytest.mark.usefixtures('ds1_copy') -@pytest.mark.parametrize('yadm_alt', [True, False], ids=['alt', 'worktree']) +@pytest.mark.usefixtures("ds1_copy") +@pytest.mark.parametrize("yadm_alt", [True, False], ids=["alt", "worktree"]) def test_relative_link(runner, paths, yadm_alt): """Confirm links created are relative""" yadm_dir, yadm_data = setup_standard_yadm_dir(paths) utils.create_alt_files( - paths, '##default', tracked=True, encrypt=False, exclude=False, - yadm_alt=yadm_alt, yadm_dir=yadm_dir) - run = runner([paths.pgm, '-Y', yadm_dir, '--yadm-data', yadm_data, 'alt']) + paths, "##default", tracked=True, encrypt=False, exclude=False, yadm_alt=yadm_alt, yadm_dir=yadm_dir + ) + run = runner([paths.pgm, "-Y", yadm_dir, "--yadm-data", yadm_data, "alt"]) assert run.success - assert run.err == '' + assert run.err == "" - basepath = yadm_dir.join('alt') if yadm_alt else paths.work + basepath = yadm_dir.join("alt") if yadm_alt else paths.work for link_path in TEST_PATHS: - source_file_content = link_path + '##default' + source_file_content = link_path + "##default" source_file = basepath.join(source_file_content) link_file = paths.work.join(link_path) link = link_file.readlink() - relpath = os.path.relpath( - source_file, start=os.path.dirname(link_file)) + relpath = os.path.relpath(source_file, start=os.path.dirname(link_file)) assert link == relpath -@pytest.mark.usefixtures('ds1_copy') -@pytest.mark.parametrize('suffix', [ - '##default', - '##default,e.txt', '##default,extension.txt', - '##a.$tst_arch', '##arch.$tst_arch', - '##o.$tst_sys', '##os.$tst_sys', - '##d.$tst_distro', '##distro.$tst_distro', - '##f.$tst_distro_family', '##distro_family.$tst_distro_family', - '##c.$tst_class', '##class.$tst_class', - '##h.$tst_host', '##hostname.$tst_host', - '##u.$tst_user', '##user.$tst_user', - ]) -def test_alt_conditions( - runner, paths, - tst_arch, tst_sys, tst_distro, tst_distro_family, tst_host, tst_user, - suffix): +@pytest.mark.usefixtures("ds1_copy") +@pytest.mark.parametrize( + "suffix", + [ + "##default", + "##default,e.txt", + "##default,extension.txt", + "##a.$tst_arch", + "##arch.$tst_arch", + "##o.$tst_sys", + "##os.$tst_sys", + "##d.$tst_distro", + "##distro.$tst_distro", + "##f.$tst_distro_family", + "##distro_family.$tst_distro_family", + "##c.$tst_class", + "##class.$tst_class", + "##h.$tst_host", + "##hostname.$tst_host", + "##u.$tst_user", + "##user.$tst_user", + ], +) +def test_alt_conditions(runner, paths, tst_arch, tst_sys, tst_distro, tst_distro_family, tst_host, tst_user, suffix): """Test conditions supported by yadm alt""" yadm_dir, yadm_data = setup_standard_yadm_dir(paths) # set the class - tst_class = 'testclass' - utils.set_local(paths, 'class', tst_class + ".before") - utils.set_local(paths, 'class', tst_class, add=True) - utils.set_local(paths, 'class', tst_class + ".after", add=True) + tst_class = "testclass" + utils.set_local(paths, "class", tst_class + ".before") + utils.set_local(paths, "class", tst_class, add=True) + utils.set_local(paths, "class", tst_class + ".after", add=True) suffix = string.Template(suffix).substitute( tst_arch=tst_arch, @@ -114,9 +121,9 @@ def test_alt_conditions( ) utils.create_alt_files(paths, suffix) - run = runner([paths.pgm, '-Y', yadm_dir, '--yadm-data', yadm_data, 'alt']) + run = runner([paths.pgm, "-Y", yadm_dir, "--yadm-data", yadm_data, "alt"]) assert run.success - assert run.err == '' + assert run.err == "" linked = utils.parse_alt_output(run.out) for link_path in TEST_PATHS: @@ -127,27 +134,31 @@ def test_alt_conditions( assert paths.work.join(link_path).read() == source_file assert str(paths.work.join(source_file)) in linked else: - assert paths.work.join(link_path).join( - utils.CONTAINED).read() == source_file + assert paths.work.join(link_path).join(utils.CONTAINED).read() == source_file assert str(paths.work.join(source_file)) in linked -@pytest.mark.usefixtures('ds1_copy') +@pytest.mark.usefixtures("ds1_copy") +@pytest.mark.parametrize("kind", ["default", "", None, "envtpl", "j2cli", "j2", "esh"]) @pytest.mark.parametrize( - 'kind', ['default', '', None, 'envtpl', 'j2cli', 'j2', 'esh']) -@pytest.mark.parametrize('label', ['t', 'template', 'yadm', ]) -def test_alt_templates( - runner, paths, kind, label): + "label", + [ + "t", + "template", + "yadm", + ], +) +def test_alt_templates(runner, paths, kind, label): """Test templates supported by yadm alt""" yadm_dir, yadm_data = setup_standard_yadm_dir(paths) - suffix = f'##{label}.{kind}' + suffix = f"##{label}.{kind}" if kind is None: - suffix = f'##{label}' + suffix = f"##{label}" utils.create_alt_files(paths, suffix) - run = runner([paths.pgm, '-Y', yadm_dir, '--yadm-data', yadm_data, 'alt']) + run = runner([paths.pgm, "-Y", yadm_dir, "--yadm-data", yadm_data, "alt"]) assert run.success - assert run.err == '' + assert run.err == "" created = utils.parse_alt_output(run.out, linked=False) for created_path in TEST_PATHS: @@ -158,41 +169,39 @@ def test_alt_templates( assert str(paths.work.join(source_file)) in created -@pytest.mark.usefixtures('ds1_copy') -@pytest.mark.parametrize('autoalt', [None, 'true', 'false']) +@pytest.mark.usefixtures("ds1_copy") +@pytest.mark.parametrize("autoalt", [None, "true", "false"]) def test_auto_alt(runner, yadm_cmd, paths, autoalt): """Test auto alt""" # set the value of auto-alt if autoalt: - os.system(' '.join(yadm_cmd('config', 'yadm.auto-alt', autoalt))) + os.system(" ".join(yadm_cmd("config", "yadm.auto-alt", autoalt))) - utils.create_alt_files(paths, '##default') - run = runner(yadm_cmd('status')) + utils.create_alt_files(paths, "##default") + run = runner(yadm_cmd("status")) assert run.success - assert run.err == '' + assert run.err == "" linked = utils.parse_alt_output(run.out) for link_path in TEST_PATHS: - source_file = link_path + '##default' - if autoalt == 'false': + source_file = link_path + "##default" + if autoalt == "false": assert not paths.work.join(link_path).exists() else: assert paths.work.join(link_path).islink() - target = py.path.local( - os.path.realpath(paths.work.join(link_path))) + target = py.path.local(os.path.realpath(paths.work.join(link_path))) if target.isfile(): assert paths.work.join(link_path).read() == source_file # no linking output when run via auto-alt assert str(paths.work.join(source_file)) not in linked else: - assert paths.work.join(link_path).join( - utils.CONTAINED).read() == source_file + assert paths.work.join(link_path).join(utils.CONTAINED).read() == source_file # no linking output when run via auto-alt assert str(paths.work.join(source_file)) not in linked -@pytest.mark.usefixtures('ds1_copy') +@pytest.mark.usefixtures("ds1_copy") def test_stale_link_removal(runner, yadm_cmd, paths): """Stale links to alternative files are removed @@ -201,48 +210,47 @@ def test_stale_link_removal(runner, yadm_cmd, paths): """ # set the class - tst_class = 'testclass' - utils.set_local(paths, 'class', tst_class) + tst_class = "testclass" + utils.set_local(paths, "class", tst_class) # create files which match the test class - utils.create_alt_files(paths, f'##class.{tst_class}') + utils.create_alt_files(paths, f"##class.{tst_class}") # run alt to trigger linking - run = runner(yadm_cmd('alt')) + run = runner(yadm_cmd("alt")) assert run.success - assert run.err == '' + assert run.err == "" linked = utils.parse_alt_output(run.out) # assert the proper linking has occurred for stale_path in TEST_PATHS: - source_file = stale_path + '##class.' + tst_class + source_file = stale_path + "##class." + tst_class assert paths.work.join(stale_path).islink() target = py.path.local(os.path.realpath(paths.work.join(stale_path))) if target.isfile(): assert paths.work.join(stale_path).read() == source_file assert str(paths.work.join(source_file)) in linked else: - assert paths.work.join(stale_path).join( - utils.CONTAINED).read() == source_file + assert paths.work.join(stale_path).join(utils.CONTAINED).read() == source_file assert str(paths.work.join(source_file)) in linked # change the class so there are no valid alternates - utils.set_local(paths, 'class', 'changedclass') + utils.set_local(paths, "class", "changedclass") # run alt to trigger linking - run = runner(yadm_cmd('alt')) + run = runner(yadm_cmd("alt")) assert run.success - assert run.err == '' + assert run.err == "" linked = utils.parse_alt_output(run.out) # assert the linking is removed for stale_path in TEST_PATHS: - source_file = stale_path + '##class.' + tst_class + source_file = stale_path + "##class." + tst_class assert not paths.work.join(stale_path).exists() assert str(paths.work.join(source_file)) not in linked -@pytest.mark.usefixtures('ds1_repo_copy') +@pytest.mark.usefixtures("ds1_repo_copy") def test_template_overwrite_symlink(runner, yadm_cmd, paths, tst_sys): """Remove symlinks before processing a template @@ -251,45 +259,44 @@ def test_template_overwrite_symlink(runner, yadm_cmd, paths, tst_sys): be removed just before processing a template. """ - target = paths.work.join(f'test_link##os.{tst_sys}') - target.write('target') + target = paths.work.join(f"test_link##os.{tst_sys}") + target.write("target") - link = paths.work.join('test_link') + link = paths.work.join("test_link") link.mksymlinkto(target, absolute=1) - template = paths.work.join('test_link##template.default') - template.write('test-data') + template = paths.work.join("test_link##template.default") + template.write("test-data") - run = runner(yadm_cmd('add', target, template)) + run = runner(yadm_cmd("add", target, template)) assert run.success - assert run.err == '' - assert run.out == '' + assert run.err == "" + assert run.out == "" assert not link.islink() - assert target.read().strip() == 'target' - assert link.read().strip() == 'test-data' + assert target.read().strip() == "target" + assert link.read().strip() == "test-data" -@pytest.mark.usefixtures('ds1_copy') -@pytest.mark.parametrize('style', ['symlink', 'template']) +@pytest.mark.usefixtures("ds1_copy") +@pytest.mark.parametrize("style", ["symlink", "template"]) def test_ensure_alt_path(runner, paths, style): """Test that directories are created before making alternates""" yadm_dir, yadm_data = setup_standard_yadm_dir(paths) - suffix = 'default' if style == 'symlink' else 'template' - filename = 'a/b/c/file' - source = yadm_dir.join(f'alt/{filename}##{suffix}') - source.write('test-data', ensure=True) - run = runner([ - paths.pgm, '-Y', yadm_dir, '--yadm-data', yadm_data, 'add', source]) + suffix = "default" if style == "symlink" else "template" + filename = "a/b/c/file" + source = yadm_dir.join(f"alt/{filename}##{suffix}") + source.write("test-data", ensure=True) + run = runner([paths.pgm, "-Y", yadm_dir, "--yadm-data", yadm_data, "add", source]) assert run.success - assert run.err == '' - assert run.out == '' - assert paths.work.join(filename).read().strip() == 'test-data' + assert run.err == "" + assert run.out == "" + assert paths.work.join(filename).read().strip() == "test-data" def setup_standard_yadm_dir(paths): """Configure a yadm home within the work tree""" - std_yadm_dir = paths.work.mkdir('.config').mkdir('yadm') - std_yadm_data = paths.work.mkdir('.local').mkdir('share').mkdir('yadm') - std_yadm_data.join('repo.git').mksymlinkto(paths.repo, absolute=1) - std_yadm_dir.join('encrypt').mksymlinkto(paths.encrypt, absolute=1) + std_yadm_dir = paths.work.mkdir(".config").mkdir("yadm") + std_yadm_data = paths.work.mkdir(".local").mkdir("share").mkdir("yadm") + std_yadm_data.join("repo.git").mksymlinkto(paths.repo, absolute=1) + std_yadm_dir.join("encrypt").mksymlinkto(paths.encrypt, absolute=1) return std_yadm_dir, std_yadm_data diff --git a/test/test_alt_copy.py b/test/test_alt_copy.py index fa8e09c..e1beece 100644 --- a/test/test_alt_copy.py +++ b/test/test_alt_copy.py @@ -1,45 +1,46 @@ """Test yadm.alt-copy""" import os + import pytest @pytest.mark.parametrize( - 'setting, expect_link, pre_existing', [ + "setting, expect_link, pre_existing", + [ (None, True, None), (True, False, None), (False, True, None), - (True, False, 'link'), - (True, False, 'file'), + (True, False, "link"), + (True, False, "file"), ], ids=[ - 'unset', - 'true', - 'false', - 'pre-existing symlink', - 'pre-existing file', - ]) -@pytest.mark.usefixtures('ds1_copy') -def test_alt_copy( - runner, yadm_cmd, paths, tst_sys, - setting, expect_link, pre_existing): + "unset", + "true", + "false", + "pre-existing symlink", + "pre-existing file", + ], +) +@pytest.mark.usefixtures("ds1_copy") +def test_alt_copy(runner, yadm_cmd, paths, tst_sys, setting, expect_link, pre_existing): """Test yadm.alt-copy""" if setting is not None: - os.system(' '.join(yadm_cmd('config', 'yadm.alt-copy', str(setting)))) + os.system(" ".join(yadm_cmd("config", "yadm.alt-copy", str(setting)))) - expected_content = f'test_alt_copy##os.{tst_sys}' + expected_content = f"test_alt_copy##os.{tst_sys}" - alt_path = paths.work.join('test_alt_copy') - if pre_existing == 'symlink': + alt_path = paths.work.join("test_alt_copy") + if pre_existing == "symlink": alt_path.mklinkto(expected_content) - elif pre_existing == 'file': - alt_path.write('wrong content') + elif pre_existing == "file": + alt_path.write("wrong content") - run = runner(yadm_cmd('alt')) + run = runner(yadm_cmd("alt")) assert run.success - assert run.err == '' - assert 'Linking' in run.out + assert run.err == "" + assert "Linking" in run.out assert alt_path.read() == expected_content assert alt_path.islink() == expect_link diff --git a/test/test_assert_private_dirs.py b/test/test_assert_private_dirs.py index bfd55ac..0cba1f6 100644 --- a/test/test_assert_private_dirs.py +++ b/test/test_assert_private_dirs.py @@ -2,13 +2,14 @@ import os import re + import pytest -pytestmark = pytest.mark.usefixtures('ds1_copy') -PRIVATE_DIRS = ['.gnupg', '.ssh'] +pytestmark = pytest.mark.usefixtures("ds1_copy") +PRIVATE_DIRS = [".gnupg", ".ssh"] -@pytest.mark.parametrize('home', [True, False], ids=['home', 'not-home']) +@pytest.mark.parametrize("home", [True, False], ids=["home", "not-home"]) def test_pdirs_missing(runner, yadm_cmd, paths, home): """Private dirs (private dirs missing) @@ -24,15 +25,15 @@ def test_pdirs_missing(runner, yadm_cmd, paths, home): path.remove() assert not path.exists() - env = {'DEBUG': 'yes'} + env = {"DEBUG": "yes"} if home: - env['HOME'] = paths.work + env["HOME"] = paths.work # run status - run = runner(command=yadm_cmd('status'), env=env) + run = runner(command=yadm_cmd("status"), env=env) assert run.success - assert run.err == '' - assert 'On branch master' in run.out + assert run.err == "" + assert "On branch master" in run.out # confirm directories are created # and are protected @@ -40,17 +41,15 @@ def test_pdirs_missing(runner, yadm_cmd, paths, home): path = paths.work.join(pdir) if home: assert path.exists() - assert oct(path.stat().mode).endswith('00'), ('Directory is ' - 'not secured') + assert oct(path.stat().mode).endswith("00"), "Directory is " "not secured" else: assert not path.exists() # confirm directories are created before command is run: if home: assert re.search( - (r'Creating.+\.(gnupg|ssh).+Creating.+\.(gnupg|ssh).+' - r'Running git command git status'), - run.out, re.DOTALL), 'directories created before command is run' + r"Creating.+\.(gnupg|ssh).+Creating.+\.(gnupg|ssh).+Running git command git status", run.out, re.DOTALL + ), "directories created before command is run" def test_pdirs_missing_apd_false(runner, yadm_cmd, paths): @@ -70,14 +69,13 @@ def test_pdirs_missing_apd_false(runner, yadm_cmd, paths): assert not path.exists() # set configuration - os.system(' '.join(yadm_cmd( - 'config', '--bool', 'yadm.auto-private-dirs', 'false'))) + os.system(" ".join(yadm_cmd("config", "--bool", "yadm.auto-private-dirs", "false"))) # run status - run = runner(command=yadm_cmd('status')) + run = runner(command=yadm_cmd("status")) assert run.success - assert run.err == '' - assert 'On branch master' in run.out + assert run.err == "" + assert "On branch master" in run.out # confirm directories are STILL missing for pdir in PRIVATE_DIRS: @@ -99,19 +97,18 @@ def test_pdirs_exist_apd_false(runner, yadm_cmd, paths): if not path.isdir(): path.mkdir() path.chmod(0o777) - assert oct(path.stat().mode).endswith('77'), 'Directory is secure.' + assert oct(path.stat().mode).endswith("77"), "Directory is secure." # set configuration - os.system(' '.join(yadm_cmd( - 'config', '--bool', 'yadm.auto-perms', 'false'))) + os.system(" ".join(yadm_cmd("config", "--bool", "yadm.auto-perms", "false"))) # run status - run = runner(command=yadm_cmd('status')) + run = runner(command=yadm_cmd("status")) assert run.success - assert run.err == '' - assert 'On branch master' in run.out + assert run.err == "" + assert "On branch master" in run.out # created directories are STILL permissive for pdir in PRIVATE_DIRS: path = paths.work.join(pdir) - assert oct(path.stat().mode).endswith('77'), 'Directory is secure' + assert oct(path.stat().mode).endswith("77"), "Directory is secure" diff --git a/test/test_bootstrap.py b/test/test_bootstrap.py index 4865ece..099dde8 100644 --- a/test/test_bootstrap.py +++ b/test/test_bootstrap.py @@ -4,32 +4,30 @@ import pytest @pytest.mark.parametrize( - 'exists, executable, code, expect', [ - (False, False, 1, 'Cannot execute bootstrap'), - (True, False, 1, 'is not an executable program'), - (True, True, 123, 'Bootstrap successful'), - ], ids=[ - 'missing', - 'not executable', - 'executable', - ]) -def test_bootstrap( - runner, yadm_cmd, paths, exists, executable, code, expect): + "exists, executable, code, expect", + [ + (False, False, 1, "Cannot execute bootstrap"), + (True, False, 1, "is not an executable program"), + (True, True, 123, "Bootstrap successful"), + ], + ids=[ + "missing", + "not executable", + "executable", + ], +) +def test_bootstrap(runner, yadm_cmd, paths, exists, executable, code, expect): """Test bootstrap command""" if exists: - paths.bootstrap.write('') + paths.bootstrap.write("") if executable: - paths.bootstrap.write( - '#!/bin/bash\n' - f'echo {expect}\n' - f'exit {code}\n' - ) + paths.bootstrap.write("#!/bin/bash\n" f"echo {expect}\n" f"exit {code}\n") paths.bootstrap.chmod(0o775) - run = runner(command=yadm_cmd('bootstrap')) + run = runner(command=yadm_cmd("bootstrap")) assert run.code == code if exists and executable: - assert run.err == '' + assert run.err == "" assert expect in run.out else: assert expect in run.err - assert run.out == '' + assert run.out == "" diff --git a/test/test_clean.py b/test/test_clean.py index 39e7e54..4eaf1e4 100644 --- a/test/test_clean.py +++ b/test/test_clean.py @@ -3,9 +3,9 @@ def test_clean_command(runner, yadm_cmd): """Run with clean command""" - run = runner(command=yadm_cmd('clean')) + run = runner(command=yadm_cmd("clean")) # do nothing, this is a dangerous Git command when managing dot files # report the command as disabled and exit as a failure assert run.failure - assert run.out == '' - assert 'disabled' in run.err + assert run.out == "" + assert "disabled" in run.err diff --git a/test/test_clone.py b/test/test_clone.py index d5da7d9..1cae929 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -2,30 +2,32 @@ import os import re + import pytest BOOTSTRAP_CODE = 123 -BOOTSTRAP_MSG = 'Bootstrap successful' +BOOTSTRAP_MSG = "Bootstrap successful" -@pytest.mark.usefixtures('remote') +@pytest.mark.usefixtures("remote") @pytest.mark.parametrize( - 'good_remote, repo_exists, force, conflicts', [ + "good_remote, repo_exists, force, conflicts", + [ (False, False, False, False), (True, False, False, False), (True, True, False, False), (True, True, True, False), (True, False, False, True), - ], ids=[ - 'bad remote', - 'simple', - 'existing repo', - '-f', - 'conflicts', - ]) -def test_clone( - runner, paths, yadm_cmd, repo_config, ds1, - good_remote, repo_exists, force, conflicts): + ], + ids=[ + "bad remote", + "simple", + "existing repo", + "-f", + "conflicts", + ], +) +def test_clone(runner, paths, yadm_cmd, repo_config, ds1, good_remote, repo_exists, force, conflicts): """Test basic clone operation""" # clear out the work path @@ -33,74 +35,70 @@ def test_clone( paths.work.mkdir() # determine remote url - remote_url = f'file://{paths.remote}' + remote_url = f"file://{paths.remote}" if not good_remote: - remote_url = 'file://bad_remote' + remote_url = "file://bad_remote" old_repo = None if repo_exists: # put a repo in the way paths.repo.mkdir() - old_repo = paths.repo.join('old_repo') - old_repo.write('old_repo') + old_repo = paths.repo.join("old_repo") + old_repo.write("old_repo") if conflicts: - ds1.tracked[0].relative.write('conflict') + ds1.tracked[0].relative.write("conflict") assert ds1.tracked[0].relative.exists() # run the clone command - args = ['clone', '-w', paths.work] + args = ["clone", "-w", paths.work] if force: - args += ['-f'] + args += ["-f"] args += [remote_url] run = runner(command=yadm_cmd(*args)) if not good_remote: # clone should fail assert run.failure - assert run.out == '' - assert 'Unable to clone the repository' in run.err + assert run.out == "" + assert "Unable to clone the repository" in run.err assert not paths.repo.exists() elif repo_exists and not force: # can't overwrite data assert run.failure - assert run.out == '' - assert 'Git repo already exists' in run.err + assert run.out == "" + assert "Git repo already exists" in run.err else: # clone should succeed, and repo should be configured properly assert successful_clone(run, paths, repo_config) # these clones should have master as HEAD - verify_head(paths, 'master') + verify_head(paths, "master") # ensure conflicts are handled properly if conflicts: - assert 'NOTE' in run.out - assert 'Local files with content that differs' in run.out + assert "NOTE" in run.out + assert "Local files with content that differs" in run.out # confirm correct Git origin - run = runner( - command=('git', 'remote', '-v', 'show'), - env={'GIT_DIR': paths.repo}) + run = runner(command=("git", "remote", "-v", "show"), env={"GIT_DIR": paths.repo}) assert run.success - assert run.err == '' - assert f'origin\t{remote_url}' in run.out + assert run.err == "" + assert f"origin\t{remote_url}" in run.out # ensure conflicts are really preserved if conflicts: # test that the conflicts are preserved in the work tree - run = runner( - command=yadm_cmd('status', '-uno', '--porcelain'), - cwd=paths.work) + run = runner(command=yadm_cmd("status", "-uno", "--porcelain"), cwd=paths.work) assert run.success - assert run.err == '' + assert run.err == "" assert str(ds1.tracked[0].path) in run.out # verify content of the conflicts - run = runner(command=yadm_cmd('diff'), cwd=paths.work) + run = runner(command=yadm_cmd("diff"), cwd=paths.work) assert run.success - assert run.err == '' - assert '\n+conflict' in run.out, 'conflict overwritten' + assert run.err == "" + assert "\n+conflict" in run.out, "conflict overwritten" # another force-related assertion if old_repo: @@ -110,54 +108,56 @@ def test_clone( assert old_repo.exists() -@pytest.mark.usefixtures('remote') +@pytest.mark.usefixtures("remote") @pytest.mark.parametrize( - 'bs_exists, bs_param, answer', [ - (False, '--bootstrap', None), - (True, '--bootstrap', None), - (True, '--no-bootstrap', None), - (True, None, 'n'), - (True, None, 'y'), - ], ids=[ - 'force, missing', - 'force, existing', - 'prevent', - 'existing, answer n', - 'existing, answer y', - ]) -def test_clone_bootstrap( - runner, paths, yadm_cmd, repo_config, bs_exists, bs_param, answer): + "bs_exists, bs_param, answer", + [ + (False, "--bootstrap", None), + (True, "--bootstrap", None), + (True, "--no-bootstrap", None), + (True, None, "n"), + (True, None, "y"), + ], + ids=[ + "force, missing", + "force, existing", + "prevent", + "existing, answer n", + "existing, answer y", + ], +) +def test_clone_bootstrap(runner, paths, yadm_cmd, repo_config, bs_exists, bs_param, answer): """Test bootstrap clone features""" # establish a bootstrap create_bootstrap(paths, bs_exists) # run the clone command - args = ['clone', '-w', paths.work] + args = ["clone", "-w", paths.work] if bs_param: args += [bs_param] - args += [f'file://{paths.remote}'] + args += [f"file://{paths.remote}"] expect = [] if answer: - expect.append(('Would you like to execute it now', answer)) + expect.append(("Would you like to execute it now", answer)) run = runner(command=yadm_cmd(*args), expect=expect) if answer: - assert 'Would you like to execute it now' in run.out + assert "Would you like to execute it now" in run.out expected_code = 0 - if bs_exists and bs_param != '--no-bootstrap': + if bs_exists and bs_param != "--no-bootstrap": expected_code = BOOTSTRAP_CODE - if answer == 'y': + if answer == "y": expected_code = BOOTSTRAP_CODE assert BOOTSTRAP_MSG in run.out - elif answer == 'n': + elif answer == "n": expected_code = 0 assert BOOTSTRAP_MSG not in run.out assert successful_clone(run, paths, repo_config, expected_code) - verify_head(paths, 'master') + verify_head(paths, "master") if not bs_exists: assert BOOTSTRAP_MSG not in run.out @@ -166,108 +166,90 @@ def test_clone_bootstrap( def create_bootstrap(paths, exists): """Create bootstrap file for test""" if exists: - paths.bootstrap.write( - '#!/bin/sh\n' - f'echo {BOOTSTRAP_MSG}\n' - f'exit {BOOTSTRAP_CODE}\n') + paths.bootstrap.write("#!/bin/sh\n" f"echo {BOOTSTRAP_MSG}\n" f"exit {BOOTSTRAP_CODE}\n") paths.bootstrap.chmod(0o775) assert paths.bootstrap.exists() else: assert not paths.bootstrap.exists() -@pytest.mark.usefixtures('remote') +@pytest.mark.usefixtures("remote") @pytest.mark.parametrize( - 'private_type, in_repo, in_work', [ - ('ssh', False, True), - ('gnupg', False, True), - ('ssh', True, True), - ('gnupg', True, True), - ('ssh', True, False), - ('gnupg', True, False), - ], ids=[ - 'open ssh, not tracked', - 'open gnupg, not tracked', - 'open ssh, tracked', - 'open gnupg, tracked', - 'missing ssh, tracked', - 'missing gnupg, tracked', - ]) -def test_clone_perms( - runner, yadm_cmd, paths, repo_config, - private_type, in_repo, in_work): + "private_type, in_repo, in_work", + [ + ("ssh", False, True), + ("gnupg", False, True), + ("ssh", True, True), + ("gnupg", True, True), + ("ssh", True, False), + ("gnupg", True, False), + ], + ids=[ + "open ssh, not tracked", + "open gnupg, not tracked", + "open ssh, tracked", + "open gnupg, tracked", + "missing ssh, tracked", + "missing gnupg, tracked", + ], +) +def test_clone_perms(runner, yadm_cmd, paths, repo_config, private_type, in_repo, in_work): """Test clone permission-related functions""" # update remote repo to include private data if in_repo: - rpath = paths.work.mkdir(f'.{private_type}').join('related') - rpath.write('related') + rpath = paths.work.mkdir(f".{private_type}").join("related") + rpath.write("related") os.system(f'GIT_DIR="{paths.remote}" git add {rpath}') os.system(f'GIT_DIR="{paths.remote}" git commit -m "{rpath}"') rpath.remove() # ensure local private data is insecure at the start if in_work: - pdir = paths.work.join(f'.{private_type}') + pdir = paths.work.join(f".{private_type}") if not pdir.exists(): pdir.mkdir() - pfile = pdir.join('existing') - pfile.write('existing') + pfile = pdir.join("existing") + pfile.write("existing") pdir.chmod(0o777) pfile.chmod(0o777) else: paths.work.remove() paths.work.mkdir() - env = {'HOME': paths.work} - run = runner( - yadm_cmd('clone', '-d', '-w', paths.work, f'file://{paths.remote}'), - env=env - ) + env = {"HOME": paths.work} + run = runner(yadm_cmd("clone", "-d", "-w", paths.work, f"file://{paths.remote}"), env=env) assert successful_clone(run, paths, repo_config) - verify_head(paths, 'master') + verify_head(paths, "master") if in_work: # private directories which already exist, should be left as they are, # which in this test is "insecure". - assert re.search( - f'initial private dir perms drwxrwxrwx.+.{private_type}', - run.out) - assert re.search( - f'pre-checkout private dir perms drwxrwxrwx.+.{private_type}', - run.out) - assert re.search( - f'post-checkout private dir perms drwxrwxrwx.+.{private_type}', - run.out) + assert re.search(f"initial private dir perms drwxrwxrwx.+.{private_type}", run.out) + assert re.search(f"pre-checkout private dir perms drwxrwxrwx.+.{private_type}", run.out) + assert re.search(f"post-checkout private dir perms drwxrwxrwx.+.{private_type}", run.out) else: # private directories which are created, should be done prior to # checkout, and with secure permissions. - assert 'initial private dir perms' not in run.out - assert re.search( - f'pre-checkout private dir perms drwx------.+.{private_type}', - run.out) - assert re.search( - f'post-checkout private dir perms drwx------.+.{private_type}', - run.out) + assert "initial private dir perms" not in run.out + assert re.search(f"pre-checkout private dir perms drwx------.+.{private_type}", run.out) + assert re.search(f"post-checkout private dir perms drwx------.+.{private_type}", run.out) # standard perms still apply afterwards unless disabled with auto.perms - assert oct( - paths.work.join(f'.{private_type}').stat().mode).endswith('00'), ( - f'.{private_type} has not been secured by auto.perms') + assert oct(paths.work.join(f".{private_type}").stat().mode).endswith( + "00" + ), f".{private_type} has not been secured by auto.perms" -@pytest.mark.usefixtures('remote') -@pytest.mark.parametrize( - 'branch', ['master', 'default', 'valid', 'invalid']) +@pytest.mark.usefixtures("remote") +@pytest.mark.parametrize("branch", ["master", "default", "valid", "invalid"]) def test_alternate_branch(runner, paths, yadm_cmd, repo_config, branch): """Test cloning a branch other than master""" # add a "valid" branch to the remote os.system(f'GIT_DIR="{paths.remote}" git checkout -b valid') - os.system( - f'GIT_DIR="{paths.remote}" git commit ' - f'--allow-empty -m "This branch is valid"') - if branch != 'default': + os.system(f'GIT_DIR="{paths.remote}" git commit ' f'--allow-empty -m "This branch is valid"') + if branch != "default": # When branch == 'default', the "default" branch of the remote repo # will remain "valid" to validate identification the correct default # branch by inspecting the repo. Otherwise it will be set back to @@ -278,45 +260,43 @@ def test_alternate_branch(runner, paths, yadm_cmd, repo_config, branch): paths.work.remove() paths.work.mkdir() - remote_url = f'file://{paths.remote}' + remote_url = f"file://{paths.remote}" # run the clone command - args = ['clone', '-w', paths.work] - if branch not in ['master', 'default']: - args += ['-b', branch] + args = ["clone", "-w", paths.work] + if branch not in ["master", "default"]: + args += ["-b", branch] args += [remote_url] run = runner(command=yadm_cmd(*args)) - if branch == 'invalid': + if branch == "invalid": assert run.failure - assert 'ERROR: Unable to clone the repository' in run.err + assert "ERROR: Unable to clone the repository" in run.err assert f"Remote branch {branch} not found in upstream" in run.err else: assert successful_clone(run, paths, repo_config) # confirm correct Git origin - run = runner( - command=('git', 'remote', '-v', 'show'), - env={'GIT_DIR': paths.repo}) + run = runner(command=("git", "remote", "-v", "show"), env={"GIT_DIR": paths.repo}) assert run.success - assert run.err == '' - assert f'origin\t{remote_url}' in run.out - run = runner(command=yadm_cmd('show')) - if branch == 'master': - assert 'Initial commit' in run.out - verify_head(paths, 'master') + assert run.err == "" + assert f"origin\t{remote_url}" in run.out + run = runner(command=yadm_cmd("show")) + if branch == "master": + assert "Initial commit" in run.out + verify_head(paths, "master") else: - assert 'This branch is valid' in run.out - verify_head(paths, 'valid') + assert "This branch is valid" in run.out + verify_head(paths, "valid") def successful_clone(run, paths, repo_config, expected_code=0): """Assert clone is successful""" assert run.code == expected_code - assert oct(paths.repo.stat().mode).endswith('00'), 'Repo is not secured' - assert repo_config('core.bare') == 'false' - assert repo_config('status.showUntrackedFiles') == 'no' - assert repo_config('yadm.managed') == 'true' + assert oct(paths.repo.stat().mode).endswith("00"), "Repo is not secured" + assert repo_config("core.bare") == "false" + assert repo_config("status.showUntrackedFiles") == "no" + assert repo_config("yadm.managed") == "true" return True @@ -331,15 +311,18 @@ def remote(paths, ds1_repo_copy): paths.repo.move(paths.remote) -def test_no_repo(runner, yadm_cmd, ): +def test_no_repo( + runner, + yadm_cmd, +): """Test cloning without specifying a repo""" - run = runner(command=yadm_cmd('clone', '-f')) + run = runner(command=yadm_cmd("clone", "-f")) assert run.failure - assert run.out == '' - assert 'ERROR: Unable to clone the repository' in run.err - assert 'repository \'repo.git\' does not exist' in run.err + assert run.out == "" + assert "ERROR: Unable to clone the repository" in run.err + assert "repository 'repo.git' does not exist" in run.err def verify_head(paths, branch): """Assert the local repo has the correct head branch""" - assert paths.repo.join('HEAD').read() == f'ref: refs/heads/{branch}\n' + assert paths.repo.join("HEAD").read() == f"ref: refs/heads/{branch}\n" diff --git a/test/test_config.py b/test/test_config.py index d364128..c43d81c 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -1,13 +1,14 @@ """Test config""" import os + import pytest -TEST_SECTION = 'test' -TEST_ATTRIBUTE = 'attribute' -TEST_KEY = f'{TEST_SECTION}.{TEST_ATTRIBUTE}' -TEST_VALUE = 'testvalue' -TEST_FILE = f'[{TEST_SECTION}]\n\t{TEST_ATTRIBUTE} = {TEST_VALUE}' +TEST_SECTION = "test" +TEST_ATTRIBUTE = "attribute" +TEST_KEY = f"{TEST_SECTION}.{TEST_ATTRIBUTE}" +TEST_VALUE = "testvalue" +TEST_FILE = f"[{TEST_SECTION}]\n\t{TEST_ATTRIBUTE} = {TEST_VALUE}" def test_config_no_params(runner, yadm_cmd, supported_configs): @@ -18,11 +19,11 @@ def test_config_no_params(runner, yadm_cmd, supported_configs): Exit with 0 """ - run = runner(yadm_cmd('config')) + run = runner(yadm_cmd("config")) assert run.success - assert run.err == '' - assert 'Please read the CONFIGURATION section' in run.out + assert run.err == "" + assert "Please read the CONFIGURATION section" in run.out for config in supported_configs: assert config in run.out @@ -34,11 +35,11 @@ def test_config_read_missing(runner, yadm_cmd): Exit with 0 """ - run = runner(yadm_cmd('config', TEST_KEY)) + run = runner(yadm_cmd("config", TEST_KEY)) assert run.success - assert run.err == '' - assert run.out == '' + assert run.err == "" + assert run.out == "" def test_config_write(runner, yadm_cmd, paths): @@ -49,11 +50,11 @@ def test_config_write(runner, yadm_cmd, paths): Exit with 0 """ - run = runner(yadm_cmd('config', TEST_KEY, TEST_VALUE)) + run = runner(yadm_cmd("config", TEST_KEY, TEST_VALUE)) assert run.success - assert run.err == '' - assert run.out == '' + assert run.err == "" + assert run.out == "" assert paths.config.read().strip() == TEST_FILE @@ -65,10 +66,10 @@ def test_config_read(runner, yadm_cmd, paths): """ paths.config.write(TEST_FILE) - run = runner(yadm_cmd('config', TEST_KEY)) + run = runner(yadm_cmd("config", TEST_KEY)) assert run.success - assert run.err == '' + assert run.err == "" assert run.out.strip() == TEST_VALUE @@ -82,16 +83,16 @@ def test_config_update(runner, yadm_cmd, paths): paths.config.write(TEST_FILE) - run = runner(yadm_cmd('config', TEST_KEY, TEST_VALUE + 'extra')) + run = runner(yadm_cmd("config", TEST_KEY, TEST_VALUE + "extra")) assert run.success - assert run.err == '' - assert run.out == '' + assert run.err == "" + assert run.out == "" - assert paths.config.read().strip() == TEST_FILE + 'extra' + assert paths.config.read().strip() == TEST_FILE + "extra" -@pytest.mark.usefixtures('ds1_repo_copy') +@pytest.mark.usefixtures("ds1_repo_copy") def test_config_local_read(runner, yadm_cmd, paths, supported_local_configs): """Read local attribute @@ -101,19 +102,17 @@ def test_config_local_read(runner, yadm_cmd, paths, supported_local_configs): # populate test values for config in supported_local_configs: - os.system( - f'GIT_DIR="{paths.repo}" ' - f'git config --local "{config}" "value_of_{config}"') + os.system(f'GIT_DIR="{paths.repo}" ' f'git config --local "{config}" "value_of_{config}"') # run yadm config for config in supported_local_configs: - run = runner(yadm_cmd('config', config)) + run = runner(yadm_cmd("config", config)) assert run.success - assert run.err == '' - assert run.out.strip() == f'value_of_{config}' + assert run.err == "" + assert run.out.strip() == f"value_of_{config}" -@pytest.mark.usefixtures('ds1_repo_copy') +@pytest.mark.usefixtures("ds1_repo_copy") def test_config_local_write(runner, yadm_cmd, paths, supported_local_configs): """Write local attribute @@ -124,40 +123,37 @@ def test_config_local_write(runner, yadm_cmd, paths, supported_local_configs): # run yadm config for config in supported_local_configs: - run = runner(yadm_cmd('config', config, f'value_of_{config}')) + run = runner(yadm_cmd("config", config, f"value_of_{config}")) assert run.success - assert run.err == '' - assert run.out == '' + assert run.err == "" + assert run.out == "" # verify test values for config in supported_local_configs: - run = runner( - command=('git', 'config', config), - env={'GIT_DIR': paths.repo}) + run = runner(command=("git", "config", config), env={"GIT_DIR": paths.repo}) assert run.success - assert run.err == '' - assert run.out.strip() == f'value_of_{config}' + assert run.err == "" + assert run.out.strip() == f"value_of_{config}" def test_config_without_parent_directory(runner, yadm_cmd, paths): - """Write and read attribute to/from config file with non-existent parent dir + """Write/read attribute to/from config file with non-existent parent dir Update configuration file Display value Exit with 0 """ - config_file = paths.root + '/folder/does/not/exist/config' + config_file = paths.root + "/folder/does/not/exist/config" - run = runner( - yadm_cmd('--yadm-config', config_file, 'config', TEST_KEY, TEST_VALUE)) + run = runner(yadm_cmd("--yadm-config", config_file, "config", TEST_KEY, TEST_VALUE)) assert run.success - assert run.err == '' - assert run.out == '' + assert run.err == "" + assert run.out == "" - run = runner(yadm_cmd('--yadm-config', config_file, 'config', TEST_KEY)) + run = runner(yadm_cmd("--yadm-config", config_file, "config", TEST_KEY)) assert run.success - assert run.err == '' + assert run.err == "" assert run.out.strip() == TEST_VALUE diff --git a/test/test_encryption.py b/test/test_encryption.py index 73f4c35..8c64222 100644 --- a/test/test_encryption.py +++ b/test/test_encryption.py @@ -1,30 +1,31 @@ """Test encryption""" import os -import pipes +import shlex import time + import pytest -KEY_FILE = 'test/test_key' -KEY_FINGERPRINT = 'F8BBFC746C58945442349BCEBA54FFD04C599B1A' -KEY_NAME = 'yadm-test1' -KEY_TRUST = 'test/ownertrust.txt' -PASSPHRASE = 'ExamplePassword' +KEY_FILE = "test/test_key" +KEY_FINGERPRINT = "F8BBFC746C58945442349BCEBA54FFD04C599B1A" +KEY_NAME = "yadm-test1" +KEY_TRUST = "test/ownertrust.txt" +PASSPHRASE = "ExamplePassword" -pytestmark = pytest.mark.usefixtures('config_git') +pytestmark = pytest.mark.usefixtures("config_git") def add_asymmetric_key(runner, gnupg): """Add asymmetric key""" env = os.environ.copy() - env['GNUPGHOME'] = gnupg.home + env["GNUPGHOME"] = gnupg.home runner( - ['gpg', '--import', pipes.quote(KEY_FILE)], + ["gpg", "--import", shlex.quote(KEY_FILE)], env=env, shell=True, ) runner( - ['gpg', '--import-ownertrust', '<', pipes.quote(KEY_TRUST)], + ["gpg", "--import-ownertrust", "<", shlex.quote(KEY_TRUST)], env=env, shell=True, ) @@ -33,20 +34,14 @@ def add_asymmetric_key(runner, gnupg): def remove_asymmetric_key(runner, gnupg): """Remove asymmetric key""" env = os.environ.copy() - env['GNUPGHOME'] = gnupg.home + env["GNUPGHOME"] = gnupg.home runner( - [ - 'gpg', '--batch', '--yes', - '--delete-secret-keys', pipes.quote(KEY_FINGERPRINT) - ], + ["gpg", "--batch", "--yes", "--delete-secret-keys", shlex.quote(KEY_FINGERPRINT)], env=env, shell=True, ) runner( - [ - 'gpg', '--batch', '--yes', - '--delete-key', pipes.quote(KEY_FINGERPRINT) - ], + ["gpg", "--batch", "--yes", "--delete-key", shlex.quote(KEY_FINGERPRINT)], env=env, shell=True, ) @@ -78,48 +73,48 @@ def encrypt_targets(yadm_cmd, paths): """ # init empty yadm repo - os.system(' '.join(yadm_cmd('init', '-w', str(paths.work), '-f'))) + os.system(" ".join(yadm_cmd("init", "-w", str(paths.work), "-f"))) expected = [] # standard files w/ dirs & spaces - paths.work.join('inc file1').write('inc file1') - expected.append('inc file1') - paths.encrypt.write('inc file1\n') - paths.work.join('inc dir').mkdir() - paths.work.join('inc dir/inc file2').write('inc file2') - expected.append('inc dir/inc file2') - paths.encrypt.write('inc dir/inc file2\n', mode='a') + paths.work.join("inc file1").write("inc file1") + expected.append("inc file1") + paths.encrypt.write("inc file1\n") + paths.work.join("inc dir").mkdir() + paths.work.join("inc dir/inc file2").write("inc file2") + expected.append("inc dir/inc file2") + paths.encrypt.write("inc dir/inc file2\n", mode="a") # standard globs w/ dirs & spaces - paths.work.join('globs file1').write('globs file1') - expected.append('globs file1') - paths.work.join('globs dir').mkdir() - paths.work.join('globs dir/globs file2').write('globs file2') - expected.append('globs dir/globs file2') - paths.encrypt.write('globs*\n', mode='a') + paths.work.join("globs file1").write("globs file1") + expected.append("globs file1") + paths.work.join("globs dir").mkdir() + paths.work.join("globs dir/globs file2").write("globs file2") + expected.append("globs dir/globs file2") + paths.encrypt.write("globs*\n", mode="a") # blank lines - paths.encrypt.write('\n \n\t\n', mode='a') + paths.encrypt.write("\n \n\t\n", mode="a") # comments - paths.work.join('commentfile1').write('commentfile1') - paths.encrypt.write('#commentfile1\n', mode='a') - paths.encrypt.write(' #commentfile1\n', mode='a') + paths.work.join("commentfile1").write("commentfile1") + paths.encrypt.write("#commentfile1\n", mode="a") + paths.encrypt.write(" #commentfile1\n", mode="a") # exclusions - paths.work.join('extest').mkdir() - paths.encrypt.write('extest/*\n', mode='a') # include within extest - paths.work.join('extest/inglob1').write('inglob1') - paths.work.join('extest/exglob1').write('exglob1') - paths.work.join('extest/exglob2').write('exglob2') - paths.encrypt.write('!extest/ex*\n', mode='a') # exclude the ex* - expected.append('extest/inglob1') # should be left with only in* + paths.work.join("extest").mkdir() + paths.encrypt.write("extest/*\n", mode="a") # include within extest + paths.work.join("extest/inglob1").write("inglob1") + paths.work.join("extest/exglob1").write("exglob1") + paths.work.join("extest/exglob2").write("exglob2") + paths.encrypt.write("!extest/ex*\n", mode="a") # exclude the ex* + expected.append("extest/inglob1") # should be left with only in* return expected -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def decrypt_targets(tmpdir_factory, runner, gnupg): """Fixture for setting data to decrypt @@ -128,234 +123,203 @@ def decrypt_targets(tmpdir_factory, runner, gnupg): * creates a list of expected decrypted files """ - tmpdir = tmpdir_factory.mktemp('decrypt_targets') - symmetric = tmpdir.join('symmetric.tar.gz.gpg') - asymmetric = tmpdir.join('asymmetric.tar.gz.gpg') + tmpdir = tmpdir_factory.mktemp("decrypt_targets") + symmetric = tmpdir.join("symmetric.tar.gz.gpg") + asymmetric = tmpdir.join("asymmetric.tar.gz.gpg") expected = [] - tmpdir.join('decrypt1').write('decrypt1') - expected.append('decrypt1') - tmpdir.join('decrypt2').write('decrypt2') - expected.append('decrypt2') - tmpdir.join('subdir').mkdir() - tmpdir.join('subdir/decrypt3').write('subdir/decrypt3') - expected.append('subdir/decrypt3') + tmpdir.join("decrypt1").write("decrypt1") + expected.append("decrypt1") + tmpdir.join("decrypt2").write("decrypt2") + expected.append("decrypt2") + tmpdir.join("subdir").mkdir() + tmpdir.join("subdir/decrypt3").write("subdir/decrypt3") + expected.append("subdir/decrypt3") gnupg.pw(PASSPHRASE) env = os.environ.copy() - env['GNUPGHOME'] = gnupg.home + env["GNUPGHOME"] = gnupg.home run = runner( - ['tar', 'cvf', '-'] + - expected + - ['|', 'gpg', '--batch', '--yes', '-c'] + - ['--output', pipes.quote(str(symmetric))], + ["tar", "cvf", "-"] + + expected + + ["|", "gpg", "--batch", "--yes", "-c"] + + ["--output", shlex.quote(str(symmetric))], cwd=tmpdir, env=env, - shell=True) + shell=True, + ) assert run.success - gnupg.pw('') + gnupg.pw("") add_asymmetric_key(runner, gnupg) run = runner( - ['tar', 'cvf', '-'] + - expected + - ['|', 'gpg', '--batch', '--yes', '-e'] + - ['-r', pipes.quote(KEY_NAME)] + - ['--output', pipes.quote(str(asymmetric))], + ["tar", "cvf", "-"] + + expected + + ["|", "gpg", "--batch", "--yes", "-e"] + + ["-r", shlex.quote(KEY_NAME)] + + ["--output", shlex.quote(str(asymmetric))], cwd=tmpdir, env=env, - shell=True) + shell=True, + ) assert run.success remove_asymmetric_key(runner, gnupg) return { - 'asymmetric': asymmetric, - 'expected': expected, - 'symmetric': symmetric, + "asymmetric": asymmetric, + "expected": expected, + "symmetric": symmetric, } -@pytest.mark.parametrize( - 'bad_phrase', [False, True], - ids=['good_phrase', 'bad_phrase']) -@pytest.mark.parametrize( - 'missing_encrypt', [False, True], - ids=['encrypt_exists', 'encrypt_missing']) -@pytest.mark.parametrize( - 'overwrite', [False, True], - ids=['clean', 'overwrite']) -def test_symmetric_encrypt( - runner, yadm_cmd, paths, encrypt_targets, - gnupg, bad_phrase, overwrite, missing_encrypt): +@pytest.mark.parametrize("bad_phrase", [False, True], ids=["good_phrase", "bad_phrase"]) +@pytest.mark.parametrize("missing_encrypt", [False, True], ids=["encrypt_exists", "encrypt_missing"]) +@pytest.mark.parametrize("overwrite", [False, True], ids=["clean", "overwrite"]) +def test_symmetric_encrypt(runner, yadm_cmd, paths, encrypt_targets, gnupg, bad_phrase, overwrite, missing_encrypt): """Test symmetric encryption""" if missing_encrypt: paths.encrypt.remove() if bad_phrase: - gnupg.pw('') + gnupg.pw("") else: gnupg.pw(PASSPHRASE) if overwrite: - paths.archive.write('existing archive') + paths.archive.write("existing archive") env = os.environ.copy() - env['GNUPGHOME'] = gnupg.home - run = runner(yadm_cmd('encrypt'), env=env) + env["GNUPGHOME"] = gnupg.home + run = runner(yadm_cmd("encrypt"), env=env) if missing_encrypt or bad_phrase: assert run.failure else: assert run.success - assert run.err == '' + assert run.err == "" if missing_encrypt: - assert 'does not exist' in run.err + assert "does not exist" in run.err elif bad_phrase: - assert 'Invalid passphrase' in run.err + assert "Invalid IPC" in run.err else: - assert encrypted_data_valid( - runner, gnupg, paths.archive, encrypt_targets) + assert encrypted_data_valid(runner, gnupg, paths.archive, encrypt_targets) -@pytest.mark.parametrize( - 'bad_phrase', [False, True], - ids=['good_phrase', 'bad_phrase']) -@pytest.mark.parametrize( - 'archive_exists', [True, False], - ids=['archive_exists', 'archive_missing']) -@pytest.mark.parametrize( - 'dolist', [False, True], - ids=['decrypt', 'list']) -def test_symmetric_decrypt( - runner, yadm_cmd, paths, decrypt_targets, gnupg, - dolist, archive_exists, bad_phrase): +@pytest.mark.parametrize("bad_phrase", [False, True], ids=["good_phrase", "bad_phrase"]) +@pytest.mark.parametrize("archive_exists", [True, False], ids=["archive_exists", "archive_missing"]) +@pytest.mark.parametrize("dolist", [False, True], ids=["decrypt", "list"]) +def test_symmetric_decrypt(runner, yadm_cmd, paths, decrypt_targets, gnupg, dolist, archive_exists, bad_phrase): """Test decryption""" # init empty yadm repo - os.system(' '.join(yadm_cmd('init', '-w', str(paths.work), '-f'))) + os.system(" ".join(yadm_cmd("init", "-w", str(paths.work), "-f"))) if bad_phrase: - gnupg.pw('') + gnupg.pw("") time.sleep(1) # allow gpg-agent cache to expire else: gnupg.pw(PASSPHRASE) if archive_exists: - decrypt_targets['symmetric'].copy(paths.archive) + decrypt_targets["symmetric"].copy(paths.archive) # to test overwriting - paths.work.join('decrypt1').write('pre-existing file') + paths.work.join("decrypt1").write("pre-existing file") env = os.environ.copy() - env['GNUPGHOME'] = gnupg.home + env["GNUPGHOME"] = gnupg.home args = [] if dolist: - args.append('-l') - run = runner(yadm_cmd('decrypt') + args, env=env) + args.append("-l") + run = runner(yadm_cmd("decrypt") + args, env=env) if archive_exists and not bad_phrase: assert run.success - assert 'encrypted with 1 passphrase' in run.err + assert "encrypted with 1 passphrase" in run.err if dolist: - for filename in decrypt_targets['expected']: - if filename != 'decrypt1': # this one should exist + for filename in decrypt_targets["expected"]: + if filename != "decrypt1": # this one should exist assert not paths.work.join(filename).exists() assert filename in run.out else: - for filename in decrypt_targets['expected']: + for filename in decrypt_targets["expected"]: assert paths.work.join(filename).read() == filename else: assert run.failure -@pytest.mark.usefixtures('asymmetric_key') -@pytest.mark.parametrize( - 'ask', [False, True], - ids=['no_ask', 'ask']) -@pytest.mark.parametrize( - 'key_exists', [True, False], - ids=['key_exists', 'key_missing']) -@pytest.mark.parametrize( - 'overwrite', [False, True], - ids=['clean', 'overwrite']) -def test_asymmetric_encrypt( - runner, yadm_cmd, paths, encrypt_targets, gnupg, - overwrite, key_exists, ask): +@pytest.mark.usefixtures("asymmetric_key") +@pytest.mark.parametrize("ask", [False, True], ids=["no_ask", "ask"]) +@pytest.mark.parametrize("key_exists", [True, False], ids=["key_exists", "key_missing"]) +@pytest.mark.parametrize("overwrite", [False, True], ids=["clean", "overwrite"]) +def test_asymmetric_encrypt(runner, yadm_cmd, paths, encrypt_targets, gnupg, overwrite, key_exists, ask): """Test asymmetric encryption""" # specify encryption recipient if ask: - os.system(' '.join(yadm_cmd('config', 'yadm.gpg-recipient', 'ASK'))) - expect = [('Enter the user ID', KEY_NAME), ('Enter the user ID', '')] + os.system(" ".join(yadm_cmd("config", "yadm.gpg-recipient", "ASK"))) + expect = [("Enter the user ID", KEY_NAME), ("Enter the user ID", "")] else: - os.system(' '.join(yadm_cmd('config', 'yadm.gpg-recipient', KEY_NAME))) + os.system(" ".join(yadm_cmd("config", "yadm.gpg-recipient", KEY_NAME))) expect = [] if overwrite: - paths.archive.write('existing archive') + paths.archive.write("existing archive") if not key_exists: remove_asymmetric_key(runner, gnupg) env = os.environ.copy() - env['GNUPGHOME'] = gnupg.home + env["GNUPGHOME"] = gnupg.home - run = runner(yadm_cmd('encrypt'), env=env, expect=expect) + run = runner(yadm_cmd("encrypt"), env=env, expect=expect) if key_exists: assert run.success - assert encrypted_data_valid( - runner, gnupg, paths.archive, encrypt_targets) + assert encrypted_data_valid(runner, gnupg, paths.archive, encrypt_targets) else: assert run.failure - assert 'Unable to write' in run.out if expect else run.err + assert "Unable to write" in run.out if expect else run.err if ask: - assert 'Enter the user ID' in run.out + assert "Enter the user ID" in run.out -@pytest.mark.usefixtures('asymmetric_key') -@pytest.mark.usefixtures('encrypt_targets') +@pytest.mark.usefixtures("asymmetric_key") +@pytest.mark.usefixtures("encrypt_targets") def test_multi_key(runner, yadm_cmd, gnupg): """Test multiple recipients""" # specify two encryption recipient - os.system(' '.join(yadm_cmd( - 'config', 'yadm.gpg-recipient', f'"second-key {KEY_NAME}"'))) + os.system(" ".join(yadm_cmd("config", "yadm.gpg-recipient", f'"second-key {KEY_NAME}"'))) env = os.environ.copy() - env['GNUPGHOME'] = gnupg.home + env["GNUPGHOME"] = gnupg.home - run = runner(yadm_cmd('encrypt'), env=env) + run = runner(yadm_cmd("encrypt"), env=env) assert run.failure - assert 'second-key: skipped: No public key' in run.err + assert "second-key: skipped: No public key" in run.err -@pytest.mark.usefixtures('asymmetric_key') -@pytest.mark.parametrize( - 'key_exists', [True, False], - ids=['key_exists', 'key_missing']) -@pytest.mark.parametrize( - 'dolist', [False, True], - ids=['decrypt', 'list']) -def test_asymmetric_decrypt( - runner, yadm_cmd, paths, decrypt_targets, gnupg, - dolist, key_exists): +@pytest.mark.usefixtures("asymmetric_key") +@pytest.mark.parametrize("key_exists", [True, False], ids=["key_exists", "key_missing"]) +@pytest.mark.parametrize("dolist", [False, True], ids=["decrypt", "list"]) +def test_asymmetric_decrypt(runner, yadm_cmd, paths, decrypt_targets, gnupg, dolist, key_exists): """Test decryption""" # init empty yadm repo - os.system(' '.join(yadm_cmd('init', '-w', str(paths.work), '-f'))) + os.system(" ".join(yadm_cmd("init", "-w", str(paths.work), "-f"))) - decrypt_targets['asymmetric'].copy(paths.archive) + decrypt_targets["asymmetric"].copy(paths.archive) # to test overwriting - paths.work.join('decrypt1').write('pre-existing file') + paths.work.join("decrypt1").write("pre-existing file") if not key_exists: remove_asymmetric_key(runner, gnupg) @@ -363,32 +327,28 @@ def test_asymmetric_decrypt( args = [] if dolist: - args.append('-l') + args.append("-l") env = os.environ.copy() - env['GNUPGHOME'] = gnupg.home - run = runner(yadm_cmd('decrypt') + args, env=env) + env["GNUPGHOME"] = gnupg.home + run = runner(yadm_cmd("decrypt") + args, env=env) if key_exists: assert run.success if dolist: - for filename in decrypt_targets['expected']: - if filename != 'decrypt1': # this one should exist + for filename in decrypt_targets["expected"]: + if filename != "decrypt1": # this one should exist assert not paths.work.join(filename).exists() assert filename in run.out else: - for filename in decrypt_targets['expected']: + for filename in decrypt_targets["expected"]: assert paths.work.join(filename).read() == filename else: assert run.failure - assert 'Unable to extract encrypted files' in run.err + assert "Unable to extract encrypted files" in run.err -@pytest.mark.parametrize( - 'untracked', - [False, 'y', 'n'], - ids=['tracked', 'untracked_answer_y', 'untracked_answer_n']) -def test_offer_to_add( - runner, yadm_cmd, paths, encrypt_targets, gnupg, untracked): +@pytest.mark.parametrize("untracked", [False, "y", "n"], ids=["tracked", "untracked_answer_y", "untracked_answer_n"]) +def test_offer_to_add(runner, yadm_cmd, paths, encrypt_targets, gnupg, untracked): """Test offer to add encrypted archive All the other encryption tests use an archive outside of the work tree. @@ -396,85 +356,75 @@ def test_offer_to_add( should be an offer to add it to the repo if it is not tracked. """ - worktree_archive = paths.work.join('worktree-archive.tar.gpg') + worktree_archive = paths.work.join("worktree-archive.tar.gpg") expect = [] gnupg.pw(PASSPHRASE) env = os.environ.copy() - env['GNUPGHOME'] = gnupg.home + env["GNUPGHOME"] = gnupg.home if untracked: - expect.append(('add it now', untracked)) + expect.append(("add it now", untracked)) else: - worktree_archive.write('exists') - os.system(' '.join(yadm_cmd('add', str(worktree_archive)))) + worktree_archive.write("exists") + os.system(" ".join(yadm_cmd("add", str(worktree_archive)))) - run = runner( - yadm_cmd('encrypt', '--yadm-archive', str(worktree_archive)), - env=env, - expect=expect - ) + run = runner(yadm_cmd("encrypt", "--yadm-archive", str(worktree_archive)), env=env, expect=expect) assert run.success - assert run.err == '' - assert encrypted_data_valid( - runner, gnupg, worktree_archive, encrypt_targets) + assert run.err == "" + assert encrypted_data_valid(runner, gnupg, worktree_archive, encrypt_targets) - run = runner( - yadm_cmd('status', '--porcelain', '-uall', str(worktree_archive))) + run = runner(yadm_cmd("status", "--porcelain", "-uall", str(worktree_archive))) assert run.success - assert run.err == '' + assert run.err == "" - if untracked == 'y': + if untracked == "y": # should be added to the index - assert f'A {worktree_archive.basename}' in run.out - elif untracked == 'n': + assert f"A {worktree_archive.basename}" in run.out + elif untracked == "n": # should NOT be added to the index - assert f'?? {worktree_archive.basename}' in run.out + assert f"?? {worktree_archive.basename}" in run.out else: # should appear modified in the index - assert f'AM {worktree_archive.basename}' in run.out + assert f"AM {worktree_archive.basename}" in run.out -@pytest.mark.usefixtures('ds1_copy') +@pytest.mark.usefixtures("ds1_copy") def test_encrypt_added_to_exclude(runner, yadm_cmd, paths, gnupg): """Confirm that .config/yadm/encrypt is added to exclude""" gnupg.pw(PASSPHRASE) env = os.environ.copy() - env['GNUPGHOME'] = gnupg.home + env["GNUPGHOME"] = gnupg.home - exclude_file = paths.repo.join('info/exclude') - paths.encrypt.write('test-encrypt-data\n') - paths.work.join('test-encrypt-data').write('') - exclude_file.write('original-data', ensure=True) + exclude_file = paths.repo.join("info/exclude") + paths.encrypt.write("test-encrypt-data\n") + paths.work.join("test-encrypt-data").write("") + exclude_file.write("original-data", ensure=True) - run = runner(yadm_cmd('encrypt'), env=env) + run = runner(yadm_cmd("encrypt"), env=env) - assert 'test-encrypt-data' in paths.repo.join('info/exclude').read() - assert 'original-data' in paths.repo.join('info/exclude').read() + assert "test-encrypt-data" in paths.repo.join("info/exclude").read() + assert "original-data" in paths.repo.join("info/exclude").read() assert run.success - assert run.err == '' + assert run.err == "" def encrypted_data_valid(runner, gnupg, encrypted, expected): """Verify encrypted data matches expectations""" gnupg.pw(PASSPHRASE) env = os.environ.copy() - env['GNUPGHOME'] = gnupg.home - run = runner([ - 'gpg', - '-d', pipes.quote(str(encrypted)), - '2>/dev/null', - '|', 'tar', 't'], env=env, shell=True, report=False) + env["GNUPGHOME"] = gnupg.home + run = runner( + ["gpg", "-d", shlex.quote(str(encrypted)), "2>/dev/null", "|", "tar", "t"], env=env, shell=True, report=False + ) file_count = 0 for filename in run.out.splitlines(): - if filename.endswith('/'): + if filename.endswith("/"): continue file_count += 1 - assert filename in expected, ( - f'Unexpected file in archive: {filename}') - assert file_count == len(expected), ( - 'Number of files in archive does not match expected') + assert filename in expected, f"Unexpected file in archive: {filename}" + assert file_count == len(expected), "Number of files in archive does not match expected" return True diff --git a/test/test_enter.py b/test/test_enter.py index f5ea2d8..b0626ae 100644 --- a/test/test_enter.py +++ b/test/test_enter.py @@ -1,110 +1,117 @@ """Test enter""" import os + import pytest @pytest.mark.parametrize( - 'shell, success', [ - ('delete', True), # if there is no shell variable, bash creates it - ('', False), - ('/usr/bin/env', True), - ('noexec', False), - ], ids=[ - 'shell-missing', - 'shell-empty', - 'shell-env', - 'shell-noexec', - ]) -@pytest.mark.usefixtures('ds1_copy') + "shell, success", + [ + ("delete", True), # if there is no shell variable, bash creates it + ("", False), + ("/usr/bin/env", True), + ("noexec", False), + ], + ids=[ + "shell-missing", + "shell-empty", + "shell-env", + "shell-noexec", + ], +) +@pytest.mark.usefixtures("ds1_copy") def test_enter(runner, yadm_cmd, paths, shell, success): """Enter tests""" env = os.environ.copy() - if shell == 'delete': + if shell == "delete": # remove shell - if 'SHELL' in env: - del env['SHELL'] - elif shell == 'noexec': + if "SHELL" in env: + del env["SHELL"] + elif shell == "noexec": # specify a non-executable path - noexec = paths.root.join('noexec') - noexec.write('') + noexec = paths.root.join("noexec") + noexec.write("") noexec.chmod(0o664) - env['SHELL'] = str(noexec) + env["SHELL"] = str(noexec) else: - env['SHELL'] = shell + env["SHELL"] = shell - run = runner(command=yadm_cmd('enter'), env=env) + run = runner(command=yadm_cmd("enter"), env=env) assert run.success == success - prompt = f'yadm shell ({paths.repo})' + prompt = f"yadm shell ({paths.repo})" if success: - assert run.out.startswith('Entering yadm repo') - assert run.out.rstrip().endswith('Leaving yadm repo') - assert run.err == '' + assert run.out.startswith("Entering yadm repo") + assert run.out.rstrip().endswith("Leaving yadm repo") + assert run.err == "" else: - assert 'does not refer to an executable' in run.err - if 'env' in shell: - assert f'GIT_DIR={paths.repo}' in run.out - assert f'GIT_WORK_TREE={paths.work}' in run.out - assert f'PROMPT={prompt}' in run.out - assert f'PS1={prompt}' in run.out + assert "does not refer to an executable" in run.err + if "env" in shell: + assert f"GIT_DIR={paths.repo}" in run.out + assert f"GIT_WORK_TREE={paths.work}" in run.out + assert f"PROMPT={prompt}" in run.out + assert f"PS1={prompt}" in run.out @pytest.mark.parametrize( - 'shell, opts, path', [ - ('bash', '--norc', '\\w'), - ('csh', '-f', '%~'), - ('zsh', '-f', '%~'), - ], ids=[ - 'bash', - 'csh', - 'zsh', - ]) + "shell, opts, path", + [ + ("bash", "--norc", "\\w"), + ("csh", "-f", "%~"), + ("zsh", "-f", "%~"), + ], + ids=[ + "bash", + "csh", + "zsh", + ], +) @pytest.mark.parametrize( - 'cmd', - [False, 'cmd', 'cmd-bad-exit'], - ids=['no-cmd', 'cmd', 'cmd-bad-exit']) + "cmd", + [False, "cmd", "cmd-bad-exit"], + ids=["no-cmd", "cmd", "cmd-bad-exit"], +) @pytest.mark.parametrize( - 'term', ['', 'dumb'], - ids=['term-empty', 'term-dumb']) -@pytest.mark.usefixtures('ds1_copy') -def test_enter_shell_ops(runner, yadm_cmd, paths, shell, - opts, path, cmd, term): + "term", + ["", "dumb"], + ids=["term-empty", "term-dumb"], +) +@pytest.mark.usefixtures("ds1_copy") +def test_enter_shell_ops(runner, yadm_cmd, paths, shell, opts, path, cmd, term): """Enter tests for specific shell options""" - change_exit = '\nfalse' if cmd == 'cmd-bad-exit' else '' + change_exit = "\nfalse" if cmd == "cmd-bad-exit" else "" # Create custom shell to detect options passed custom_shell = paths.root.join(shell) - custom_shell.write( - f'#!/bin/sh\necho OPTS=$*\necho PROMPT=$PROMPT{change_exit}' - ) + custom_shell.write(f"#!/bin/sh\necho OPTS=$*\necho PROMPT=$PROMPT{change_exit}") custom_shell.chmod(0o775) - test_cmd = ['test1', 'test2', 'test3'] + test_cmd = ["test1", "test2", "test3"] - enter_cmd = ['enter'] + enter_cmd = ["enter"] if cmd: enter_cmd += test_cmd env = os.environ.copy() - env['TERM'] = term - env['SHELL'] = custom_shell + env["TERM"] = term + env["SHELL"] = custom_shell - if shell == 'zsh' and term == 'dumb': - opts += ' --no-zle' + if shell == "zsh" and term == "dumb": + opts += " --no-zle" run = runner(command=yadm_cmd(*enter_cmd), env=env) - if cmd == 'cmd-bad-exit': + if cmd == "cmd-bad-exit": assert run.failure else: assert run.success - assert run.err == '' - assert f'OPTS={opts}' in run.out - assert f'PROMPT=yadm shell ({paths.repo}) {path} >' in run.out + assert run.err == "" + assert f"OPTS={opts}" in run.out + assert f"PROMPT=yadm shell ({paths.repo}) {path} >" in run.out if cmd: - assert '-c ' + ' '.join(test_cmd) in run.out - assert 'Entering yadm repo' not in run.out - assert 'Leaving yadm repo' not in run.out + assert "-c " + " ".join(test_cmd) in run.out + assert "Entering yadm repo" not in run.out + assert "Leaving yadm repo" not in run.out else: - assert 'Entering yadm repo' in run.out - assert 'Leaving yadm repo' in run.out + assert "Entering yadm repo" in run.out + assert "Leaving yadm repo" in run.out diff --git a/test/test_ext_crypt.py b/test/test_ext_crypt.py index cb74afc..92f703f 100644 --- a/test/test_ext_crypt.py +++ b/test/test_ext_crypt.py @@ -4,28 +4,30 @@ import pytest @pytest.mark.parametrize( - 'crypt', - [False, 'installed', 'installed-but-failed'], - ids=['not-installed', 'installed', 'installed-but-failed'] + "crypt", + [False, "installed", "installed-but-failed"], + ids=["not-installed", "installed", "installed-but-failed"], ) @pytest.mark.parametrize( - 'cmd,var', [ - ['git_crypt', 'GIT_CRYPT_PROGRAM'], - ['transcrypt', 'TRANSCRYPT_PROGRAM'], - ], - ids=['git-crypt', 'transcrypt']) + "cmd,var", + [ + ["git_crypt", "GIT_CRYPT_PROGRAM"], + ["transcrypt", "TRANSCRYPT_PROGRAM"], + ], + ids=["git-crypt", "transcrypt"], +) def test_ext_encryption(runner, yadm, paths, tmpdir, crypt, cmd, var): """External encryption tests""" paths.repo.ensure(dir=True) - bindir = tmpdir.mkdir('bin') - pgm = bindir.join('test-ext-crypt') + bindir = tmpdir.mkdir("bin") + pgm = bindir.join("test-ext-crypt") if crypt: - pgm.write('#!/bin/sh\necho ext-crypt ran\n') + pgm.write("#!/bin/sh\necho ext-crypt ran\n") pgm.chmod(0o775) - if crypt == 'installed-but-failed': - pgm.write('false\n', mode='a') + if crypt == "installed-but-failed": + pgm.write("false\n", mode="a") script = f""" YADM_TEST=1 source {yadm} @@ -34,15 +36,15 @@ def test_ext_encryption(runner, yadm, paths, tmpdir, crypt, cmd, var): {cmd} "param1" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) if crypt: - if crypt == 'installed-but-failed': + if crypt == "installed-but-failed": assert run.failure else: assert run.success - assert run.out.strip() == 'ext-crypt ran' - assert run.err == '' + assert run.out.strip() == "ext-crypt ran" + assert run.err == "" else: assert run.failure assert f"command '{pgm}' cannot be located" in run.err diff --git a/test/test_git.py b/test/test_git.py index 76eccab..dea538d 100644 --- a/test/test_git.py +++ b/test/test_git.py @@ -1,10 +1,11 @@ """Test git""" import re + import pytest -@pytest.mark.usefixtures('ds1_copy') +@pytest.mark.usefixtures("ds1_copy") def test_git(runner, yadm_cmd, paths): """Test series of passthrough git commands @@ -17,42 +18,42 @@ def test_git(runner, yadm_cmd, paths): """ # passthru unknown commands to Git - run = runner(command=yadm_cmd('bogus')) + run = runner(command=yadm_cmd("bogus")) assert run.failure assert "git: 'bogus' is not a git command." in run.err assert "See 'git --help'" in run.err - assert run.out == '' + assert run.out == "" # git command 'add' - badfile - run = runner(command=yadm_cmd('add', '-v', 'does_not_exist')) + run = runner(command=yadm_cmd("add", "-v", "does_not_exist")) assert run.code == 128 assert "pathspec 'does_not_exist' did not match any files" in run.err - assert run.out == '' + assert run.out == "" # git command 'add' - newfile = paths.work.join('test_git') - newfile.write('test_git') - run = runner(command=yadm_cmd('add', '-v', str(newfile))) + newfile = paths.work.join("test_git") + newfile.write("test_git") + run = runner(command=yadm_cmd("add", "-v", str(newfile))) assert run.success - assert run.err == '' + assert run.err == "" assert "add 'test_git'" in run.out # git command 'status' - run = runner(command=yadm_cmd('status')) + run = runner(command=yadm_cmd("status")) assert run.success - assert run.err == '' - assert re.search(r'new file:\s+test_git', run.out) + assert run.err == "" + assert re.search(r"new file:\s+test_git", run.out) # git command 'commit' - run = runner(command=yadm_cmd('commit', '-m', 'Add test_git')) + run = runner(command=yadm_cmd("commit", "-m", "Add test_git")) assert run.success - assert run.err == '' - assert '1 file changed' in run.out - assert '1 insertion' in run.out - assert re.search(r'create mode .+ test_git', run.out) + assert run.err == "" + assert "1 file changed" in run.out + assert "1 insertion" in run.out + assert re.search(r"create mode .+ test_git", run.out) # git command 'log' - run = runner(command=yadm_cmd('log', '--oneline')) + run = runner(command=yadm_cmd("log", "--oneline")) assert run.success - assert run.err == '' - assert 'Add test_git' in run.out + assert run.err == "" + assert "Add test_git" in run.out diff --git a/test/test_help.py b/test/test_help.py index 0d8f2c3..cbfabcc 100644 --- a/test/test_help.py +++ b/test/test_help.py @@ -6,14 +6,14 @@ def test_missing_command(runner, yadm_cmd): """Run without any command""" run = runner(command=yadm_cmd()) assert run.failure - assert run.err == '' - assert run.out.startswith('Usage: yadm') + assert run.err == "" + assert run.out.startswith("Usage: yadm") -@pytest.mark.parametrize('cmd', ['--help', 'help']) +@pytest.mark.parametrize("cmd", ["--help", "help"]) def test_help_command(runner, yadm_cmd, cmd): """Run with help command""" run = runner(command=yadm_cmd(cmd)) assert run.failure - assert run.err == '' - assert run.out.startswith('Usage: yadm') + assert run.err == "" + assert run.out.startswith("Usage: yadm") diff --git a/test/test_hooks.py b/test/test_hooks.py index 704636a..aff3daf 100644 --- a/test/test_hooks.py +++ b/test/test_hooks.py @@ -4,7 +4,8 @@ import pytest @pytest.mark.parametrize( - 'pre, pre_code, post, post_code', [ + "pre, pre_code, post, post_code", + [ (False, 0, False, 0), (True, 0, False, 0), (True, 5, False, 0), @@ -12,83 +13,83 @@ import pytest (False, 0, True, 5), (True, 0, True, 0), (True, 5, True, 5), - ], ids=[ - 'no-hooks', - 'pre-success', - 'pre-fail', - 'post-success', - 'post-fail', - 'pre-post-success', - 'pre-post-fail', - ]) -@pytest.mark.parametrize('cmd', ['--version', 'version']) -def test_hooks( - runner, yadm_cmd, paths, cmd, - pre, pre_code, post, post_code): + ], + ids=[ + "no-hooks", + "pre-success", + "pre-fail", + "post-success", + "post-fail", + "pre-post-success", + "pre-post-fail", + ], +) +@pytest.mark.parametrize("cmd", ["--version", "version"]) +def test_hooks(runner, yadm_cmd, paths, cmd, pre, pre_code, post, post_code): """Test pre/post hook""" # generate hooks if pre: - create_hook(paths, 'pre_version', pre_code) + create_hook(paths, "pre_version", pre_code) if post: - create_hook(paths, 'post_version', post_code) + create_hook(paths, "post_version", post_code) # run yadm run = runner(yadm_cmd(cmd)) # when a pre hook fails, yadm should exit with the hook's code assert run.code == pre_code - assert run.err == '' + assert run.err == "" if pre: - assert 'HOOK:pre_version' in run.out + assert "HOOK:pre_version" in run.out # if pre hook is missing or successful, yadm itself should exit 0 if run.success: if post: - assert 'HOOK:post_version' in run.out + assert "HOOK:post_version" in run.out else: # when a pre hook fails, yadm should not run the command - assert 'version will not be run' in run.out + assert "version will not be run" in run.out # when a pre hook fails, yadm should not run the post hook - assert 'HOOK:post_version' not in run.out + assert "HOOK:post_version" not in run.out # repo fixture is needed to test the population of YADM_HOOK_WORK -@pytest.mark.usefixtures('ds1_repo_copy') +@pytest.mark.usefixtures("ds1_repo_copy") def test_hook_env(runner, yadm_cmd, paths): """Test hook environment""" # test will be done with a non existent "git" passthru command # which should exit with a failing code - cmd = 'passthrucmd' + cmd = "passthrucmd" # write the hook - hook = paths.hooks.join(f'post_{cmd}') - hook.write('#!/bin/bash\nenv\ndeclare\n') + hook = paths.hooks.join(f"post_{cmd}") + hook.write("#!/bin/bash\nenv\ndeclare\n") hook.chmod(0o755) - run = runner(yadm_cmd(cmd, 'extra_args')) + run = runner(yadm_cmd(cmd, "extra_args")) # expect passthru to fail assert run.failure assert f"'{cmd}' is not a git command" in run.err # verify hook environment - assert 'YADM_HOOK_EXIT=1\n' in run.out - assert f'YADM_HOOK_COMMAND={cmd}\n' in run.out - assert f'YADM_HOOK_DIR={paths.yadm}\n' in run.out - assert f'YADM_HOOK_FULL_COMMAND={cmd} extra_args\n' in run.out - assert f'YADM_HOOK_REPO={paths.repo}\n' in run.out - assert f'YADM_HOOK_WORK={paths.work}\n' in run.out - assert 'YADM_ENCRYPT_INCLUDE_FILES=\n' in run.out + assert "YADM_HOOK_EXIT=1\n" in run.out + assert f"YADM_HOOK_COMMAND={cmd}\n" in run.out + assert f"YADM_HOOK_DIR={paths.yadm}\n" in run.out + assert f"YADM_HOOK_FULL_COMMAND={cmd} extra_args\n" in run.out + assert f"YADM_HOOK_REPO={paths.repo}\n" in run.out + assert f"YADM_HOOK_WORK={paths.work}\n" in run.out + assert "YADM_ENCRYPT_INCLUDE_FILES=\n" in run.out # verify the hook environment contains certain exported functions for func in [ - 'builtin_dirname', - 'relative_path', - 'unix_path', - 'mixed_path', + "builtin_dirname", + "relative_path", + "unix_path", + "mixed_path", ]: - assert f'BASH_FUNC_{func}' in run.out + assert f"BASH_FUNC_{func}" in run.out # verify the hook environment contains the list of encrypted files script = f""" @@ -98,10 +99,10 @@ def test_hook_env(runner, yadm_cmd, paths): ENCRYPT_INCLUDE_FILES=(a b c) invoke_hook "post" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert 'YADM_ENCRYPT_INCLUDE_FILES=a\nb\nc\n' in run.out + assert run.err == "" + assert "YADM_ENCRYPT_INCLUDE_FILES=a\nb\nc\n" in run.out def test_escaped(runner, yadm_cmd, paths): @@ -109,35 +110,33 @@ def test_escaped(runner, yadm_cmd, paths): # test will be done with a non existent "git" passthru command # which should exit with a failing code - cmd = 'passthrucmd' + cmd = "passthrucmd" # write the hook - hook = paths.hooks.join(f'post_{cmd}') - hook.write('#!/bin/bash\nenv\n') + hook = paths.hooks.join(f"post_{cmd}") + hook.write("#!/bin/bash\nenv\n") hook.chmod(0o755) - run = runner(yadm_cmd(cmd, 'a b', 'c\td', 'e\\f')) + run = runner(yadm_cmd(cmd, "a b", "c\td", "e\\f")) # expect passthru to fail assert run.failure # verify escaped values - assert ( - f'YADM_HOOK_FULL_COMMAND={cmd} ' - 'a\\ b c\\\td e\\\\f\n') in run.out + assert f"YADM_HOOK_FULL_COMMAND={cmd} a\\ b c\\\td e\\\\f\n" in run.out -@pytest.mark.parametrize('condition', ['exec', 'no-exec', 'mingw']) +@pytest.mark.parametrize("condition", ["exec", "no-exec", "mingw"]) def test_executable(runner, paths, condition): """Verify hook must be exectuable""" - cmd = 'version' - hook = paths.hooks.join(f'pre_{cmd}') - hook.write('#!/bin/sh\necho HOOK\n') + cmd = "version" + hook = paths.hooks.join(f"pre_{cmd}") + hook.write("#!/bin/sh\necho HOOK\n") hook.chmod(0o644) - if condition == 'exec': + if condition == "exec": hook.chmod(0o755) - mingw = 'OPERATING_SYSTEM="MINGWx"' if condition == 'mingw' else '' + mingw = 'OPERATING_SYSTEM="MINGWx"' if condition == "mingw" else "" script = f""" YADM_TEST=1 source {paths.pgm} YADM_HOOKS="{paths.hooks}" @@ -145,27 +144,23 @@ def test_executable(runner, paths, condition): {mingw} invoke_hook "pre" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) - if condition != 'mingw': + if condition != "mingw": assert run.success - assert run.err == '' + assert run.err == "" else: assert run.failure - assert 'Permission denied' in run.err + assert "Permission denied" in run.err - if condition == 'exec': - assert 'HOOK' in run.out - elif condition == 'no-exec': - assert 'HOOK' not in run.out + if condition == "exec": + assert "HOOK" in run.out + elif condition == "no-exec": + assert "HOOK" not in run.out def create_hook(paths, name, code): """Create hook""" hook = paths.hooks.join(name) - hook.write( - '#!/bin/sh\n' - f'echo HOOK:{name}\n' - f'exit {code}\n' - ) + hook.write("#!/bin/sh\n" f"echo HOOK:{name}\n" f"exit {code}\n") hook.chmod(0o755) diff --git a/test/test_init.py b/test/test_init.py index 5542332..a8ba493 100644 --- a/test/test_init.py +++ b/test/test_init.py @@ -4,22 +4,24 @@ import pytest @pytest.mark.parametrize( - 'alt_work, repo_present, force', [ + "alt_work, repo_present, force", + [ (False, False, False), (True, False, False), (False, True, False), (False, True, True), (True, True, True), - ], ids=[ - 'simple', - '-w', - 'existing repo', - '-f', - '-w & -f', - ]) -@pytest.mark.usefixtures('ds1_work_copy') -def test_init( - runner, yadm_cmd, paths, repo_config, alt_work, repo_present, force): + ], + ids=[ + "simple", + "-w", + "existing repo", + "-f", + "-w & -f", + ], +) +@pytest.mark.usefixtures("ds1_work_copy") +def test_init(runner, yadm_cmd, paths, repo_config, alt_work, repo_present, force): """Test init Repos should have attribs: @@ -31,54 +33,54 @@ def test_init( """ # these tests will assume this for $HOME - home = str(paths.root.mkdir('HOME')) + home = str(paths.root.mkdir("HOME")) # ds1_work_copy comes WITH an empty repo dir present. - old_repo = paths.repo.join('old_repo') + old_repo = paths.repo.join("old_repo") if repo_present: # Let's put some data in it, so we can confirm that data is gone when # forced to be overwritten. - old_repo.write('old repo data') + old_repo.write("old repo data") assert old_repo.isfile() else: paths.repo.remove() # command args - args = ['init'] + args = ["init"] cwd = None if alt_work: if force: cwd = paths.work.dirname - args.extend(['-w', paths.work.basename]) + args.extend(["-w", paths.work.basename]) else: - args.extend(['-w', paths.work]) + args.extend(["-w", paths.work]) if force: - args.append('-f') + args.append("-f") # run init - run = runner(yadm_cmd(*args), env={'HOME': home}, cwd=cwd) + runner(["git", "config", "--global", "init.defaultBranch", "master"], env={"HOME": home}, cwd=cwd) + run = runner(yadm_cmd(*args), env={"HOME": home}, cwd=cwd) if repo_present and not force: assert run.failure - assert 'repo already exists' in run.err - assert old_repo.isfile(), 'Missing original repo' + assert "repo already exists" in run.err + assert old_repo.isfile(), "Missing original repo" else: assert run.success - assert 'Initialized empty shared Git repository' in run.out + assert "Initialized empty shared Git repository" in run.out if repo_present: - assert not old_repo.isfile(), 'Original repo still exists' + assert not old_repo.isfile(), "Original repo still exists" else: - assert run.err == '' + assert run.err == "" if alt_work: - assert repo_config('core.worktree') == paths.work + assert repo_config("core.worktree") == paths.work else: - assert repo_config('core.worktree') == home + assert repo_config("core.worktree") == home # uniform repo assertions - assert oct(paths.repo.stat().mode).endswith('00'), ( - 'Repo is not secure') - assert repo_config('core.bare') == 'false' - assert repo_config('status.showUntrackedFiles') == 'no' - assert repo_config('yadm.managed') == 'true' + assert oct(paths.repo.stat().mode).endswith("00"), "Repo is not secure" + assert repo_config("core.bare") == "false" + assert repo_config("status.showUntrackedFiles") == "no" + assert repo_config("yadm.managed") == "true" diff --git a/test/test_introspect.py b/test/test_introspect.py index b292bd4..3a4fb46 100644 --- a/test/test_introspect.py +++ b/test/test_introspect.py @@ -4,38 +4,38 @@ import pytest @pytest.mark.parametrize( - 'name', [ - '', - 'invalid', - 'commands', - 'configs', - 'repo', - 'switches', - ]) -def test_introspect_category( - runner, yadm_cmd, paths, name, - supported_commands, supported_configs, supported_switches): + "name", + [ + "", + "invalid", + "commands", + "configs", + "repo", + "switches", + ], +) +def test_introspect_category(runner, yadm_cmd, paths, name, supported_commands, supported_configs, supported_switches): """Validate introspection category""" if name: - run = runner(command=yadm_cmd('introspect', name)) + run = runner(command=yadm_cmd("introspect", name)) else: - run = runner(command=yadm_cmd('introspect')) + run = runner(command=yadm_cmd("introspect")) assert run.success - assert run.err == '' + assert run.err == "" expected = [] - if name == 'commands': + if name == "commands": expected = supported_commands - elif name == 'configs': + elif name == "configs": expected = supported_configs - elif name == 'switches': + elif name == "switches": expected = supported_switches # assert values - if name in ('', 'invalid'): - assert run.out == '' - if name == 'repo': + if name in ("", "invalid"): + assert run.out == "" + if name == "repo": assert run.out.rstrip() == paths.repo # make sure every expected value is present diff --git a/test/test_list.py b/test/test_list.py index dcfe500..afcea6f 100644 --- a/test/test_list.py +++ b/test/test_list.py @@ -1,31 +1,34 @@ """Test list""" import os + import pytest @pytest.mark.parametrize( - 'location', [ - 'work', - 'outside', - 'subdir', - ]) -@pytest.mark.usefixtures('ds1_copy') + "location", + [ + "work", + "outside", + "subdir", + ], +) +@pytest.mark.usefixtures("ds1_copy") def test_list(runner, yadm_cmd, paths, ds1, location): """List tests""" - if location == 'work': + if location == "work": run_dir = paths.work - elif location == 'outside': - run_dir = paths.work.join('..') - elif location == 'subdir': + elif location == "outside": + run_dir = paths.work.join("..") + elif location == "subdir": # first directory with tracked data run_dir = paths.work.join(ds1.tracked_dirs[0]) with run_dir.as_cwd(): # test with '-a' # should get all tracked files, relative to the work path - run = runner(command=yadm_cmd('list', '-a')) + run = runner(command=yadm_cmd("list", "-a")) assert run.success - assert run.err == '' + assert run.err == "" returned_files = set(run.out.splitlines()) expected_files = {e.path for e in ds1 if e.tracked} assert returned_files == expected_files @@ -33,16 +36,14 @@ def test_list(runner, yadm_cmd, paths, ds1, location): # should get all tracked files, relative to the work path unless in a # subdir, then those should be a limited set of files, relative to the # subdir - run = runner(command=yadm_cmd('list')) + run = runner(command=yadm_cmd("list")) assert run.success - assert run.err == '' + assert run.err == "" returned_files = set(run.out.splitlines()) - if location == 'subdir': + if location == "subdir": basepath = os.path.basename(os.getcwd()) # only expect files within the subdir # names should be relative to subdir - expected_files = { - e.path[len(basepath)+1:] - for e in ds1 if e.tracked and e.path.startswith(basepath) - } + index = len(basepath) + 1 + expected_files = {e.path[index:] for e in ds1 if e.tracked and e.path.startswith(basepath)} assert returned_files == expected_files diff --git a/test/test_perms.py b/test/test_perms.py index 4f052bd..1491c16 100644 --- a/test/test_perms.py +++ b/test/test_perms.py @@ -1,21 +1,21 @@ """Test perms""" import os + import pytest -@pytest.mark.parametrize('autoperms', ['notest', 'unset', 'true', 'false']) -@pytest.mark.usefixtures('ds1_copy') +@pytest.mark.parametrize("autoperms", ["notest", "unset", "true", "false"]) +@pytest.mark.usefixtures("ds1_copy") def test_perms(runner, yadm_cmd, paths, ds1, autoperms): """Test perms""" # set the value of auto-perms - if autoperms != 'notest': - if autoperms != 'unset': - os.system(' '.join( - yadm_cmd('config', 'yadm.auto-perms', autoperms))) + if autoperms != "notest": + if autoperms != "unset": + os.system(" ".join(yadm_cmd("config", "yadm.auto-perms", autoperms))) # privatepaths will hold all paths that should become secured - privatepaths = [paths.work.join('.ssh'), paths.work.join('.gnupg')] + privatepaths = [paths.work.join(".ssh"), paths.work.join(".gnupg")] privatepaths += [paths.work.join(private.path) for private in ds1.private] # create an archive file @@ -23,82 +23,71 @@ def test_perms(runner, yadm_cmd, paths, ds1, autoperms): privatepaths.append(paths.archive) # create encrypted file test data - efile1 = paths.work.join('efile1') - efile1.write('efile1') - efile2 = paths.work.join('efile2') - efile2.write('efile2') - paths.encrypt.write('efile1\nefile2\n!efile1\n') + efile1 = paths.work.join("efile1") + efile1.write("efile1") + efile2 = paths.work.join("efile2") + efile2.write("efile2") + paths.encrypt.write("efile1\nefile2\n!efile1\n") insecurepaths = [efile1] privatepaths.append(efile2) # assert these paths begin unsecured for private in privatepaths + insecurepaths: - assert not oct(private.stat().mode).endswith('00'), ( - 'Path started secured') + assert not oct(private.stat().mode).endswith("00"), "Path started secured" - cmd = 'perms' - if autoperms != 'notest': - cmd = 'status' - run = runner(yadm_cmd(cmd), env={'HOME': paths.work}) + cmd = "perms" + if autoperms != "notest": + cmd = "status" + run = runner(yadm_cmd(cmd), env={"HOME": paths.work}) assert run.success - assert run.err == '' - if cmd == 'perms': - assert run.out == '' + assert run.err == "" + if cmd == "perms": + assert run.out == "" # these paths should be secured if processing perms for private in privatepaths: - if autoperms == 'false': - assert not oct(private.stat().mode).endswith('00'), ( - 'Path should not be secured') + if autoperms == "false": + assert not oct(private.stat().mode).endswith("00"), "Path should not be secured" else: - assert oct(private.stat().mode).endswith('00'), ( - 'Path has not been secured') + assert oct(private.stat().mode).endswith("00"), "Path has not been secured" # these paths should never be secured for private in insecurepaths: - assert not oct(private.stat().mode).endswith('00'), ( - 'Path should not be secured') + assert not oct(private.stat().mode).endswith("00"), "Path should not be secured" -@pytest.mark.parametrize('sshperms', [None, 'true', 'false']) -@pytest.mark.parametrize('gpgperms', [None, 'true', 'false']) -@pytest.mark.usefixtures('ds1_copy') +@pytest.mark.parametrize("sshperms", [None, "true", "false"]) +@pytest.mark.parametrize("gpgperms", [None, "true", "false"]) +@pytest.mark.usefixtures("ds1_copy") def test_perms_control(runner, yadm_cmd, paths, ds1, sshperms, gpgperms): """Test fine control of perms""" # set the value of ssh-perms if sshperms: - os.system(' '.join(yadm_cmd('config', 'yadm.ssh-perms', sshperms))) + os.system(" ".join(yadm_cmd("config", "yadm.ssh-perms", sshperms))) # set the value of gpg-perms if gpgperms: - os.system(' '.join(yadm_cmd('config', 'yadm.gpg-perms', gpgperms))) + os.system(" ".join(yadm_cmd("config", "yadm.gpg-perms", gpgperms))) # privatepaths will hold all paths that should become secured - privatepaths = [paths.work.join('.ssh'), paths.work.join('.gnupg')] + privatepaths = [paths.work.join(".ssh"), paths.work.join(".gnupg")] privatepaths += [paths.work.join(private.path) for private in ds1.private] # assert these paths begin unsecured for private in privatepaths: - assert not oct(private.stat().mode).endswith('00'), ( - 'Path started secured') + assert not oct(private.stat().mode).endswith("00"), "Path started secured" - run = runner(yadm_cmd('perms'), env={'HOME': paths.work}) + run = runner(yadm_cmd("perms"), env={"HOME": paths.work}) assert run.success - assert run.err == '' - assert run.out == '' + assert run.err == "" + assert run.out == "" # these paths should be secured if processing perms for private in privatepaths: - if ( - (sshperms == 'false' and 'ssh' in str(private)) - or - (gpgperms == 'false' and 'gnupg' in str(private)) - ): - assert not oct(private.stat().mode).endswith('00'), ( - 'Path should not be secured') + if (sshperms == "false" and "ssh" in str(private)) or (gpgperms == "false" and "gnupg" in str(private)): + assert not oct(private.stat().mode).endswith("00"), "Path should not be secured" else: - assert oct(private.stat().mode).endswith('00'), ( - 'Path has not been secured') + assert oct(private.stat().mode).endswith("00"), "Path has not been secured" # verify permissions aren't changed for the worktree - assert oct(paths.work.stat().mode).endswith('0755') + assert oct(paths.work.stat().mode).endswith("0755") diff --git a/test/test_syntax.py b/test/test_syntax.py index 72f4ccc..6549822 100644 --- a/test/test_syntax.py +++ b/test/test_syntax.py @@ -1,65 +1,83 @@ """Syntax checks""" import os + import pytest def test_yadm_syntax(runner, yadm): """Is syntactically valid""" - run = runner(command=['bash', '-n', yadm]) + run = runner(command=["bash", "-n", yadm]) assert run.success def test_shellcheck(pytestconfig, runner, yadm, shellcheck_version): """Passes shellcheck""" if not pytestconfig.getoption("--force-linters"): - run = runner(command=['shellcheck', '-V'], report=False) - if f'version: {shellcheck_version}' not in run.out: - pytest.skip('Unsupported shellcheck version') - run = runner(command=['shellcheck', '-s', 'bash', yadm]) + run = runner(command=["shellcheck", "-V"], report=False) + if f"version: {shellcheck_version}" not in run.out: + pytest.skip("Unsupported shellcheck version") + run = runner(command=["shellcheck", "-s", "bash", yadm]) assert run.success def test_pylint(pytestconfig, runner, pylint_version): """Passes pylint""" if not pytestconfig.getoption("--force-linters"): - run = runner(command=['pylint', '--version'], report=False) - if f'pylint {pylint_version}' not in run.out: - pytest.skip('Unsupported pylint version') - pyfiles = list() - for tfile in os.listdir('test'): - if tfile.endswith('.py'): - pyfiles.append(f'test/{tfile}') - run = runner(command=['pylint'] + pyfiles) + run = runner(command=["pylint", "--version"], report=False) + if f"pylint {pylint_version}" not in run.out: + pytest.skip("Unsupported pylint version") + pyfiles = [] + for tfile in os.listdir("test"): + if tfile.endswith(".py"): + pyfiles.append(f"test/{tfile}") + run = runner(command=["pylint"] + pyfiles) + assert run.success + + +def test_isort(pytestconfig, runner, isort_version): + """Passes isort""" + if not pytestconfig.getoption("--force-linters"): + run = runner(command=["isort", "--version"], report=False) + if isort_version not in run.out: + pytest.skip("Unsupported isort version") + run = runner(command=["isort", "-c", "test"]) assert run.success def test_flake8(pytestconfig, runner, flake8_version): """Passes flake8""" if not pytestconfig.getoption("--force-linters"): - run = runner(command=['flake8', '--version'], report=False) + run = runner(command=["flake8", "--version"], report=False) if not run.out.startswith(flake8_version): - pytest.skip('Unsupported flake8 version') - run = runner(command=['flake8', 'test']) + pytest.skip("Unsupported flake8 version") + run = runner(command=["flake8", "test"]) + assert run.success + + +def test_black(pytestconfig, runner, black_version): + """Passes black""" + if not pytestconfig.getoption("--force-linters"): + run = runner(command=["black", "--version"], report=False) + if black_version not in run.out: + pytest.skip("Unsupported black version") + run = runner(command=["black", "--check", "test"]) assert run.success def test_yamllint(pytestconfig, runner, yamllint_version): """Passes yamllint""" if not pytestconfig.getoption("--force-linters"): - run = runner(command=['yamllint', '--version'], report=False) + run = runner(command=["yamllint", "--version"], report=False) if not run.out.strip().endswith(yamllint_version): - pytest.skip('Unsupported yamllint version') - run = runner( - command=['yamllint', '-s', '$(find . -name \\*.yml)'], - shell=True) + pytest.skip("Unsupported yamllint version") + run = runner(command=["yamllint", "-s", "$(find . -name \\*.yml)"], shell=True) assert run.success def test_man(runner): """Check for warnings from man""" - run = runner( - command=['man', '--warnings', './yadm.1']) + run = runner(command=["man.REAL", "--warnings", "./yadm.1"]) assert run.success - assert run.err == '' - assert 'yadm - Yet Another Dotfiles Manager' in run.out + assert run.err == "" + assert "yadm - Yet Another Dotfiles Manager" in run.out diff --git a/test/test_unit_bootstrap_available.py b/test/test_unit_bootstrap_available.py index f37ac08..6ab8df8 100644 --- a/test/test_unit_bootstrap_available.py +++ b/test/test_unit_bootstrap_available.py @@ -8,14 +8,14 @@ def test_bootstrap_missing(runner, paths): def test_bootstrap_no_exec(runner, paths): """Test result of bootstrap_available, when bootstrap not executable""" - paths.bootstrap.write('') + paths.bootstrap.write("") paths.bootstrap.chmod(0o644) run_test(runner, paths, False) def test_bootstrap_exec(runner, paths): """Test result of bootstrap_available, when bootstrap executable""" - paths.bootstrap.write('') + paths.bootstrap.write("") paths.bootstrap.chmod(0o775) run_test(runner, paths, True) @@ -27,7 +27,7 @@ def run_test(runner, paths, success): YADM_BOOTSTRAP='{paths.bootstrap}' bootstrap_available """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success == success - assert run.err == '' - assert run.out == '' + assert run.err == "" + assert run.out == "" diff --git a/test/test_unit_choose_template_cmd.py b/test/test_unit_choose_template_cmd.py index 536735d..cf7600b 100644 --- a/test/test_unit_choose_template_cmd.py +++ b/test/test_unit_choose_template_cmd.py @@ -2,20 +2,20 @@ import pytest -@pytest.mark.parametrize('label', ['', 'default', 'other']) -@pytest.mark.parametrize('awk', [True, False], ids=['awk', 'no-awk']) +@pytest.mark.parametrize("label", ["", "default", "other"]) +@pytest.mark.parametrize("awk", [True, False], ids=["awk", "no-awk"]) def test_kind_default(runner, yadm, awk, label): """Test kind: default""" - expected = 'template_default' - awk_avail = 'true' + expected = "template_default" + awk_avail = "true" if not awk: - awk_avail = 'false' - expected = '' + awk_avail = "false" + expected = "" - if label == 'other': - expected = '' + if label == "other": + expected = "" script = f""" YADM_TEST=1 source {yadm} @@ -23,30 +23,30 @@ def test_kind_default(runner, yadm, awk, label): template="$(choose_template_cmd "{label}")" echo "TEMPLATE:$template" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert f'TEMPLATE:{expected}\n' in run.out + assert run.err == "" + assert f"TEMPLATE:{expected}\n" in run.out -@pytest.mark.parametrize('label', ['envtpl', 'j2cli', 'j2', 'other']) -@pytest.mark.parametrize('envtpl', [True, False], ids=['envtpl', 'no-envtpl']) -@pytest.mark.parametrize('j2cli', [True, False], ids=['j2cli', 'no-j2cli']) +@pytest.mark.parametrize("label", ["envtpl", "j2cli", "j2", "other"]) +@pytest.mark.parametrize("envtpl", [True, False], ids=["envtpl", "no-envtpl"]) +@pytest.mark.parametrize("j2cli", [True, False], ids=["j2cli", "no-j2cli"]) def test_kind_j2cli_envtpl(runner, yadm, envtpl, j2cli, label): """Test kind: j2 (both j2cli & envtpl) j2cli is preferred over envtpl if available. """ - envtpl_avail = 'true' if envtpl else 'false' - j2cli_avail = 'true' if j2cli else 'false' + envtpl_avail = "true" if envtpl else "false" + j2cli_avail = "true" if j2cli else "false" - if label in ('j2cli', 'j2') and j2cli: - expected = 'template_j2cli' - elif label in ('envtpl', 'j2') and envtpl: - expected = 'template_envtpl' + if label in ("j2cli", "j2") and j2cli: + expected = "template_j2cli" + elif label in ("envtpl", "j2") and envtpl: + expected = "template_envtpl" else: - expected = '' + expected = "" script = f""" YADM_TEST=1 source {yadm} @@ -55,7 +55,7 @@ def test_kind_j2cli_envtpl(runner, yadm, envtpl, j2cli, label): template="$(choose_template_cmd "{label}")" echo "TEMPLATE:$template" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert f'TEMPLATE:{expected}\n' in run.out + assert run.err == "" + assert f"TEMPLATE:{expected}\n" in run.out diff --git a/test/test_unit_configure_paths.py b/test/test_unit_configure_paths.py index 8ecd0ea..d2a680e 100644 --- a/test/test_unit_configure_paths.py +++ b/test/test_unit_configure_paths.py @@ -2,52 +2,56 @@ import pytest -ARCHIVE = 'archive' -BOOTSTRAP = 'bootstrap' -CONFIG = 'config' -ENCRYPT = 'encrypt' -HOME = '/testhome' -REPO = 'repo.git' -YDIR = '.config/yadm' -YDATA = '.local/share/yadm' +ARCHIVE = "archive" +BOOTSTRAP = "bootstrap" +CONFIG = "config" +ENCRYPT = "encrypt" +HOME = "/testhome" +REPO = "repo.git" +YDIR = ".config/yadm" +YDATA = ".local/share/yadm" @pytest.mark.parametrize( - 'override, expect', [ + "override, expect", + [ (None, {}), - ('-Y', {'yadm': 'YADM_DIR'}), - ('--yadm-data', {'data': 'YADM_DATA'}), - ('--yadm-repo', {'repo': 'YADM_REPO', 'git': 'GIT_DIR'}), - ('--yadm-config', {'config': 'YADM_CONFIG'}), - ('--yadm-encrypt', {'encrypt': 'YADM_ENCRYPT'}), - ('--yadm-archive', {'archive': 'YADM_ARCHIVE'}), - ('--yadm-bootstrap', {'bootstrap': 'YADM_BOOTSTRAP'}), - ], ids=[ - 'default', - 'override yadm dir', - 'override yadm data', - 'override repo', - 'override config', - 'override encrypt', - 'override archive', - 'override bootstrap', - ]) + ("-Y", {"yadm": "YADM_DIR"}), + ("--yadm-data", {"data": "YADM_DATA"}), + ("--yadm-repo", {"repo": "YADM_REPO", "git": "GIT_DIR"}), + ("--yadm-config", {"config": "YADM_CONFIG"}), + ("--yadm-encrypt", {"encrypt": "YADM_ENCRYPT"}), + ("--yadm-archive", {"archive": "YADM_ARCHIVE"}), + ("--yadm-bootstrap", {"bootstrap": "YADM_BOOTSTRAP"}), + ], + ids=[ + "default", + "override yadm dir", + "override yadm data", + "override repo", + "override config", + "override encrypt", + "override archive", + "override bootstrap", + ], +) @pytest.mark.parametrize( - 'path', ['.', './override', 'override', '.override', '/override'], ids=[ - 'cwd', './relative', 'relative', 'hidden relative', 'absolute' - ]) + "path", + [".", "./override", "override", ".override", "/override"], + ids=["cwd", "./relative", "relative", "hidden relative", "absolute"], +) def test_config(runner, paths, override, expect, path): """Test configure_paths""" - if path.startswith('/'): + if path.startswith("/"): expected_path = path else: expected_path = str(paths.root.join(path)) args = [override, path] if override else [] - if override == '-Y': + if override == "-Y": matches = match_map(expected_path) - elif override == '--yadm-data': + elif override == "--yadm-data": matches = match_map(None, expected_path) else: matches = match_map() @@ -61,23 +65,23 @@ def test_config(runner, paths, override, expect, path): def match_map(yadm_dir=None, yadm_data=None): """Create a dictionary of matches, relative to yadm_dir""" if not yadm_dir: - yadm_dir = '/'.join([HOME, YDIR]) + yadm_dir = "/".join([HOME, YDIR]) if not yadm_data: - yadm_data = '/'.join([HOME, YDATA]) + yadm_data = "/".join([HOME, YDATA]) return { - 'yadm': f'YADM_DIR="{yadm_dir}"', - 'repo': f'YADM_REPO="{yadm_data}/{REPO}"', - 'config': f'YADM_CONFIG="{yadm_dir}/{CONFIG}"', - 'encrypt': f'YADM_ENCRYPT="{yadm_dir}/{ENCRYPT}"', - 'archive': f'YADM_ARCHIVE="{yadm_data}/{ARCHIVE}"', - 'bootstrap': f'YADM_BOOTSTRAP="{yadm_dir}/{BOOTSTRAP}"', - 'git': f'GIT_DIR="{yadm_data}/{REPO}"', - } + "yadm": f'YADM_DIR="{yadm_dir}"', + "repo": f'YADM_REPO="{yadm_data}/{REPO}"', + "config": f'YADM_CONFIG="{yadm_dir}/{CONFIG}"', + "encrypt": f'YADM_ENCRYPT="{yadm_dir}/{ENCRYPT}"', + "archive": f'YADM_ARCHIVE="{yadm_data}/{ARCHIVE}"', + "bootstrap": f'YADM_BOOTSTRAP="{yadm_dir}/{BOOTSTRAP}"', + "git": f'GIT_DIR="{yadm_data}/{REPO}"', + } def run_test(runner, paths, args, expected_matches, cwd=None): """Run proces global args, and run configure_paths""" - argstring = ' '.join(['"'+a+'"' for a in args]) + argstring = " ".join(['"' + a + '"' for a in args]) script = f""" YADM_TEST=1 HOME="{HOME}" source {paths.pgm} process_global_args {argstring} @@ -87,8 +91,8 @@ def run_test(runner, paths, args, expected_matches, cwd=None): configure_paths declare -p | grep -E '(YADM|GIT)_' """ - run = runner(command=['bash'], inp=script, cwd=cwd) + run = runner(command=["bash"], inp=script, cwd=cwd) assert run.success - assert run.err == '' + assert run.err == "" for match in expected_matches: assert match in run.out diff --git a/test/test_unit_copy_perms.py b/test/test_unit_copy_perms.py index 3c79768..c0ea04f 100644 --- a/test/test_unit_copy_perms.py +++ b/test/test_unit_copy_perms.py @@ -1,43 +1,42 @@ """Unit tests: copy_perms""" import os + import pytest -OCTAL = '7654' -NON_OCTAL = '9876' +OCTAL = "7654" +NON_OCTAL = "9876" -@pytest.mark.parametrize( - 'stat_broken', [True, False], ids=['normal', 'stat broken']) +@pytest.mark.parametrize("stat_broken", [True, False], ids=["normal", "stat broken"]) def test_copy_perms(runner, yadm, tmpdir, stat_broken): """Test function copy_perms""" src_mode = 0o754 dst_mode = 0o644 - source = tmpdir.join('source') - source.write('test', ensure=True) + source = tmpdir.join("source") + source.write("test", ensure=True) source.chmod(src_mode) - dest = tmpdir.join('dest') - dest.write('test', ensure=True) + dest = tmpdir.join("dest") + dest.write("test", ensure=True) dest.chmod(dst_mode) - override_stat = '' + override_stat = "" if stat_broken: - override_stat = 'function stat() { echo broken; }' + override_stat = "function stat() { echo broken; }" script = f""" YADM_TEST=1 source {yadm} {override_stat} copy_perms "{source}" "{dest}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert run.out == '' + assert run.err == "" + assert run.out == "" expected = dst_mode if stat_broken else src_mode assert oct(os.stat(dest).st_mode)[-3:] == oct(expected)[-3:] -@pytest.mark.parametrize( - 'stat_output', [OCTAL, NON_OCTAL], ids=['octal', 'non-octal']) +@pytest.mark.parametrize("stat_output", [OCTAL, NON_OCTAL], ids=["octal", "non-octal"]) def test_get_mode(runner, yadm, stat_output): """Test function get_mode""" script = f""" @@ -46,8 +45,8 @@ def test_get_mode(runner, yadm, stat_output): mode=$(get_mode abc) echo "MODE:$mode" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" expected = OCTAL if stat_output == OCTAL else "" - assert f'MODE:{expected}\n' in run.out + assert f"MODE:{expected}\n" in run.out diff --git a/test/test_unit_encryption.py b/test/test_unit_encryption.py index ab03c62..098d5f6 100644 --- a/test/test_unit_encryption.py +++ b/test/test_unit_encryption.py @@ -3,12 +3,12 @@ import pytest -@pytest.mark.parametrize('condition', ['default', 'override']) +@pytest.mark.parametrize("condition", ["default", "override"]) def test_get_cipher(runner, paths, condition): """Test _get_cipher()""" - if condition == 'override': - paths.config.write('[yadm]\n\tcipher = override-cipher') + if condition == "override": + paths.config.write("[yadm]\n\tcipher = override-cipher") script = f""" YADM_TEST=1 source {paths.pgm} @@ -19,18 +19,18 @@ def test_get_cipher(runner, paths, condition): echo "output_archive:$output_archive" echo "yadm_cipher:$yadm_cipher" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert 'output_archive:test-archive' in run.out - if condition == 'override': - assert 'yadm_cipher:override-cipher' in run.out + assert run.err == "" + assert "output_archive:test-archive" in run.out + if condition == "override": + assert "yadm_cipher:override-cipher" in run.out else: - assert 'yadm_cipher:gpg' in run.out + assert "yadm_cipher:gpg" in run.out -@pytest.mark.parametrize('cipher', ['gpg', 'openssl', 'bad']) -@pytest.mark.parametrize('mode', ['_encrypt_to', '_decrypt_from']) +@pytest.mark.parametrize("cipher", ["gpg", "openssl", "bad"]) +@pytest.mark.parametrize("mode", ["_encrypt_to", "_decrypt_from"]) def test_encrypt_decrypt(runner, paths, cipher, mode): """Test _encrypt_to() & _decrypt_from""" @@ -49,24 +49,24 @@ def test_encrypt_decrypt(runner, paths, cipher, mode): GPG_PROGRAM=mock_gpg {mode} {paths.archive} """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) - if cipher != 'bad': + if cipher != "bad": assert run.success assert run.out.startswith(cipher) assert str(paths.archive) in run.out - assert run.err == '' + assert run.err == "" else: assert run.failure - assert 'Unknown cipher' in run.err + assert "Unknown cipher" in run.err -@pytest.mark.parametrize('condition', ['default', 'override']) +@pytest.mark.parametrize("condition", ["default", "override"]) def test_get_openssl_ciphername(runner, paths, condition): """Test _get_openssl_ciphername()""" - if condition == 'override': - paths.config.write('[yadm]\n\topenssl-ciphername = override-cipher') + if condition == "override": + paths.config.write("[yadm]\n\topenssl-ciphername = override-cipher") script = f""" YADM_TEST=1 source {paths.pgm} @@ -76,21 +76,21 @@ def test_get_openssl_ciphername(runner, paths, condition): result=$(_get_openssl_ciphername) echo "result:$result" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - if condition == 'override': - assert run.out.strip() == 'result:override-cipher' + assert run.err == "" + if condition == "override": + assert run.out.strip() == "result:override-cipher" else: - assert run.out.strip() == 'result:aes-256-cbc' + assert run.out.strip() == "result:aes-256-cbc" -@pytest.mark.parametrize('condition', ['old', 'not-old']) +@pytest.mark.parametrize("condition", ["old", "not-old"]) def test_set_openssl_options(runner, paths, condition): """Test _set_openssl_options()""" - if condition == 'old': - paths.config.write('[yadm]\n\topenssl-old = true') + if condition == "old": + paths.config.write("[yadm]\n\topenssl-old = true") script = f""" YADM_TEST=1 source {paths.pgm} @@ -101,20 +101,20 @@ def test_set_openssl_options(runner, paths, condition): _set_openssl_options echo "result:${{OPENSSL_OPTS[@]}}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - if condition == 'old': - assert '-testcipher -salt -md md5' in run.out + assert run.err == "" + if condition == "old": + assert "-testcipher -salt -md md5" in run.out else: - assert '-testcipher -salt -pbkdf2 -iter 100000 -md sha512' in run.out + assert "-testcipher -salt -pbkdf2 -iter 100000 -md sha512" in run.out -@pytest.mark.parametrize('recipient', ['ASK', 'present', '']) +@pytest.mark.parametrize("recipient", ["ASK", "present", ""]) def test_set_gpg_options(runner, paths, recipient): """Test _set_gpg_options()""" - paths.config.write(f'[yadm]\n\tgpg-recipient = {recipient}') + paths.config.write(f"[yadm]\n\tgpg-recipient = {recipient}") script = f""" YADM_TEST=1 source {paths.pgm} @@ -124,12 +124,12 @@ def test_set_gpg_options(runner, paths, recipient): _set_gpg_options echo "result:${{GPG_OPTS[@]}}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - if recipient == 'ASK': - assert run.out.strip() == 'result:--no-default-recipient -e' - elif recipient != '': - assert run.out.strip() == f'result:-e -r {recipient}' + assert run.err == "" + if recipient == "ASK": + assert run.out.strip() == "result:--no-default-recipient -e" + elif recipient != "": + assert run.out.strip() == f"result:-e -r {recipient}" else: - assert run.out.strip() == 'result:-c' + assert run.out.strip() == "result:-c" diff --git a/test/test_unit_exclude_encrypted.py b/test/test_unit_exclude_encrypted.py index 9d9a074..8937a8b 100644 --- a/test/test_unit_exclude_encrypted.py +++ b/test/test_unit_exclude_encrypted.py @@ -2,38 +2,28 @@ import pytest -@pytest.mark.parametrize( - 'exclude', ['missing', 'outdated', 'up-to-date']) -@pytest.mark.parametrize( - 'encrypt_exists', [True, False], ids=['encrypt', 'no-encrypt']) -@pytest.mark.parametrize( - 'auto_exclude', [True, False], ids=['enabled', 'disabled']) -def test_exclude_encrypted( - runner, tmpdir, yadm, encrypt_exists, auto_exclude, exclude): +@pytest.mark.parametrize("exclude", ["missing", "outdated", "up-to-date"]) +@pytest.mark.parametrize("encrypt_exists", [True, False], ids=["encrypt", "no-encrypt"]) +@pytest.mark.parametrize("auto_exclude", [True, False], ids=["enabled", "disabled"]) +def test_exclude_encrypted(runner, tmpdir, yadm, encrypt_exists, auto_exclude, exclude): """Test exclude_encrypted()""" - header = ( - "# yadm-auto-excludes\n" - "# This section is managed by yadm.\n" - "# Any edits below will be lost.\n" - ) + header = "# yadm-auto-excludes\n# This section is managed by yadm.\n# Any edits below will be lost.\n" config_function = 'function config() { echo "false";}' if auto_exclude: - config_function = 'function config() { return; }' + config_function = "function config() { return; }" - encrypt_file = tmpdir.join('encrypt_file') - repo_dir = tmpdir.join('repodir') - exclude_file = repo_dir.join('info/exclude') + encrypt_file = tmpdir.join("encrypt_file") + repo_dir = tmpdir.join("repodir") + exclude_file = repo_dir.join("info/exclude") if encrypt_exists: - encrypt_file.write('test-encrypt-data\n', ensure=True) - if exclude == 'outdated': - exclude_file.write( - f'original-exclude\n{header}outdated\n', ensure=True) - elif exclude == 'up-to-date': - exclude_file.write( - f'original-exclude\n{header}test-encrypt-data\n', ensure=True) + encrypt_file.write("test-encrypt-data\n", ensure=True) + if exclude == "outdated": + exclude_file.write(f"original-exclude\n{header}outdated\n", ensure=True) + elif exclude == "up-to-date": + exclude_file.write(f"original-exclude\n{header}test-encrypt-data\n", ensure=True) script = f""" YADM_TEST=1 source {yadm} @@ -43,24 +33,22 @@ def test_exclude_encrypted( YADM_REPO="{repo_dir}" exclude_encrypted """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" if auto_exclude: if encrypt_exists: assert exclude_file.exists() - if exclude == 'missing': - assert exclude_file.read() == f'{header}test-encrypt-data\n' + if exclude == "missing": + assert exclude_file.read() == f"{header}test-encrypt-data\n" else: - assert exclude_file.read() == ( - 'original-exclude\n' - f'{header}test-encrypt-data\n') - if exclude != 'up-to-date': - assert f'Updating {exclude_file}' in run.out + assert exclude_file.read() == ("original-exclude\n" f"{header}test-encrypt-data\n") + if exclude != "up-to-date": + assert f"Updating {exclude_file}" in run.out else: - assert run.out == '' + assert run.out == "" else: - assert run.out == '' + assert run.out == "" else: - assert run.out == '' + assert run.out == "" diff --git a/test/test_unit_issue_legacy_path_warning.py b/test/test_unit_issue_legacy_path_warning.py index e43228b..faae7fa 100644 --- a/test/test_unit_issue_legacy_path_warning.py +++ b/test/test_unit_issue_legacy_path_warning.py @@ -3,25 +3,24 @@ import pytest @pytest.mark.parametrize( - 'legacy_path', [ + "legacy_path", + [ None, - 'repo.git', - 'files.gpg', - ], - ) -@pytest.mark.parametrize( - 'override', [True, False], ids=['override', 'no-override']) -@pytest.mark.parametrize( - 'upgrade', [True, False], ids=['upgrade', 'no-upgrade']) + "repo.git", + "files.gpg", + ], +) +@pytest.mark.parametrize("override", [True, False], ids=["override", "no-override"]) +@pytest.mark.parametrize("upgrade", [True, False], ids=["upgrade", "no-upgrade"]) def test_legacy_warning(tmpdir, runner, yadm, upgrade, override, legacy_path): """Use issue_legacy_path_warning""" - home = tmpdir.mkdir('home') + home = tmpdir.mkdir("home") if legacy_path: - home.ensure(f'.config/yadm/{str(legacy_path)}') + home.ensure(f".config/yadm/{str(legacy_path)}") - override = 'YADM_OVERRIDE_REPO=override' if override else '' - main_args = 'MAIN_ARGS=("upgrade")' if upgrade else '' + override = "YADM_OVERRIDE_REPO=override" if override else "" + main_args = 'MAIN_ARGS=("upgrade")' if upgrade else "" script = f""" XDG_CONFIG_HOME= XDG_DATA_HOME= @@ -32,10 +31,10 @@ def test_legacy_warning(tmpdir, runner, yadm, upgrade, override, legacy_path): set_yadm_dirs issue_legacy_path_warning """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.out == '' + assert run.out == "" if legacy_path and (not upgrade) and (not override): - assert 'Legacy paths have been detected' in run.err + assert "Legacy paths have been detected" in run.err else: - assert 'Legacy paths have been detected' not in run.err + assert "Legacy paths have been detected" not in run.err diff --git a/test/test_unit_parse_encrypt.py b/test/test_unit_parse_encrypt.py index ec3a6ee..6a5c23b 100644 --- a/test/test_unit_parse_encrypt.py +++ b/test/test_unit_parse_encrypt.py @@ -7,136 +7,137 @@ def test_not_called(runner, paths): """Test parse_encrypt (not called)""" run = run_parse_encrypt(runner, paths, skip_parse=True) assert run.success - assert run.err == '' - assert 'EIF:unparsed' in run.out, 'EIF should be unparsed' - assert 'EIF_COUNT:1' in run.out, 'Only value of EIF should be unparsed' + assert run.err == "" + assert "EIF:unparsed" in run.out, "EIF should be unparsed" + assert "EIF_COUNT:1" in run.out, "Only value of EIF should be unparsed" def test_short_circuit(runner, paths): """Test parse_encrypt (short-circuit)""" run = run_parse_encrypt(runner, paths, twice=True) assert run.success - assert run.err == '' - assert 'PARSE_ENCRYPT_SHORT=parse_encrypt() not reprocessed' in run.out, ( - 'parse_encrypt() should short-circuit') + assert run.err == "" + assert "PARSE_ENCRYPT_SHORT=parse_encrypt() not reprocessed" in run.out, "parse_encrypt() should short-circuit" @pytest.mark.parametrize( - 'encrypt', [ - ('missing'), - ('empty'), - ]) + "encrypt", + [ + ("missing"), + ("empty"), + ], +) def test_empty(runner, paths, encrypt): """Test parse_encrypt (file missing/empty)""" # write encrypt file - if encrypt == 'missing': - assert not paths.encrypt.exists(), 'Encrypt should be missing' + if encrypt == "missing": + assert not paths.encrypt.exists(), "Encrypt should be missing" else: - paths.encrypt.write('') - assert paths.encrypt.exists(), 'Encrypt should exist' - assert paths.encrypt.size() == 0, 'Encrypt should be empty' + paths.encrypt.write("") + assert paths.encrypt.exists(), "Encrypt should exist" + assert paths.encrypt.size() == 0, "Encrypt should be empty" # run parse_encrypt run = run_parse_encrypt(runner, paths) assert run.success - assert run.err == '' + assert run.err == "" # validate parsing result - assert 'EIF_COUNT:0' in run.out, 'EIF should be empty' + assert "EIF_COUNT:0" in run.out, "EIF should be empty" def create_test_encrypt_data(paths): """Generate test data for testing encrypt""" - edata = '' + edata = "" expected = set() # empty line - edata += '\n' + edata += "\n" # simple comments - edata += '# a simple comment\n' - edata += ' # a comment with leading space\n' + edata += "# a simple comment\n" + edata += " # a comment with leading space\n" # unreferenced directory - paths.work.join('unreferenced').mkdir() + paths.work.join("unreferenced").mkdir() # simple files - edata += 'simple_file\n' - edata += 'simple.file\n' - paths.work.join('simple_file').write('') - paths.work.join('simple.file').write('') - paths.work.join('simple_file2').write('') - paths.work.join('simple.file2').write('') - expected.add('simple_file') - expected.add('simple.file') + edata += "simple_file\n" + edata += "simple.file\n" + paths.work.join("simple_file").write("") + paths.work.join("simple.file").write("") + paths.work.join("simple_file2").write("") + paths.work.join("simple.file2").write("") + expected.add("simple_file") + expected.add("simple.file") # simple files in directories - edata += 'simple_dir/simple_file\n' - paths.work.join('simple_dir/simple_file').write('', ensure=True) - paths.work.join('simple_dir/simple_file2').write('', ensure=True) - expected.add('simple_dir/simple_file') + edata += "simple_dir/simple_file\n" + paths.work.join("simple_dir/simple_file").write("", ensure=True) + paths.work.join("simple_dir/simple_file2").write("", ensure=True) + expected.add("simple_dir/simple_file") # paths with spaces - edata += 'with space/with space\n' - paths.work.join('with space/with space').write('', ensure=True) - paths.work.join('with space/with space2').write('', ensure=True) - expected.add('with space/with space') + edata += "with space/with space\n" + paths.work.join("with space/with space").write("", ensure=True) + paths.work.join("with space/with space2").write("", ensure=True) + expected.add("with space/with space") # hidden files - edata += '.hidden\n' - paths.work.join('.hidden').write('') - expected.add('.hidden') + edata += ".hidden\n" + paths.work.join(".hidden").write("") + expected.add(".hidden") # hidden files in directories - edata += '.hidden_dir/.hidden_file\n' - paths.work.join('.hidden_dir/.hidden_file').write('', ensure=True) - expected.add('.hidden_dir/.hidden_file') + edata += ".hidden_dir/.hidden_file\n" + paths.work.join(".hidden_dir/.hidden_file").write("", ensure=True) + expected.add(".hidden_dir/.hidden_file") # wildcards - edata += 'wild*\n' - paths.work.join('wildcard1').write('', ensure=True) - paths.work.join('wildcard2').write('', ensure=True) - expected.add('wildcard1') - expected.add('wildcard2') + edata += "wild*\n" + paths.work.join("wildcard1").write("", ensure=True) + paths.work.join("wildcard2").write("", ensure=True) + expected.add("wildcard1") + expected.add("wildcard2") - edata += 'dirwild*\n' - paths.work.join('dirwildcard/file1').write('', ensure=True) - paths.work.join('dirwildcard/file2').write('', ensure=True) - expected.add('dirwildcard') + edata += "dirwild*\n" + paths.work.join("dirwildcard/file1").write("", ensure=True) + paths.work.join("dirwildcard/file2").write("", ensure=True) + expected.add("dirwildcard") # excludes - edata += 'exclude*\n' - edata += 'ex ex/*\n' - paths.work.join('exclude_file1').write('') - paths.work.join('exclude_file2.ex').write('') - paths.work.join('exclude_file3.ex3').write('') - expected.add('exclude_file1') - expected.add('exclude_file3.ex3') - edata += '!*.ex\n' - edata += '!ex ex/*.txt\n' - paths.work.join('ex ex/file4').write('', ensure=True) - paths.work.join('ex ex/file5.txt').write('', ensure=True) - paths.work.join('ex ex/file6.text').write('', ensure=True) - expected.add('ex ex/file4') - expected.add('ex ex/file6.text') + edata += "exclude*\n" + edata += "ex ex/*\n" + paths.work.join("exclude_file1").write("") + paths.work.join("exclude_file2.ex").write("") + paths.work.join("exclude_file3.ex3").write("") + expected.add("exclude_file1") + expected.add("exclude_file3.ex3") + edata += "!*.ex\n" + edata += "!ex ex/*.txt\n" + paths.work.join("ex ex/file4").write("", ensure=True) + paths.work.join("ex ex/file5.txt").write("", ensure=True) + paths.work.join("ex ex/file6.text").write("", ensure=True) + expected.add("ex ex/file4") + expected.add("ex ex/file6.text") # double star - edata += 'doublestar/**/file*\n' - edata += '!**/file3\n' - paths.work.join('doublestar/a/b/file1').write('', ensure=True) - paths.work.join('doublestar/c/d/file2').write('', ensure=True) - paths.work.join('doublestar/e/f/file3').write('', ensure=True) - paths.work.join('doublestar/g/h/nomatch').write('', ensure=True) - expected.add('doublestar/a/b/file1') - expected.add('doublestar/c/d/file2') + edata += "doublestar/**/file*\n" + edata += "!**/file3\n" + paths.work.join("doublestar/a/b/file1").write("", ensure=True) + paths.work.join("doublestar/c/d/file2").write("", ensure=True) + paths.work.join("doublestar/e/f/file3").write("", ensure=True) + paths.work.join("doublestar/g/h/nomatch").write("", ensure=True) + expected.add("doublestar/a/b/file1") + expected.add("doublestar/c/d/file2") # doublestar/e/f/file3 is excluded return edata, expected -@pytest.mark.usefixtures('ds1_repo_copy') +@pytest.mark.usefixtures("ds1_repo_copy") def test_file_parse_encrypt(runner, paths): """Test parse_encrypt @@ -147,39 +148,35 @@ def test_file_parse_encrypt(runner, paths): edata, expected = create_test_encrypt_data(paths) # write encrypt file - print(f'ENCRYPT:\n---\n{edata}---\n') + print(f"ENCRYPT:\n---\n{edata}---\n") paths.encrypt.write(edata) assert paths.encrypt.isfile() # run parse_encrypt run = run_parse_encrypt(runner, paths) assert run.success - assert run.err == '' + assert run.err == "" - assert f'EIF_COUNT:{len(expected)}' in run.out, 'EIF count wrong' + assert f"EIF_COUNT:{len(expected)}" in run.out, "EIF count wrong" for expected_file in expected: - assert f'EIF:{expected_file}\n' in run.out + assert f"EIF:{expected_file}\n" in run.out - sorted_expectations = '\n'.join( - [f'EIF:{exp}' for exp in sorted(expected)]) + sorted_expectations = "\n".join([f"EIF:{exp}" for exp in sorted(expected)]) assert sorted_expectations in run.out -def run_parse_encrypt( - runner, paths, - skip_parse=False, - twice=False): +def run_parse_encrypt(runner, paths, skip_parse=False, twice=False): """Run parse_encrypt A count of ENCRYPT_INCLUDE_FILES will be reported as EIF_COUNT:X. All values of ENCRYPT_INCLUDE_FILES will be reported as individual EIF:value lines. """ - parse_cmd = 'parse_encrypt' + parse_cmd = "parse_encrypt" if skip_parse: - parse_cmd = '' + parse_cmd = "" if twice: - parse_cmd = 'parse_encrypt; parse_encrypt' + parse_cmd = "parse_encrypt; parse_encrypt" script = f""" YADM_TEST=1 source {paths.pgm} YADM_ENCRYPT={paths.encrypt} @@ -197,5 +194,5 @@ def run_parse_encrypt( echo "EIF:$value" done """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) return run diff --git a/test/test_unit_private_dirs.py b/test/test_unit_private_dirs.py index 4f182da..58bd39c 100644 --- a/test/test_unit_private_dirs.py +++ b/test/test_unit_private_dirs.py @@ -3,15 +3,15 @@ import pytest @pytest.mark.parametrize( - 'gnupghome', + "gnupghome", [True, False], - ids=['gnupghome-set', 'gnupghome-unset'], + ids=["gnupghome-set", "gnupghome-unset"], ) -@pytest.mark.parametrize('param', ['all', 'gnupg']) +@pytest.mark.parametrize("param", ["all", "gnupg"]) def test_relative_path(runner, paths, gnupghome, param): """Test translate_to_relative""" - alt_gnupghome = 'alt/gnupghome' + alt_gnupghome = "alt/gnupghome" env_gnupghome = paths.work.join(alt_gnupghome) script = f""" @@ -22,13 +22,13 @@ def test_relative_path(runner, paths, gnupghome, param): env = {} if gnupghome: - env['GNUPGHOME'] = env_gnupghome + env["GNUPGHOME"] = env_gnupghome - expected = alt_gnupghome if gnupghome else '.gnupg' - if param == 'all': - expected = f'.ssh {expected}' + expected = alt_gnupghome if gnupghome else ".gnupg" + if param == "all": + expected = f".ssh {expected}" - run = runner(command=['bash'], inp=script, env=env) + run = runner(command=["bash"], inp=script, env=env) assert run.success - assert run.err == '' + assert run.err == "" assert run.out.strip() == expected diff --git a/test/test_unit_query_distro.py b/test/test_unit_query_distro.py index 4f46501..c32760b 100644 --- a/test/test_unit_query_distro.py +++ b/test/test_unit_query_distro.py @@ -2,18 +2,16 @@ import pytest -@pytest.mark.parametrize( - 'condition', ['lsb_release', 'os-release', 'os-release-quotes', 'missing']) +@pytest.mark.parametrize("condition", ["lsb_release", "os-release", "os-release-quotes", "missing"]) def test_query_distro(runner, yadm, tst_distro, tmp_path, condition): """Match lsb_release -si when present""" - test_release = 'testrelease' - lsb_release = '' - os_release = tmp_path.joinpath('os-release') - if 'os-release' in condition: - quotes = '"' if 'quotes' in condition else '' - os_release.write_text( - f"testing\nID={quotes}{test_release}{quotes}\nrelease") - if condition != 'lsb_release': + test_release = "testrelease" + lsb_release = "" + os_release = tmp_path.joinpath("os-release") + if "os-release" in condition: + quotes = '"' if "quotes" in condition else "" + os_release.write_text(f"testing\nID={quotes}{test_release}{quotes}\nrelease") + if condition != "lsb_release": lsb_release = 'LSB_RELEASE_PROGRAM="missing_lsb_release"' script = f""" YADM_TEST=1 source {yadm} @@ -21,12 +19,12 @@ def test_query_distro(runner, yadm, tst_distro, tmp_path, condition): OS_RELEASE="{os_release}" query_distro """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - if condition == 'lsb_release': + assert run.err == "" + if condition == "lsb_release": assert run.out.rstrip() == tst_distro - elif 'os-release' in condition: + elif "os-release" in condition: assert run.out.rstrip() == test_release else: - assert run.out.rstrip() == '' + assert run.out.rstrip() == "" diff --git a/test/test_unit_query_distro_family.py b/test/test_unit_query_distro_family.py index bf68319..1935bf6 100644 --- a/test/test_unit_query_distro_family.py +++ b/test/test_unit_query_distro_family.py @@ -2,25 +2,23 @@ import pytest -@pytest.mark.parametrize( - 'condition', ['os-release', 'os-release-quotes', 'missing']) +@pytest.mark.parametrize("condition", ["os-release", "os-release-quotes", "missing"]) def test_query_distro_family(runner, yadm, tmp_path, condition): """Match ID_LIKE when present""" - test_family = 'testfamily' - os_release = tmp_path.joinpath('os-release') - if 'os-release' in condition: - quotes = '"' if 'quotes' in condition else '' - os_release.write_text( - f"testing\nID_LIKE={quotes}{test_family}{quotes}\nfamily") + test_family = "testfamily" + os_release = tmp_path.joinpath("os-release") + if "os-release" in condition: + quotes = '"' if "quotes" in condition else "" + os_release.write_text(f"testing\nID_LIKE={quotes}{test_family}{quotes}\nfamily") script = f""" YADM_TEST=1 source {yadm} OS_RELEASE="{os_release}" query_distro_family """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - if 'os-release' in condition: + assert run.err == "" + if "os-release" in condition: assert run.out.rstrip() == test_family else: - assert run.out.rstrip() == '' + assert run.out.rstrip() == "" diff --git a/test/test_unit_record_score.py b/test/test_unit_record_score.py index 78596e1..a82046c 100644 --- a/test/test_unit_record_score.py +++ b/test/test_unit_record_score.py @@ -30,13 +30,13 @@ def test_dont_record_zeros(runner, yadm): record_score "0" "testtgt" "testsrc" {REPORT_RESULTS} """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert 'SIZE:0\n' in run.out - assert 'SCORES:\n' in run.out - assert 'TARGETS:\n' in run.out - assert 'SOURCES:\n' in run.out + assert run.err == "" + assert "SIZE:0\n" in run.out + assert "SCORES:\n" in run.out + assert "TARGETS:\n" in run.out + assert "SOURCES:\n" in run.out def test_new_scores(runner, yadm): @@ -50,29 +50,29 @@ def test_new_scores(runner, yadm): record_score "4" "tgt_three" "src_three" {REPORT_RESULTS} """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert 'SIZE:3\n' in run.out - assert 'SCORES:1 2 4\n' in run.out - assert 'TARGETS:tgt_one tgt_two tgt_three\n' in run.out - assert 'SOURCES:src_one src_two src_three\n' in run.out + assert run.err == "" + assert "SIZE:3\n" in run.out + assert "SCORES:1 2 4\n" in run.out + assert "TARGETS:tgt_one tgt_two tgt_three\n" in run.out + assert "SOURCES:src_one src_two src_three\n" in run.out -@pytest.mark.parametrize('difference', ['lower', 'equal', 'higher']) +@pytest.mark.parametrize("difference", ["lower", "equal", "higher"]) def test_existing_scores(runner, yadm, difference): """Test existing scores""" - expected_score = '2' - expected_src = 'existing_src' - if difference == 'lower': - score = '1' - elif difference == 'equal': - score = '2' + expected_score = "2" + expected_src = "existing_src" + if difference == "lower": + score = "1" + elif difference == "equal": + score = "2" else: - score = '4' - expected_score = '4' - expected_src = 'new_src' + score = "4" + expected_score = "4" + expected_src = "new_src" script = f""" YADM_TEST=1 source {yadm} @@ -83,13 +83,13 @@ def test_existing_scores(runner, yadm, difference): record_score "{score}" "testtgt" "new_src" {REPORT_RESULTS} """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert 'SIZE:1\n' in run.out - assert f'SCORES:{expected_score}\n' in run.out - assert 'TARGETS:testtgt\n' in run.out - assert f'SOURCES:{expected_src}\n' in run.out + assert run.err == "" + assert "SIZE:1\n" in run.out + assert f"SCORES:{expected_score}\n" in run.out + assert "TARGETS:testtgt\n" in run.out + assert f"SOURCES:{expected_src}\n" in run.out def test_existing_template(runner, yadm): @@ -105,19 +105,19 @@ def test_existing_template(runner, yadm): record_score "2" "testtgt" "new_src" {REPORT_RESULTS} """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert 'SIZE:1\n' in run.out - assert 'SCORES:1\n' in run.out - assert 'TARGETS:testtgt\n' in run.out - assert 'SOURCES:\n' in run.out + assert run.err == "" + assert "SIZE:1\n" in run.out + assert "SCORES:1\n" in run.out + assert "TARGETS:testtgt\n" in run.out + assert "SOURCES:\n" in run.out def test_config_first(runner, yadm): """Verify YADM_CONFIG is always processed first""" - config = 'yadm_config_file' + config = "yadm_config_file" script = f""" YADM_TEST=1 source {yadm} {INIT_VARS} @@ -130,12 +130,12 @@ def test_config_first(runner, yadm): echo "CMD_VALUE:${{alt_template_cmds[@]}}" echo "CMD_INDEX:${{!alt_template_cmds[@]}}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert 'SIZE:3\n' in run.out - assert 'SCORES:2 1 3\n' in run.out - assert f'TARGETS:{config} tgt_before tgt_tmp tgt_after\n' in run.out - assert 'SOURCES:src_config src_before src_tmp src_after\n' in run.out - assert 'CMD_VALUE:cmd_tmp\n' in run.out - assert 'CMD_INDEX:2\n' in run.out + assert run.err == "" + assert "SIZE:3\n" in run.out + assert "SCORES:2 1 3\n" in run.out + assert f"TARGETS:{config} tgt_before tgt_tmp tgt_after\n" in run.out + assert "SOURCES:src_config src_before src_tmp src_after\n" in run.out + assert "CMD_VALUE:cmd_tmp\n" in run.out + assert "CMD_INDEX:2\n" in run.out diff --git a/test/test_unit_record_template.py b/test/test_unit_record_template.py index 6bfd012..4f3c3e8 100644 --- a/test/test_unit_record_template.py +++ b/test/test_unit_record_template.py @@ -25,13 +25,13 @@ def test_new_template(runner, yadm): record_template "tgt_three" "cmd_three" "src_three" {REPORT_RESULTS} """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert 'SIZE:3\n' in run.out - assert 'TARGETS:tgt_one tgt_two tgt_three\n' in run.out - assert 'CMDS:cmd_one cmd_two cmd_three\n' in run.out - assert 'SOURCES:src_one src_two src_three\n' in run.out + assert run.err == "" + assert "SIZE:3\n" in run.out + assert "TARGETS:tgt_one tgt_two tgt_three\n" in run.out + assert "CMDS:cmd_one cmd_two cmd_three\n" in run.out + assert "SOURCES:src_one src_two src_three\n" in run.out def test_existing_template(runner, yadm): @@ -46,10 +46,10 @@ def test_existing_template(runner, yadm): record_template "testtgt" "new_cmd" "new_src" {REPORT_RESULTS} """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert 'SIZE:1\n' in run.out - assert 'TARGETS:testtgt\n' in run.out - assert 'CMDS:new_cmd\n' in run.out - assert 'SOURCES:new_src\n' in run.out + assert run.err == "" + assert "SIZE:1\n" in run.out + assert "TARGETS:testtgt\n" in run.out + assert "CMDS:new_cmd\n" in run.out + assert "SOURCES:new_src\n" in run.out diff --git a/test/test_unit_relative_path.py b/test/test_unit_relative_path.py index f723c84..e0b32f5 100644 --- a/test/test_unit_relative_path.py +++ b/test/test_unit_relative_path.py @@ -3,7 +3,7 @@ import pytest @pytest.mark.parametrize( - 'base,full_path,expected', + "base,full_path,expected", [ ("/A/B/C", "/A", "../.."), ("/A/B/C", "/A/B", ".."), @@ -25,7 +25,7 @@ def test_relative_path(runner, paths, base, full_path, expected): relative_path "{base}" "{full_path}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert run.out.strip() == expected diff --git a/test/test_unit_remove_stale_links.py b/test/test_unit_remove_stale_links.py index 0bd960b..f389ed8 100644 --- a/test/test_unit_remove_stale_links.py +++ b/test/test_unit_remove_stale_links.py @@ -1,23 +1,24 @@ """Unit tests: remove_stale_links""" import os + import pytest -@pytest.mark.parametrize('linked', [True, False]) -@pytest.mark.parametrize('kind', ['file', 'symlink']) +@pytest.mark.parametrize("linked", [True, False]) +@pytest.mark.parametrize("kind", ["file", "symlink"]) def test_remove_stale_links(runner, yadm, tmpdir, kind, linked): """Test remove_stale_links()""" - source_file = tmpdir.join('source_file') - source_file.write('source file', ensure=True) - link = tmpdir.join('link') + source_file = tmpdir.join("source_file") + source_file.write("source file", ensure=True) + link = tmpdir.join("link") - if kind == 'file': - link.write('link file', ensure=True) + if kind == "file": + link.write("link file", ensure=True) else: - os.system(f'ln -s {source_file} {link}') + os.system(f"ln -s {source_file} {link}") - alt_linked = '' + alt_linked = "" if linked: alt_linked = source_file @@ -29,9 +30,9 @@ def test_remove_stale_links(runner, yadm, tmpdir, kind, linked): remove_stale_links """ - run = runner(command=['bash'], inp=script) - assert run.err == '' - if kind == 'symlink' and not linked: - assert f'rm -f {link}' in run.out + run = runner(command=["bash"], inp=script) + assert run.err == "" + if kind == "symlink" and not linked: + assert f"rm -f {link}" in run.out else: - assert run.out == '' + assert run.out == "" diff --git a/test/test_unit_report_invalid_alts.py b/test/test_unit_report_invalid_alts.py index 8730d61..996b1ef 100644 --- a/test/test_unit_report_invalid_alts.py +++ b/test/test_unit_report_invalid_alts.py @@ -2,15 +2,15 @@ import pytest -@pytest.mark.parametrize('valid', [True, False], ids=['valid', 'no_valid']) -@pytest.mark.parametrize('previous', [True, False], ids=['prev', 'no_prev']) +@pytest.mark.parametrize("valid", [True, False], ids=["valid", "no_valid"]) +@pytest.mark.parametrize("previous", [True, False], ids=["prev", "no_prev"]) def test_report_invalid_alts(runner, yadm, valid, previous): """Use report_invalid_alts""" - lwi = '' - alts = 'INVALID_ALT=()' + lwi = "" + alts = "INVALID_ALT=()" if previous: - lwi = 'LEGACY_WARNING_ISSUED=1' + lwi = "LEGACY_WARNING_ISSUED=1" if not valid: alts = 'INVALID_ALT=("file##invalid")' @@ -20,11 +20,11 @@ def test_report_invalid_alts(runner, yadm, valid, previous): {alts} report_invalid_alts """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.out == '' + assert run.out == "" if not valid and not previous: - assert 'WARNING' in run.err - assert 'file##invalid' in run.err + assert "WARNING" in run.err + assert "file##invalid" in run.err else: - assert run.err == '' + assert run.err == "" diff --git a/test/test_unit_score_file.py b/test/test_unit_score_file.py index a63246a..c84fb1e 100644 --- a/test/test_unit_score_file.py +++ b/test/test_unit_score_file.py @@ -2,40 +2,40 @@ import pytest CONDITION = { - 'default': { - 'labels': ['default'], - 'modifier': 0, - }, - 'arch': { - 'labels': ['a', 'arch'], - 'modifier': 1, - }, - 'system': { - 'labels': ['o', 'os'], - 'modifier': 2, - }, - 'distro': { - 'labels': ['d', 'distro'], - 'modifier': 4, - }, - 'distro_family': { - 'labels': ['f', 'distro_family'], - 'modifier': 8, - }, - 'class': { - 'labels': ['c', 'class'], - 'modifier': 16, - }, - 'hostname': { - 'labels': ['h', 'hostname'], - 'modifier': 32, - }, - 'user': { - 'labels': ['u', 'user'], - 'modifier': 64, - }, - } -TEMPLATE_LABELS = ['t', 'template', 'yadm'] + "default": { + "labels": ["default"], + "modifier": 0, + }, + "arch": { + "labels": ["a", "arch"], + "modifier": 1, + }, + "system": { + "labels": ["o", "os"], + "modifier": 2, + }, + "distro": { + "labels": ["d", "distro"], + "modifier": 4, + }, + "distro_family": { + "labels": ["f", "distro_family"], + "modifier": 8, + }, + "class": { + "labels": ["c", "class"], + "modifier": 16, + }, + "hostname": { + "labels": ["h", "hostname"], + "modifier": 32, + }, + "user": { + "labels": ["u", "user"], + "modifier": 64, + }, +} +TEMPLATE_LABELS = ["t", "template", "yadm"] def calculate_score(filename): @@ -43,48 +43,48 @@ def calculate_score(filename): # pylint: disable=too-many-branches score = 0 - _, conditions = filename.split('##', 1) + _, conditions = filename.split("##", 1) - for condition in conditions.split(','): + for condition in conditions.split(","): label = condition value = None - if '.' in condition: - label, value = condition.split('.', 1) - if label in CONDITION['default']['labels']: + if "." in condition: + label, value = condition.split(".", 1) + if label in CONDITION["default"]["labels"]: score += 1000 - elif label in CONDITION['arch']['labels']: - if value == 'testarch': - score += 1000 + CONDITION['arch']['modifier'] + elif label in CONDITION["arch"]["labels"]: + if value == "testarch": + score += 1000 + CONDITION["arch"]["modifier"] else: score = 0 break - elif label in CONDITION['system']['labels']: - if value == 'testsystem': - score += 1000 + CONDITION['system']['modifier'] + elif label in CONDITION["system"]["labels"]: + if value == "testsystem": + score += 1000 + CONDITION["system"]["modifier"] else: score = 0 break - elif label in CONDITION['distro']['labels']: - if value == 'testdistro': - score += 1000 + CONDITION['distro']['modifier'] + elif label in CONDITION["distro"]["labels"]: + if value == "testdistro": + score += 1000 + CONDITION["distro"]["modifier"] else: score = 0 break - elif label in CONDITION['class']['labels']: - if value == 'testclass': - score += 1000 + CONDITION['class']['modifier'] + elif label in CONDITION["class"]["labels"]: + if value == "testclass": + score += 1000 + CONDITION["class"]["modifier"] else: score = 0 break - elif label in CONDITION['hostname']['labels']: - if value == 'testhost': - score += 1000 + CONDITION['hostname']['modifier'] + elif label in CONDITION["hostname"]["labels"]: + if value == "testhost": + score += 1000 + CONDITION["hostname"]["modifier"] else: score = 0 break - elif label in CONDITION['user']['labels']: - if value == 'testuser': - score += 1000 + CONDITION['user']['modifier'] + elif label in CONDITION["user"]["labels"]: + if value == "testuser": + score += 1000 + CONDITION["user"]["modifier"] else: score = 0 break @@ -94,111 +94,85 @@ def calculate_score(filename): return score -@pytest.mark.parametrize( - 'default', ['default', None], ids=['default', 'no-default']) -@pytest.mark.parametrize( - 'arch', ['arch', None], ids=['arch', 'no-arch']) -@pytest.mark.parametrize( - 'system', ['system', None], ids=['system', 'no-system']) -@pytest.mark.parametrize( - 'distro', ['distro', None], ids=['distro', 'no-distro']) -@pytest.mark.parametrize( - 'cla', ['class', None], ids=['class', 'no-class']) -@pytest.mark.parametrize( - 'host', ['hostname', None], ids=['hostname', 'no-host']) -@pytest.mark.parametrize( - 'user', ['user', None], ids=['user', 'no-user']) -def test_score_values( - runner, yadm, default, arch, system, distro, cla, host, user): +@pytest.mark.parametrize("default", ["default", None], ids=["default", "no-default"]) +@pytest.mark.parametrize("arch", ["arch", None], ids=["arch", "no-arch"]) +@pytest.mark.parametrize("system", ["system", None], ids=["system", "no-system"]) +@pytest.mark.parametrize("distro", ["distro", None], ids=["distro", "no-distro"]) +@pytest.mark.parametrize("cla", ["class", None], ids=["class", "no-class"]) +@pytest.mark.parametrize("host", ["hostname", None], ids=["hostname", "no-host"]) +@pytest.mark.parametrize("user", ["user", None], ids=["user", "no-user"]) +def test_score_values(runner, yadm, default, arch, system, distro, cla, host, user): """Test score results""" # pylint: disable=too-many-branches - local_class = 'testclass' - local_arch = 'testarch' - local_system = 'testsystem' - local_distro = 'testdistro' - local_host = 'testhost' - local_user = 'testuser' - filenames = {'filename##': 0} + local_class = "testclass" + local_arch = "testarch" + local_system = "testsystem" + local_distro = "testdistro" + local_host = "testhost" + local_user = "testuser" + filenames = {"filename##": 0} if default: for filename in list(filenames): - for label in CONDITION[default]['labels']: + for label in CONDITION[default]["labels"]: newfile = filename - if not newfile.endswith('##'): - newfile += ',' + if not newfile.endswith("##"): + newfile += "," newfile += label filenames[newfile] = calculate_score(newfile) if arch: for filename in list(filenames): for match in [True, False]: - for label in CONDITION[arch]['labels']: + for label in CONDITION[arch]["labels"]: newfile = filename - if not newfile.endswith('##'): - newfile += ',' - newfile += '.'.join([ - label, - local_arch if match else 'badarch' - ]) + if not newfile.endswith("##"): + newfile += "," + newfile += ".".join([label, local_arch if match else "badarch"]) filenames[newfile] = calculate_score(newfile) if system: for filename in list(filenames): for match in [True, False]: - for label in CONDITION[system]['labels']: + for label in CONDITION[system]["labels"]: newfile = filename - if not newfile.endswith('##'): - newfile += ',' - newfile += '.'.join([ - label, - local_system if match else 'badsys' - ]) + if not newfile.endswith("##"): + newfile += "," + newfile += ".".join([label, local_system if match else "badsys"]) filenames[newfile] = calculate_score(newfile) if distro: for filename in list(filenames): for match in [True, False]: - for label in CONDITION[distro]['labels']: + for label in CONDITION[distro]["labels"]: newfile = filename - if not newfile.endswith('##'): - newfile += ',' - newfile += '.'.join([ - label, - local_distro if match else 'baddistro' - ]) + if not newfile.endswith("##"): + newfile += "," + newfile += ".".join([label, local_distro if match else "baddistro"]) filenames[newfile] = calculate_score(newfile) if cla: for filename in list(filenames): for match in [True, False]: - for label in CONDITION[cla]['labels']: + for label in CONDITION[cla]["labels"]: newfile = filename - if not newfile.endswith('##'): - newfile += ',' - newfile += '.'.join([ - label, - local_class if match else 'badclass' - ]) + if not newfile.endswith("##"): + newfile += "," + newfile += ".".join([label, local_class if match else "badclass"]) filenames[newfile] = calculate_score(newfile) if host: for filename in list(filenames): for match in [True, False]: - for label in CONDITION[host]['labels']: + for label in CONDITION[host]["labels"]: newfile = filename - if not newfile.endswith('##'): - newfile += ',' - newfile += '.'.join([ - label, - local_host if match else 'badhost' - ]) + if not newfile.endswith("##"): + newfile += "," + newfile += ".".join([label, local_host if match else "badhost"]) filenames[newfile] = calculate_score(newfile) if user: for filename in list(filenames): for match in [True, False]: - for label in CONDITION[user]['labels']: + for label in CONDITION[user]["labels"]: newfile = filename - if not newfile.endswith('##'): - newfile += ',' - newfile += '.'.join([ - label, - local_user if match else 'baduser' - ]) + if not newfile.endswith("##"): + newfile += "," + newfile += ".".join([label, local_user if match else "baduser"]) filenames[newfile] = calculate_score(newfile) script = f""" @@ -212,29 +186,29 @@ def test_score_values( local_host={local_host} local_user={local_user} """ - expected = '' - for filename in filenames: + expected = "" + for filename, score in filenames.items(): script += f""" score_file "{filename}" echo "{filename}" echo "$score" """ - expected += filename + '\n' - expected += str(filenames[filename]) + '\n' - run = runner(command=['bash'], inp=script) + expected += filename + "\n" + expected += str(score) + "\n" + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert run.out == expected -@pytest.mark.parametrize('ext', [None, 'e', 'extension']) +@pytest.mark.parametrize("ext", [None, "e", "extension"]) def test_extensions(runner, yadm, ext): """Verify extensions do not effect scores""" - local_user = 'testuser' - filename = f'filename##u.{local_user}' + local_user = "testuser" + filename = f"filename##u.{local_user}" if ext: - filename += f',{ext}.xyz' - expected = '' + filename += f",{ext}.xyz" + expected = "" script = f""" YADM_TEST=1 source {yadm} score=0 @@ -243,28 +217,28 @@ def test_extensions(runner, yadm, ext): echo "$score" """ expected = f'{1000 + CONDITION["user"]["modifier"]}\n' - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert run.out == expected def test_score_values_templates(runner, yadm): """Test score results""" - local_class = 'testclass' - local_arch = 'arch' - local_system = 'testsystem' - local_distro = 'testdistro' - local_host = 'testhost' - local_user = 'testuser' - filenames = {'filename##': 0} + local_class = "testclass" + local_arch = "arch" + local_system = "testsystem" + local_distro = "testdistro" + local_host = "testhost" + local_user = "testuser" + filenames = {"filename##": 0} for filename in list(filenames): for label in TEMPLATE_LABELS: newfile = filename - if not newfile.endswith('##'): - newfile += ',' - newfile += '.'.join([label, 'testtemplate']) + if not newfile.endswith("##"): + newfile += "," + newfile += ".".join([label, "testtemplate"]) filenames[newfile] = calculate_score(newfile) script = f""" @@ -277,33 +251,30 @@ def test_score_values_templates(runner, yadm): local_host={local_host} local_user={local_user} """ - expected = '' - for filename in filenames: + expected = "" + for filename, score in filenames.items(): script += f""" score_file "{filename}" echo "{filename}" echo "$score" """ - expected += filename + '\n' - expected += str(filenames[filename]) + '\n' - run = runner(command=['bash'], inp=script) + expected += filename + "\n" + expected += str(score) + "\n" + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert run.out == expected -@pytest.mark.parametrize( - 'cmd_generated', - [True, False], - ids=['supported-template', 'unsupported-template']) +@pytest.mark.parametrize("cmd_generated", [True, False], ids=["supported-template", "unsupported-template"]) def test_template_recording(runner, yadm, cmd_generated): """Template should be recorded if choose_template_cmd outputs a command""" - mock = 'function choose_template_cmd() { return; }' - expected = '' + mock = "function choose_template_cmd() { return; }" + expected = "" if cmd_generated: mock = 'function choose_template_cmd() { echo "test_cmd"; }' - expected = 'template recorded' + expected = "template recorded" script = f""" YADM_TEST=1 source {yadm} @@ -311,7 +282,41 @@ def test_template_recording(runner, yadm, cmd_generated): {mock} score_file "testfile##template.kind" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert run.out.rstrip() == expected + + +def test_underscores_in_distro_and_family(runner, yadm): + """Test replacing spaces in distro / distro_family with underscores""" + local_distro = "test distro" + local_distro_family = "test family" + filenames = { + "filename##distro.test distro": 1004, + "filename##distro.test-distro": 0, + "filename##distro.test_distro": 1004, + "filename##distro_family.test family": 1008, + "filename##distro_family.test-family": 0, + "filename##distro_family.test_family": 1008, + } + + script = f""" + YADM_TEST=1 source {yadm} + score=0 + local_distro="{local_distro}" + local_distro_family="{local_distro_family}" + """ + expected = "" + for filename, score in filenames.items(): + script += f""" + score_file "{filename}" + echo "{filename}" + echo "$score" + """ + expected += filename + "\n" + expected += str(score) + "\n" + run = runner(command=["bash"], inp=script) + assert run.success + assert run.err == "" + assert run.out == expected diff --git a/test/test_unit_set_local_alt_values.py b/test/test_unit_set_local_alt_values.py index 449850c..fa5749d 100644 --- a/test/test_unit_set_local_alt_values.py +++ b/test/test_unit_set_local_alt_values.py @@ -4,26 +4,26 @@ import utils @pytest.mark.parametrize( - 'override', [ + "override", + [ False, - 'class', - 'arch', - 'os', - 'hostname', - 'user', - ], + "class", + "arch", + "os", + "hostname", + "user", + ], ids=[ - 'no-override', - 'override-class', - 'override-arch', - 'override-os', - 'override-hostname', - 'override-user', - ] - ) -@pytest.mark.usefixtures('ds1_copy') -def test_set_local_alt_values( - runner, yadm, paths, tst_arch, tst_sys, tst_host, tst_user, override): + "no-override", + "override-class", + "override-arch", + "override-os", + "override-hostname", + "override-user", + ], +) +@pytest.mark.usefixtures("ds1_copy") +def test_set_local_alt_values(runner, yadm, paths, tst_arch, tst_sys, tst_host, tst_user, override): """Use issue_legacy_path_warning""" script = f""" YADM_TEST=1 source {yadm} && @@ -37,37 +37,37 @@ def test_set_local_alt_values( echo "user='$local_user'" """ - if override == 'class': - utils.set_local(paths, override, 'first') - utils.set_local(paths, override, 'override', add=True) + if override == "class": + utils.set_local(paths, override, "first") + utils.set_local(paths, override, "override", add=True) elif override: - utils.set_local(paths, override, 'override') + utils.set_local(paths, override, "override") - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" - if override == 'class': + if override == "class": assert "class='override'" in run.out else: assert "class=''" in run.out - if override == 'arch': + if override == "arch": assert "arch='override'" in run.out else: assert f"arch='{tst_arch}'" in run.out - if override == 'os': + if override == "os": assert "os='override'" in run.out else: assert f"os='{tst_sys}'" in run.out - if override == 'hostname': + if override == "hostname": assert "host='override'" in run.out else: assert f"host='{tst_host}'" in run.out - if override == 'user': + if override == "user": assert "user='override'" in run.out else: assert f"user='{tst_user}'" in run.out @@ -85,8 +85,8 @@ def test_distro_and_family(runner, yadm): echo "distro='$local_distro'" echo "distro_family='$local_distro_family'" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert "distro='testdistro'" in run.out assert "distro_family='testfamily'" in run.out diff --git a/test/test_unit_set_os.py b/test/test_unit_set_os.py index 936986f..ac61de2 100644 --- a/test/test_unit_set_os.py +++ b/test/test_unit_set_os.py @@ -4,25 +4,27 @@ import pytest @pytest.mark.parametrize( - 'proc_value, expected_os', [ - ('missing', 'uname'), - ('has microsoft inside', 'WSL'), # case insensitive - ('has Microsoft inside', 'WSL'), # case insensitive - ('another value', 'uname'), - ], ids=[ - '/proc/version missing', - '/proc/version includes ms', - '/proc/version excludes Ms', - 'another value', - ]) -def test_set_operating_system( - runner, paths, tst_sys, proc_value, expected_os): + "proc_value, expected_os", + [ + ("missing", "uname"), + ("has microsoft inside", "WSL"), # case insensitive + ("has Microsoft inside", "WSL"), # case insensitive + ("another value", "uname"), + ], + ids=[ + "/proc/version missing", + "/proc/version includes ms", + "/proc/version excludes Ms", + "another value", + ], +) +def test_set_operating_system(runner, paths, tst_sys, proc_value, expected_os): """Run set_operating_system and test result""" # Normally /proc/version (set in PROC_VERSION) is inspected to identify # WSL. During testing, we will override that value. - proc_version = paths.root.join('proc_version') - if proc_value != 'missing': + proc_version = paths.root.join("proc_version") + if proc_value != "missing": proc_version.write(proc_value) script = f""" YADM_TEST=1 source {paths.pgm} @@ -30,9 +32,9 @@ def test_set_operating_system( set_operating_system echo $OPERATING_SYSTEM """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - if expected_os == 'uname': + assert run.err == "" + if expected_os == "uname": expected_os = tst_sys assert run.out.rstrip() == expected_os diff --git a/test/test_unit_set_yadm_dir.py b/test/test_unit_set_yadm_dir.py index 32af8bf..b56c98d 100644 --- a/test/test_unit_set_yadm_dir.py +++ b/test/test_unit_set_yadm_dir.py @@ -3,25 +3,20 @@ import pytest @pytest.mark.parametrize( - 'condition', [ - 'basic', - 'override', - 'override_data', - 'xdg_config_home', - 'xdg_data_home' - ], - ) + "condition", + ["basic", "override", "override_data", "xdg_config_home", "xdg_data_home"], +) def test_set_yadm_dirs(runner, yadm, condition): """Test set_yadm_dirs""" - setup = '' - if condition == 'override': - setup = 'YADM_DIR=/override' - elif condition == 'override_data': - setup = 'YADM_DATA=/override' - elif condition == 'xdg_config_home': - setup = 'XDG_CONFIG_HOME=/xdg' - elif condition == 'xdg_data_home': - setup = 'XDG_DATA_HOME=/xdg' + setup = "" + if condition == "override": + setup = "YADM_DIR=/override" + elif condition == "override_data": + setup = "YADM_DATA=/override" + elif condition == "xdg_config_home": + setup = "XDG_CONFIG_HOME=/xdg" + elif condition == "xdg_data_home": + setup = "XDG_DATA_HOME=/xdg" script = f""" HOME=/testhome YADM_TEST=1 source {yadm} @@ -32,17 +27,17 @@ def test_set_yadm_dirs(runner, yadm, condition): echo "YADM_DIR=$YADM_DIR" echo "YADM_DATA=$YADM_DATA" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - if condition == 'basic': - assert 'YADM_DIR=/testhome/.config/yadm' in run.out - assert 'YADM_DATA=/testhome/.local/share/yadm' in run.out - elif condition == 'override': - assert 'YADM_DIR=/override' in run.out - elif condition == 'override_data': - assert 'YADM_DATA=/override' in run.out - elif condition == 'xdg_config_home': - assert 'YADM_DIR=/xdg/yadm' in run.out - elif condition == 'xdg_data_home': - assert 'YADM_DATA=/xdg/yadm' in run.out + assert run.err == "" + if condition == "basic": + assert "YADM_DIR=/testhome/.config/yadm" in run.out + assert "YADM_DATA=/testhome/.local/share/yadm" in run.out + elif condition == "override": + assert "YADM_DIR=/override" in run.out + elif condition == "override_data": + assert "YADM_DATA=/override" in run.out + elif condition == "xdg_config_home": + assert "YADM_DIR=/xdg/yadm" in run.out + elif condition == "xdg_data_home": + assert "YADM_DATA=/xdg/yadm" in run.out diff --git a/test/test_unit_template_default.py b/test/test_unit_template_default.py index 729784e..aaae0fe 100644 --- a/test/test_unit_template_default.py +++ b/test/test_unit_template_default.py @@ -1,4 +1,5 @@ """Unit tests: template_default""" + import os FILE_MODE = 0o754 @@ -12,7 +13,8 @@ LOCAL_HOST = "default_Test+@-!^Host" LOCAL_USER = "default_Test+@-!^User" LOCAL_DISTRO = "default_Test+@-!^Distro" LOCAL_DISTRO_FAMILY = "default_Test+@-!^Family" -TEMPLATE = f''' +ENV_VAR = "default_Test+@-!^Env" +TEMPLATE = f""" start of template default class = >{{{{yadm.class}}}}< default arch = >{{{{yadm.arch}}}}< @@ -30,6 +32,9 @@ Included section from else {{% if yadm.class == "wrongclass1" %}} wrong class 1 {{% endif %}} +{{% if yadm.class != "wronglcass" %}} +Included section from != +{{% endif\t\t %}} {{% if yadm.class == "{LOCAL_CLASS}" %}} Included section for class = {{{{yadm.class}}}} ({{{{yadm.class}}}} repeated) Multiple lines @@ -97,9 +102,16 @@ Included section for distro_family = \ {{% if yadm.distro_family == "wrongfamily2" %}} wrong family 2 {{% endif %}} +{{% if env.VAR == "{ENV_VAR}" %}} +Included section for env.VAR = {{{{env.VAR}}}} ({{{{env.VAR}}}} again) +{{% endif %}} +{{% if env.VAR == "wrongenvvar" %}} +wrong env.VAR +{{% endif %}} +yadm.no_such_var="{{{{ yadm.no_such_var }}}}" and env.NO_SUCH_VAR="{{{{ env.NO_SUCH_VAR }}}}" end of template -''' -EXPECTED = f''' +""" +EXPECTED = f""" start of template default class = >{LOCAL_CLASS}< default arch = >{LOCAL_ARCH}< @@ -111,6 +123,7 @@ default distro_family = >{LOCAL_DISTRO_FAMILY}< classes = >{LOCAL_CLASS2} {LOCAL_CLASS}< Included section from else +Included section from != Included section for class = {LOCAL_CLASS} ({LOCAL_CLASS} repeated) Multiple lines Included section for second class @@ -121,28 +134,30 @@ Included section for user = {LOCAL_USER} ({LOCAL_USER} repeated) Included section for distro = {LOCAL_DISTRO} ({LOCAL_DISTRO} again) Included section for distro_family = \ {LOCAL_DISTRO_FAMILY} ({LOCAL_DISTRO_FAMILY} again) +Included section for env.VAR = {ENV_VAR} ({ENV_VAR} again) +yadm.no_such_var="" and env.NO_SUCH_VAR="" end of template -''' +""" -INCLUDE_BASIC = 'basic\n' -INCLUDE_VARIABLES = '''\ +INCLUDE_BASIC = "basic\n" +INCLUDE_VARIABLES = """\ included <{{ yadm.class }}> file empty line above -''' -INCLUDE_NESTED = 'no newline at the end' +""" +INCLUDE_NESTED = "no newline at the end" -TEMPLATE_INCLUDE = '''\ +TEMPLATE_INCLUDE = """\ The first line {% include empty %} An empty file removes the line above {%include basic%} {% include "./variables.{{ yadm.os }}" %} -{% include dir/nested %} + {% include dir/nested %} Include basic again: {% include basic %} -''' -EXPECTED_INCLUDE = f'''\ +""" +EXPECTED_INCLUDE = f"""\ The first line An empty file removes the line above basic @@ -152,21 +167,57 @@ empty line above no newline at the end Include basic again: basic -''' +""" + +TEMPLATE_NESTED_IFS = """\ +{% if yadm.user == "me" %} + print1 + {% if yadm.user == "me" %} + print2 + {% else %} + no print1 + {% endif %} +{% else %} + {% if yadm.user == "me" %} + no print2 + {% else %} + no print3 + {% endif %} +{% endif %} +{% if yadm.user != "me" %} + no print4 + {% if yadm.user == "me" %} + no print5 + {% else %} + no print6 + {% endif %} +{% else %} + {% if yadm.user == "me" %} + print3 + {% else %} + no print7 + {% endif %} +{% endif %} +""" +EXPECTED_NESTED_IFS = """\ + print1 + print2 + print3 +""" def test_template_default(runner, yadm, tmpdir): """Test template_default""" - input_file = tmpdir.join('input') + input_file = tmpdir.join("input") input_file.write(TEMPLATE, ensure=True) input_file.chmod(FILE_MODE) - output_file = tmpdir.join('output') + output_file = tmpdir.join("output") # ensure overwrite works when file exists as read-only (there is some # special processing when this is encountered because some environments do # not properly overwrite read-only files) - output_file.write('existing') + output_file.write("existing") output_file.chmod(0o400) script = f""" @@ -182,9 +233,9 @@ def test_template_default(runner, yadm, tmpdir): local_distro_family="{LOCAL_DISTRO_FAMILY}" template_default "{input_file}" "{output_file}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script, env={"VAR": ENV_VAR}) assert run.success - assert run.err == '' + assert run.err == "" assert output_file.read() == EXPECTED assert os.stat(output_file).st_mode == os.stat(input_file).st_mode @@ -192,19 +243,19 @@ def test_template_default(runner, yadm, tmpdir): def test_source(runner, yadm, tmpdir): """Test yadm.source""" - input_file = tmpdir.join('input') - input_file.write('{{yadm.source}}', ensure=True) + input_file = tmpdir.join("input") + input_file.write("{{yadm.source}}", ensure=True) input_file.chmod(FILE_MODE) - output_file = tmpdir.join('output') + output_file = tmpdir.join("output") script = f""" YADM_TEST=1 source {yadm} set_awk template_default "{input_file}" "{output_file}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert output_file.read().strip() == str(input_file) assert os.stat(output_file).st_mode == os.stat(input_file).st_mode @@ -212,22 +263,22 @@ def test_source(runner, yadm, tmpdir): def test_include(runner, yadm, tmpdir): """Test include""" - empty_file = tmpdir.join('empty') - empty_file.write('', ensure=True) + empty_file = tmpdir.join("empty") + empty_file.write("", ensure=True) - basic_file = tmpdir.join('basic') + basic_file = tmpdir.join("basic") basic_file.write(INCLUDE_BASIC) - variables_file = tmpdir.join(f'variables.{LOCAL_SYSTEM}') + variables_file = tmpdir.join(f"variables.{LOCAL_SYSTEM}") variables_file.write(INCLUDE_VARIABLES) - nested_file = tmpdir.join('dir').join('nested') + nested_file = tmpdir.join("dir").join("nested") nested_file.write(INCLUDE_NESTED, ensure=True) - input_file = tmpdir.join('input') + input_file = tmpdir.join("input") input_file.write(TEMPLATE_INCLUDE) input_file.chmod(FILE_MODE) - output_file = tmpdir.join('output') + output_file = tmpdir.join("output") script = f""" YADM_TEST=1 source {yadm} @@ -236,27 +287,45 @@ def test_include(runner, yadm, tmpdir): local_system="{LOCAL_SYSTEM}" template_default "{input_file}" "{output_file}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert output_file.read() == EXPECTED_INCLUDE assert os.stat(output_file).st_mode == os.stat(input_file).st_mode +def test_nested_ifs(runner, yadm, tmpdir): + """Test nested if statements""" + + input_file = tmpdir.join("input") + input_file.write(TEMPLATE_NESTED_IFS, ensure=True) + output_file = tmpdir.join("output") + + script = f""" + YADM_TEST=1 source {yadm} + set_awk + local_user="me" + template_default "{input_file}" "{output_file}" + """ + run = runner(command=["bash"], inp=script) + assert run.success + assert run.err == "" + assert output_file.read() == EXPECTED_NESTED_IFS + + def test_env(runner, yadm, tmpdir): """Test env""" - input_file = tmpdir.join('input') - input_file.write('{{env.PWD}}', ensure=True) - input_file.chmod(FILE_MODE) - output_file = tmpdir.join('output') + input_file = tmpdir.join("input") + input_file.write("{{env.PWD}}", ensure=True) + output_file = tmpdir.join("output") script = f""" YADM_TEST=1 source {yadm} set_awk template_default "{input_file}" "{output_file}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - assert output_file.read().strip() == os.environ['PWD'] + assert run.err == "" + assert output_file.read().strip() == os.environ["PWD"] diff --git a/test/test_unit_template_esh.py b/test/test_unit_template_esh.py index 7f2f2b9..2c91c20 100644 --- a/test/test_unit_template_esh.py +++ b/test/test_unit_template_esh.py @@ -11,7 +11,7 @@ LOCAL_HOST = "esh_Test+@-!^Host" LOCAL_USER = "esh_Test+@-!^User" LOCAL_DISTRO = "esh_Test+@-!^Distro" LOCAL_DISTRO_FAMILY = "esh_Test+@-!^Family" -TEMPLATE = f''' +TEMPLATE = f""" start of template esh class = ><%=$YADM_CLASS%>< esh arch = ><%=$YADM_ARCH%>< @@ -90,8 +90,8 @@ Included esh section for distro_family = \ wrong family 2 <% fi -%> end of template -''' -EXPECTED = f''' +""" +EXPECTED = f""" start of template esh class = >{LOCAL_CLASS}< esh arch = >{LOCAL_ARCH}< @@ -111,21 +111,22 @@ Included esh section for distro = {LOCAL_DISTRO} ({LOCAL_DISTRO} again) Included esh section for distro_family = \ {LOCAL_DISTRO_FAMILY} ({LOCAL_DISTRO_FAMILY} again) end of template -''' +""" def test_template_esh(runner, yadm, tmpdir): """Test processing by esh""" + # pylint: disable=duplicate-code - input_file = tmpdir.join('input') + input_file = tmpdir.join("input") input_file.write(TEMPLATE, ensure=True) input_file.chmod(FILE_MODE) - output_file = tmpdir.join('output') + output_file = tmpdir.join("output") # ensure overwrite works when file exists as read-only (there is some # special processing when this is encountered because some environments do # not properly overwrite read-only files) - output_file.write('existing') + output_file.write("existing") output_file.chmod(0o400) script = f""" @@ -140,9 +141,9 @@ def test_template_esh(runner, yadm, tmpdir): local_distro_family="{LOCAL_DISTRO_FAMILY}" template_esh "{input_file}" "{output_file}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert output_file.read().strip() == str(EXPECTED).strip() assert os.stat(output_file).st_mode == os.stat(input_file).st_mode @@ -150,17 +151,17 @@ def test_template_esh(runner, yadm, tmpdir): def test_source(runner, yadm, tmpdir): """Test YADM_SOURCE""" - input_file = tmpdir.join('input') - input_file.write('<%= $YADM_SOURCE %>', ensure=True) + input_file = tmpdir.join("input") + input_file.write("<%= $YADM_SOURCE %>", ensure=True) input_file.chmod(FILE_MODE) - output_file = tmpdir.join('output') + output_file = tmpdir.join("output") script = f""" YADM_TEST=1 source {yadm} template_esh "{input_file}" "{output_file}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert output_file.read().strip() == str(input_file) assert os.stat(output_file).st_mode == os.stat(input_file).st_mode diff --git a/test/test_unit_template_j2.py b/test/test_unit_template_j2.py index 4042a2d..750ee8c 100644 --- a/test/test_unit_template_j2.py +++ b/test/test_unit_template_j2.py @@ -1,5 +1,6 @@ """Unit tests: template_j2cli & template_envtpl""" import os + import pytest FILE_MODE = 0o754 @@ -12,7 +13,7 @@ LOCAL_HOST = "j2_Test+@-!^Host" LOCAL_USER = "j2_Test+@-!^User" LOCAL_DISTRO = "j2_Test+@-!^Distro" LOCAL_DISTRO_FAMILY = "j2_Test+@-!^Family" -TEMPLATE = f''' +TEMPLATE = f""" start of template j2 class = >{{{{YADM_CLASS}}}}< j2 arch = >{{{{YADM_ARCH}}}}< @@ -93,8 +94,8 @@ Included j2 section for distro_family = \ wrong family 2 {{%- endif %}} end of template -''' -EXPECTED = f''' +""" +EXPECTED = f""" start of template j2 class = >{LOCAL_CLASS}< j2 arch = >{LOCAL_ARCH}< @@ -115,22 +116,23 @@ Included j2 section for distro = {LOCAL_DISTRO} ({LOCAL_DISTRO} again) Included j2 section for distro_family = \ {LOCAL_DISTRO_FAMILY} ({LOCAL_DISTRO_FAMILY} again) end of template -''' +""" -@pytest.mark.parametrize('processor', ('j2cli', 'envtpl')) +@pytest.mark.parametrize("processor", ("j2cli", "envtpl")) def test_template_j2(runner, yadm, tmpdir, processor): """Test processing by j2cli & envtpl""" + # pylint: disable=duplicate-code - input_file = tmpdir.join('input') + input_file = tmpdir.join("input") input_file.write(TEMPLATE, ensure=True) input_file.chmod(FILE_MODE) - output_file = tmpdir.join('output') + output_file = tmpdir.join("output") # ensure overwrite works when file exists as read-only (there is some # special processing when this is encountered because some environments do # not properly overwrite read-only files) - output_file.write('existing') + output_file.write("existing") output_file.chmod(0o400) script = f""" @@ -145,28 +147,28 @@ def test_template_j2(runner, yadm, tmpdir, processor): local_distro_family="{LOCAL_DISTRO_FAMILY}" template_{processor} "{input_file}" "{output_file}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert output_file.read() == EXPECTED assert os.stat(output_file).st_mode == os.stat(input_file).st_mode -@pytest.mark.parametrize('processor', ('j2cli', 'envtpl')) +@pytest.mark.parametrize("processor", ("j2cli", "envtpl")) def test_source(runner, yadm, tmpdir, processor): """Test YADM_SOURCE""" - input_file = tmpdir.join('input') - input_file.write('{{YADM_SOURCE}}', ensure=True) + input_file = tmpdir.join("input") + input_file.write("{{YADM_SOURCE}}", ensure=True) input_file.chmod(FILE_MODE) - output_file = tmpdir.join('output') + output_file = tmpdir.join("output") script = f""" YADM_TEST=1 source {yadm} template_{processor} "{input_file}" "{output_file}" """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' + assert run.err == "" assert output_file.read().strip() == str(input_file) assert os.stat(output_file).st_mode == os.stat(input_file).st_mode diff --git a/test/test_unit_upgrade.py b/test/test_unit_upgrade.py index 3463740..cf4f6f4 100644 --- a/test/test_unit_upgrade.py +++ b/test/test_unit_upgrade.py @@ -2,21 +2,21 @@ import pytest -@pytest.mark.parametrize('condition', ['override', 'equal', 'existing_repo']) +@pytest.mark.parametrize("condition", ["override", "equal", "existing_repo"]) def test_upgrade_errors(tmpdir, runner, yadm, condition): """Test upgrade() error conditions""" - home = tmpdir.mkdir('home') - yadm_dir = home.join('.config/yadm') - yadm_data = home.join('.local/share/yadm') - override = '' - if condition == 'override': - override = 'override' - if condition == 'equal': + home = tmpdir.mkdir("home") + yadm_dir = home.join(".config/yadm") + yadm_data = home.join(".local/share/yadm") + override = "" + if condition == "override": + override = "override" + if condition == "equal": yadm_data = yadm_dir - if condition == 'existing_repo': - yadm_dir.ensure_dir('repo.git') - yadm_data.ensure_dir('repo.git') + if condition == "existing_repo": + yadm_dir.ensure_dir("repo.git") + yadm_data.ensure_dir("repo.git") script = f""" YADM_TEST=1 source {yadm} @@ -27,17 +27,16 @@ def test_upgrade_errors(tmpdir, runner, yadm, condition): YADM_OVERRIDE_REPO="{override}" upgrade """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.failure - assert 'Unable to upgrade' in run.err - if condition in ['override', 'equal']: - assert 'Paths have been overridden' in run.err - elif condition == 'existing_repo': - assert 'already exists' in run.err + assert "Unable to upgrade" in run.err + if condition in ["override", "equal"]: + assert "Paths have been overridden" in run.err + elif condition == "existing_repo": + assert "already exists" in run.err -@pytest.mark.parametrize( - 'condition', ['no-paths', 'untracked', 'tracked', 'submodules']) +@pytest.mark.parametrize("condition", ["no-paths", "untracked", "tracked", "submodules"]) def test_upgrade(tmpdir, runner, yadm, condition): """Test upgrade() @@ -45,21 +44,21 @@ def test_upgrade(tmpdir, runner, yadm, condition): mock for git. echo will return true, simulating a positive result from "git ls-files". Also echo will report the parameters for "git mv". """ - legacy_paths = ('config', 'encrypt', 'bootstrap', 'hooks/pre_cmd') - home = tmpdir.mkdir('home') - yadm_dir = home.join('.config/yadm') - yadm_data = home.join('.local/share/yadm') - yadm_legacy = home.join('.yadm') + legacy_paths = ("config", "encrypt", "bootstrap", "hooks/pre_cmd") + home = tmpdir.mkdir("home") + yadm_dir = home.join(".config/yadm") + yadm_data = home.join(".local/share/yadm") + yadm_legacy = home.join(".yadm") - if condition != 'no-paths': - yadm_dir.join('repo.git/config').write('test-repo', ensure=True) - yadm_dir.join('files.gpg').write('files.gpg', ensure=True) + if condition != "no-paths": + yadm_dir.join("repo.git/config").write("test-repo", ensure=True) + yadm_dir.join("files.gpg").write("files.gpg", ensure=True) for path in legacy_paths: yadm_legacy.join(path).write(path, ensure=True) mock_git = "" - if condition != 'no-paths': - mock_git = f''' + if condition != "no-paths": + mock_git = f""" function git() {{ echo "$@" if [[ "$*" = *"submodule status" ]]; then @@ -71,7 +70,7 @@ def test_upgrade(tmpdir, runner, yadm, condition): fi return 0 }} - ''' + """ script = f""" YADM_TEST=1 source {yadm} @@ -85,38 +84,30 @@ def test_upgrade(tmpdir, runner, yadm, condition): function cd {{ echo "$@";}} upgrade """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success - assert run.err == '' - if condition == 'no-paths': - assert 'Upgrade is not necessary' in run.out + assert run.err == "" + if condition == "no-paths": + assert "Upgrade is not necessary" in run.out else: - for (lpath, npath) in [ - ('repo.git', 'repo.git'), ('files.gpg', 'archive')]: - expected = ( - f'Moving {yadm_dir.join(lpath)} ' - f'to {yadm_data.join(npath)}') + for lpath, npath in [("repo.git", "repo.git"), ("files.gpg", "archive")]: + expected = f"Moving {yadm_dir.join(lpath)} " f"to {yadm_data.join(npath)}" assert expected in run.out for path in legacy_paths: - expected = ( - f'Moving {yadm_legacy.join(path)} ' - f'to {yadm_dir.join(path)}') + expected = f"Moving {yadm_legacy.join(path)} " f"to {yadm_dir.join(path)}" assert expected in run.out - if condition == 'untracked': - assert 'test-repo' in yadm_data.join('repo.git/config').read() - assert 'files.gpg' in yadm_data.join('archive').read() + if condition == "untracked": + assert "test-repo" in yadm_data.join("repo.git/config").read() + assert "files.gpg" in yadm_data.join("archive").read() for path in legacy_paths: assert path in yadm_dir.join(path).read() - elif condition in ['tracked', 'submodules']: - expected = ( - f'mv {yadm_dir.join("files.gpg")} ' - f'{yadm_data.join("archive")}') + elif condition in ["tracked", "submodules"]: + expected = f'mv {yadm_dir.join("files.gpg")} ' f'{yadm_data.join("archive")}' assert expected in run.out - assert 'files tracked by yadm have been renamed' in run.out - if condition == 'submodules': - assert 'submodule deinit -- mymodule' in run.out - assert 'submodule update --init --recursive -- mymodule' \ - in run.out + assert "files tracked by yadm have been renamed" in run.out + if condition == "submodules": + assert "submodule deinit -- mymodule" in run.out + assert "submodule update --init --recursive -- mymodule" in run.out else: - assert 'submodule deinit' not in run.out - assert 'submodule update --init --recursive' not in run.out + assert "submodule deinit" not in run.out + assert "submodule update --init --recursive" not in run.out diff --git a/test/test_unit_x_program.py b/test/test_unit_x_program.py index 8302f3c..33b8473 100644 --- a/test/test_unit_x_program.py +++ b/test/test_unit_x_program.py @@ -1,28 +1,30 @@ """Unit tests: yadm.[git,gpg]-program""" import os + import pytest @pytest.mark.parametrize( - 'executable, success, value, match', [ - (None, True, 'program', None), - ('cat', True, 'cat', None), - ('badprogram', False, None, 'badprogram'), - ], ids=[ - 'executable missing', - 'valid alternative', - 'invalid alternative', - ]) -@pytest.mark.parametrize('program', ['git', 'gpg']) -def test_x_program( - runner, yadm_cmd, paths, program, executable, success, value, match): + "executable, success, value, match", + [ + (None, True, "program", None), + ("cat", True, "cat", None), + ("badprogram", False, None, "badprogram"), + ], + ids=[ + "executable missing", + "valid alternative", + "invalid alternative", + ], +) +@pytest.mark.parametrize("program", ["git", "gpg"]) +def test_x_program(runner, yadm_cmd, paths, program, executable, success, value, match): """Set yadm.X-program, and test result of require_X""" # set configuration if executable: - os.system(' '.join(yadm_cmd( - 'config', f'yadm.{program}-program', executable))) + os.system(" ".join(yadm_cmd("config", f"yadm.{program}-program", executable))) # test require_[git,gpg] script = f""" @@ -32,11 +34,11 @@ def test_x_program( require_{program} echo ${program.upper()}_PROGRAM """ - run = runner(command=['bash'], inp=script) + run = runner(command=["bash"], inp=script) assert run.success == success # [GIT,GPG]_PROGRAM set correctly - if value == 'program': + if value == "program": assert run.out.rstrip() == program elif value: assert run.out.rstrip() == value @@ -45,4 +47,4 @@ def test_x_program( if match: assert match in run.err else: - assert run.err == '' + assert run.err == "" diff --git a/test/test_upgrade.py b/test/test_upgrade.py index 2d4d697..8ab1e94 100644 --- a/test/test_upgrade.py +++ b/test/test_upgrade.py @@ -1,129 +1,131 @@ """Test upgrade""" import os + import pytest @pytest.mark.parametrize( - 'versions', [ - ('1.12.0', '2.5.0'), - ('1.12.0',), - ('2.5.0',), - ], ids=[ - '1.12.0 -> 2.5.0 -> latest', - '1.12.0 -> latest', - '2.5.0 -> latest', - ]) -@pytest.mark.parametrize( - 'submodule', [False, True], - ids=['no submodule', 'with submodules']) + "versions", + [ + ("1.12.0", "2.5.0"), + ("1.12.0",), + ("2.5.0",), + ], + ids=[ + "1.12.0 -> 2.5.0 -> latest", + "1.12.0 -> latest", + "2.5.0 -> latest", + ], +) +@pytest.mark.parametrize("submodule", [False, True], ids=["no submodule", "with submodules"]) def test_upgrade(tmpdir, runner, versions, submodule): """Upgrade tests""" # pylint: disable=too-many-statements - home = tmpdir.mkdir('HOME') - env = {'HOME': str(home)} + home = tmpdir.mkdir("HOME") + env = {"HOME": str(home)} + runner(["git", "config", "--global", "init.defaultBranch", "master"], env=env) + runner(["git", "config", "--global", "protocol.file.allow", "always"], env=env) if submodule: - ext_repo = tmpdir.mkdir('ext_repo') - ext_repo.join('afile').write('some data') + ext_repo = tmpdir.mkdir("ext_repo") + ext_repo.join("afile").write("some data") - for cmd in (('init',), ('add', 'afile'), ('commit', '-m', 'test')): - run = runner(['git', '-C', str(ext_repo), *cmd]) + for cmd in (("init",), ("add", "afile"), ("commit", "-m", "test")): + run = runner(["git", "-C", str(ext_repo), *cmd]) assert run.success - os.environ.pop('XDG_CONFIG_HOME', None) - os.environ.pop('XDG_DATA_HOME', None) + os.environ.pop("XDG_CONFIG_HOME", None) + os.environ.pop("XDG_DATA_HOME", None) def run_version(version, *args, check_stderr=True): - yadm = 'yadm-%s' % version if version else '/yadm/yadm' + yadm = f"yadm-{version}" if version else "/yadm/yadm" run = runner([yadm, *args], shell=True, cwd=str(home), env=env) assert run.success if check_stderr: - assert run.err == '' + assert run.err == "" return run # Initialize the repo with the first version first = versions[0] - run_version(first, 'init') + run_version(first, "init") - home.join('file').write('some data') - run_version(first, 'add', 'file') - run_version(first, 'commit', '-m', '"First commit"') + home.join("file").write("some data") + run_version(first, "add", "file") + run_version(first, "commit", "-m", '"First commit"') if submodule: # When upgrading via 2.5.0 we can't have a submodule that's been added # after being cloned as 2.5.0 fails the upgrade in that case. - can_upgrade_cloned_submodule = '2.5.0' not in versions[1:] + can_upgrade_cloned_submodule = "2.5.0" not in versions[1:] if can_upgrade_cloned_submodule: # Check out a repo and then add it as a submodule - run = runner(['git', '-C', str(home), 'clone', str(ext_repo), 'b']) + run = runner(["git", "-C", str(home), "clone", str(ext_repo), "b"]) assert run.success - run_version(first, 'submodule', 'add', str(ext_repo), 'b') + run_version(first, "submodule", "add", str(ext_repo), "b") # Add submodule without first checking it out - run_version(first, 'submodule', 'add', str(ext_repo), 'a', - check_stderr=False) - run_version(first, 'submodule', 'add', str(ext_repo), 'c', - check_stderr=False) + run_version(first, "submodule", "add", str(ext_repo), "a", check_stderr=False) + run_version(first, "submodule", "add", str(ext_repo), "c", check_stderr=False) - run_version(first, 'commit', '-m', '"Add submodules"') + run_version(first, "commit", "-m", '"Add submodules"') - for path in ('.yadm', '.config/yadm'): + for path in (".yadm", ".config/yadm"): yadm_dir = home.join(path) if yadm_dir.exists(): break - yadm_dir.join('bootstrap').write('init stuff') - run_version(first, 'add', yadm_dir.join('bootstrap')) - run_version(first, 'commit', '-m', 'bootstrap') + yadm_dir.join("bootstrap").write("init stuff") + run_version(first, "add", yadm_dir.join("bootstrap")) + run_version(first, "commit", "-m", "bootstrap") - yadm_dir.join('encrypt').write('secret') + yadm_dir.join("encrypt").write("secret") - hooks_dir = yadm_dir.mkdir('hooks') - hooks_dir.join('pre_status').write('status') - hooks_dir.join('post_commit').write('commit') + hooks_dir = yadm_dir.mkdir("hooks") + hooks_dir.join("pre_status").write("status") + hooks_dir.join("post_commit").write("commit") - run_version(first, 'config', 'local.class', 'test') - run_version(first, 'config', 'foo.bar', 'true') + run_version(first, "config", "local.class", "test") + run_version(first, "config", "foo.bar", "true") # Run upgrade with intermediate versions and latest latest = None for version in versions[1:] + (latest,): - run = run_version(version, 'upgrade', check_stderr=not submodule) + run = run_version(version, "upgrade", check_stderr=not submodule) if submodule: lines = run.err.splitlines() if can_upgrade_cloned_submodule: - assert 'Migrating git directory of' in lines[0] - assert str(home.join('b/.git')) in lines[1] - assert str(yadm_dir.join('repo.git/modules/b')) in lines[2] + assert "Migrating git directory of" in lines[0] + assert str(home.join("b/.git")) in lines[1] + assert str(yadm_dir.join("repo.git/modules/b")) in lines[2] del lines[:3] for line in lines: - assert line.startswith('Submodule') - assert 'registered for path' in line + assert line.startswith("Submodule") + assert "registered for path" in line # Verify result for the final upgrade - run_version(latest, 'status') + run_version(latest, "status") - run = run_version(latest, 'show', 'HEAD:file') - assert run.out == 'some data' + run = run_version(latest, "show", "HEAD:file") + assert run.out == "some data" if submodule: if can_upgrade_cloned_submodule: - assert home.join('b/afile').read() == 'some data' - assert home.join('a/afile').read() == 'some data' - assert home.join('c/afile').read() == 'some data' + assert home.join("b/afile").read() == "some data" + assert home.join("a/afile").read() == "some data" + assert home.join("c/afile").read() == "some data" - yadm_dir = home.join('.config/yadm') + yadm_dir = home.join(".config/yadm") - assert yadm_dir.join('bootstrap').read() == 'init stuff' - assert yadm_dir.join('encrypt').read() == 'secret' + assert yadm_dir.join("bootstrap").read() == "init stuff" + assert yadm_dir.join("encrypt").read() == "secret" - hooks_dir = yadm_dir.join('hooks') - assert hooks_dir.join('pre_status').read() == 'status' - assert hooks_dir.join('post_commit').read() == 'commit' + hooks_dir = yadm_dir.join("hooks") + assert hooks_dir.join("pre_status").read() == "status" + assert hooks_dir.join("post_commit").read() == "commit" - run = run_version(latest, 'config', 'local.class') - assert run.out.rstrip() == 'test' + run = run_version(latest, "config", "local.class") + assert run.out.rstrip() == "test" - run = run_version(latest, 'config', 'foo.bar') - assert run.out.rstrip() == 'true' + run = run_version(latest, "config", "foo.bar") + assert run.out.rstrip() == "true" diff --git a/test/test_version.py b/test/test_version.py index d440d3b..52b18ab 100644 --- a/test/test_version.py +++ b/test/test_version.py @@ -1,38 +1,36 @@ """Test version""" import re + import pytest -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def expected_version(yadm): """ Expected semantic version number. This is taken directly out of yadm, searching for the VERSION= string. """ - yadm_version = re.findall( - r'VERSION=([^\n]+)', - open(yadm).read()) + with open(yadm, encoding="utf-8") as source_file: + yadm_version = re.findall(r"VERSION=([^\n]+)", source_file.read()) if yadm_version: return yadm_version[0] - pytest.fail(f'version not found in {yadm}') - return 'not found' + pytest.fail(f"version not found in {yadm}") + return "not found" def test_semantic_version(expected_version): """Version is semantic""" # semantic version conforms to MAJOR.MINOR.PATCH - assert re.search(r'^\d+\.\d+\.\d+$', expected_version), ( - 'does not conform to MAJOR.MINOR.PATCH') + assert re.search(r"^\d+\.\d+\.\d+$", expected_version), "does not conform to MAJOR.MINOR.PATCH" -@pytest.mark.parametrize('cmd', ['--version', 'version']) -def test_reported_version( - runner, yadm_cmd, cmd, expected_version): +@pytest.mark.parametrize("cmd", ["--version", "version"]) +def test_reported_version(runner, yadm_cmd, cmd, expected_version): """Report correct version and bash/git versions""" run = runner(command=yadm_cmd(cmd)) assert run.success - assert run.err == '' - assert 'bash version' in run.out - assert 'git version' in run.out - assert run.out.endswith(f'\nyadm version {expected_version}\n') + assert run.err == "" + assert "bash version" in run.out + assert "git version" in run.out + assert run.out.endswith(f"\nyadm version {expected_version}\n") diff --git a/test/utils.py b/test/utils.py index 67a9e53..c36ecac 100644 --- a/test/utils.py +++ b/test/utils.py @@ -3,38 +3,42 @@ This module holds values/functions common to multiple tests. """ -import re import os +import re -ALT_FILE1 = 'test_alt' -ALT_FILE2 = 'test alt/test alt' -ALT_DIR = 'test alt/test alt dir' +ALT_FILE1 = "test_alt" +ALT_FILE2 = "test alt/test alt" +ALT_DIR = "test alt/test alt dir" # Directory based alternates must have a tracked contained file. # This will be the test contained file name -CONTAINED = 'contained_file' +CONTAINED = "contained_file" # These variables are used for making include files which will be processed # within jinja templates -INCLUDE_FILE = 'inc_file' -INCLUDE_DIRS = ['', 'test alt'] -INCLUDE_CONTENT = '8780846c02e34c930d0afd127906668f' +INCLUDE_FILE = "inc_file" +INCLUDE_DIRS = ["", "test alt"] +INCLUDE_CONTENT = "8780846c02e34c930d0afd127906668f" def set_local(paths, variable, value, add=False): """Set local override""" add = "--add" if add else "" - os.system( - f'GIT_DIR={str(paths.repo)} ' - f'git config --local {add} "local.{variable}" "{value}"' - ) + os.system(f"GIT_DIR={str(paths.repo)} " f'git config --local {add} "local.{variable}" "{value}"') -def create_alt_files(paths, suffix, - preserve=False, tracked=True, - encrypt=False, exclude=False, - content=None, includefile=False, - yadm_alt=False, yadm_dir=None): +def create_alt_files( + paths, + suffix, + preserve=False, + tracked=True, + encrypt=False, + exclude=False, + content=None, + includefile=False, + yadm_alt=False, + yadm_dir=None, +): """Create new files, and add to the repo This is used for testing alternate files. In each case, a suffix is @@ -42,7 +46,7 @@ def create_alt_files(paths, suffix, repo handling are dependent upon the function arguments. """ - basepath = yadm_dir.join('alt') if yadm_alt else paths.work + basepath = yadm_dir.join("alt") if yadm_alt else paths.work if not preserve: for remove_path in (ALT_FILE1, ALT_FILE2, ALT_DIR): @@ -60,28 +64,28 @@ def create_alt_files(paths, suffix, # Do not test directory support for jinja alternates test_paths = [new_file1, new_file2] test_names = [ALT_FILE1, ALT_FILE2] - if not re.match(r'##(t$|t\.|template|yadm)', suffix): + if not re.match(r"##(t$|t\.|template|yadm)", suffix): test_paths += [new_dir] test_names += [ALT_DIR] for test_path in test_paths: if content: - test_path.write('\n' + content, mode='a', ensure=True) + test_path.write("\n" + content, mode="a", ensure=True) assert test_path.exists() _create_includefiles(includefile, test_paths, basepath) _create_tracked(tracked, test_paths, paths) - prefix = '.config/yadm/alt/' if yadm_alt else '' + prefix = ".config/yadm/alt/" if yadm_alt else "" _create_encrypt(encrypt, test_names, suffix, paths, exclude, prefix) def parse_alt_output(output, linked=True): """Parse output of 'alt', and return list of linked files""" - regex = r'Creating (.+) from template (.+)$' + regex = r"Creating (.+) from template (.+)$" if linked: - regex = r'Linking (.+) to (.+)$' - parsed_list = dict() + regex = r"Linking (.+) to (.+)$" + parsed_list = {} for line in output.splitlines(): match = re.match(regex, line) if match: @@ -95,7 +99,7 @@ def parse_alt_output(output, linked=True): def _create_includefiles(includefile, test_paths, basepath): if includefile: for dpath in INCLUDE_DIRS: - incfile = basepath.join(dpath + '/' + INCLUDE_FILE) + incfile = basepath.join(dpath + "/" + INCLUDE_FILE) incfile.write(INCLUDE_CONTENT, ensure=True) test_paths += [incfile] @@ -110,8 +114,6 @@ def _create_tracked(tracked, test_paths, paths): def _create_encrypt(encrypt, test_names, suffix, paths, exclude, prefix): if encrypt: for encrypt_name in test_names: - paths.encrypt.write( - f'{prefix + encrypt_name + suffix}\n', mode='a') + paths.encrypt.write(f"{prefix + encrypt_name + suffix}\n", mode="a") if exclude: - paths.encrypt.write( - f'!{prefix + encrypt_name + suffix}\n', mode='a') + paths.encrypt.write(f"!{prefix + encrypt_name + suffix}\n", mode="a") diff --git a/yadm b/yadm index 2a741e1..e5dc07c 100755 --- a/yadm +++ b/yadm @@ -1,6 +1,6 @@ #!/bin/sh # yadm - Yet Another Dotfiles Manager -# Copyright (C) 2015-2022 Tim Byrne +# Copyright (C) 2015-2024 Tim Byrne # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,7 +21,7 @@ if [ -z "$BASH_VERSION" ]; then [ "$YADM_TEST" != 1 ] && exec bash "$0" "$@" fi -VERSION=3.2.1 +VERSION=3.3.0 YADM_WORK="$HOME" YADM_DIR= @@ -204,14 +204,14 @@ function score_file() { return fi elif [[ "$label" =~ ^(d|distro)$ ]]; then - if [ "$value" = "$local_distro" ]; then + if [ "${value/\ /_}" = "${local_distro/\ /_}" ]; then score=$((score + 4)) else score=0 return fi elif [[ "$label" =~ ^(f|distro_family)$ ]]; then - if [ "$value" = "$local_distro_family" ]; then + if [ "${value/\ /_}" = "${local_distro_family/\ /_}" ]; then score=$((score + 8)) else score=0 @@ -289,18 +289,18 @@ function record_score() { # increase the index of any existing alt_template_cmds new_cmds=() for cmd_index in "${!alt_template_cmds[@]}"; do - new_cmds[$((cmd_index+1))]="${alt_template_cmds[$cmd_index]}" + new_cmds[cmd_index+1]="${alt_template_cmds[$cmd_index]}" done alt_template_cmds=() for cmd_index in "${!new_cmds[@]}"; do - alt_template_cmds[$cmd_index]="${new_cmds[$cmd_index]}" + alt_template_cmds[cmd_index]="${new_cmds[$cmd_index]}" done else alt_targets+=("$tgt") # set index to the last index (newly created one) for index in "${!alt_targets[@]}"; do :; done # and set its initial score to zero - alt_scores[$index]=0 + alt_scores[index]=0 fi fi @@ -309,8 +309,8 @@ function record_score() { # record higher scoring sources if [ "$score" -gt "${alt_scores[$index]}" ]; then - alt_scores[$index]="$score" - alt_sources[$index]="$src" + alt_scores[index]="$score" + alt_sources[index]="$src" fi } @@ -336,8 +336,8 @@ function record_template() { fi # record the template command, last one wins - alt_template_cmds[$index]="$cmd" - alt_sources[$index]="$src" + alt_template_cmds[index]="$cmd" + alt_sources[index]="$src" } @@ -368,93 +368,119 @@ function template_default() { # the explicit "space + tab" character class used below is used because not # all versions of awk seem to support the POSIX character classes [[:blank:]] read -r -d '' awk_pgm << "EOF" -# built-in default template processor BEGIN { - blank = "[ ]" - c["class"] = class - c["classes"] = classes - c["arch"] = arch - c["os"] = os - c["hostname"] = host - c["user"] = user - c["distro"] = distro - c["distro_family"] = distro_family - c["source"] = source - ifs = "^{%" blank "*if" - els = "^{%" blank "*else" blank "*%}$" - end = "^{%" blank "*endif" blank "*%}$" - skp = "^{%" blank "*(if|else|endif)" - vld = conditions() - inc_start = "^{%" blank "*include" blank "+\"?" - inc_end = "\"?" blank "*%}$" - inc = inc_start ".+" inc_end - prt = 1 - err = 0 -} -END { exit err } -{ replace_vars() } # variable replacements -$0 ~ vld, $0 ~ end { - if ($0 ~ vld || $0 ~ end) prt=1; - if ($0 ~ els) prt=0; - if ($0 ~ skp) next; -} -($0 ~ ifs && $0 !~ vld), $0 ~ end { - if ($0 ~ ifs && $0 !~ vld) prt=0; - if ($0 ~ els || $0 ~ end) prt=1; - if ($0 ~ skp) next; -} -{ if (!prt) next } -$0 ~ inc { - file = $0 - sub(inc_start, "", file) - sub(inc_end, "", file) - sub(/^[^\/].*$/, source_dir "/&", file) + classes = ARGV[2] + for (i = 3; i < ARGC; ++i) { + classes = classes "\n" ARGV[i] + } + yadm["class"] = class + yadm["classes"] = classes + yadm["arch"] = arch + yadm["os"] = os + yadm["hostname"] = host + yadm["user"] = user + yadm["distro"] = distro + yadm["distro_family"] = distro_family + yadm["source"] = source - while ((res = getline 0) { - replace_vars() - print + VARIABLE = "(env|yadm)\\.[a-zA-Z0-9_]+" + + current = 0 + filename[current] = ARGV[1] + line[current] = 0 + + level = 0 + skip[level] = 0 + + for (; current >= 0; --current) { + while ((res = getline 0) { + ++line[current] + if ($0 ~ "^[ \t]*\\{%[ \t]*if[ \t]+" VARIABLE "[ \t]*[!=]=[ \t]*\".*\"[ \t]*%\\}$") { + if (skip[level]) { skip[++level] = 1; continue } + + match($0, VARIABLE) + lhs = substr($0, RSTART, RLENGTH) + match($0, /[!=]=/) + op = substr($0, RSTART, RLENGTH) + match($0, /".*"/) + rhs = replace_vars(substr($0, RSTART + 1, RLENGTH - 2)) + + if (lhs == "yadm.class") { + lhs = "not" rhs + split(classes, cls_array, "\n") + for (idx in cls_array) { + if (rhs == cls_array[idx]) { lhs = rhs; break } + } + } + else { + lhs = replace_vars("{{" lhs "}}") + } + + if (op == "==") { skip[++level] = lhs != rhs } + else { skip[++level] = lhs == rhs } + } + else if (/^[ \t]*\{%[ \t]*else[ \t]*%\}$/) { + if (level == 0 || skip[level] < 0) { error("else without matching if") } + skip[level] = skip[level] ? skip[level - 1] : -1 + } + else if (/^[ \t]*\{%[ \t]*endif[ \t]*%\}$/) { + if (--level < 0) { error("endif without matching if") } + } + else if (!skip[level]) { + $0 = replace_vars($0) + if (match($0, /^[ \t]*\{%[ \t]*include[ \t]+("[^"]+"|[^"]+)[ \t]*%\}$/)) { + include = $0 + sub(/^[ \t]*\{%[ \t]*include[ \t]+"?/, "", include) + sub(/"?[ \t]*%\}$/, "", include) + if (index(include, "/") != 1) { + include = source_dir "/" include + } + filename[++current] = include + line[current] = 0 + } + else { print } + } + } + if (res >= 0) { close(filename[current]) } + else if (current == 0) { error("could not read input file") } + else { --current; error("could not read include file '" filename[current + 1] "'") } } - if (res < 0) { - printf "%s:%d: error: could not read '%s'\n", FILENAME, NR, file | "cat 1>&2" - err = 1 + if (level > 0) { + current = 0 + error("unterminated if") } - close(file) - next + exit 0 } -{ print } -function replace_vars() { - for (label in c) { - gsub(("{{" blank "*yadm\\." label blank "*}}"), c[label]) - } - for (label in ENVIRON) { - gsub(("{{" blank "*env\\." label blank "*}}"), ENVIRON[label]) - } +function error(text) { + printf "%s:%d: error: %s\n", + filename[current], line[current], text > "/dev/stderr" + exit 1 } -function condition_helper(label, value) { - gsub(/[\\.^$(){}\[\]|*+?]/, "\\\\&", value) - return sprintf("yadm\\.%s" blank "*==" blank "*\"%s\"", label, value) -} -function conditions() { - pattern = ifs blank "+(" - for (label in c) { - if (label != "class") { - value = c[label] - pattern = sprintf("%s%s|", pattern, condition_helper(label, value)); +function replace_vars(input) { + output = "" + while (match(input, "\\{\\{[ \t]*" VARIABLE "[ \t]*\\}\\}")) { + if (RSTART > 1) { + output = output substr(input, 0, RSTART - 1) + } + data = substr(input, RSTART + 2, RLENGTH - 4) + input = substr(input, RSTART + RLENGTH) + + gsub(/[ \t]+/, "", data) + split(data, fields, /\./) + + if (fields[1] == "env") { + output = output ENVIRON[fields[2]] + } + else { + output = output yadm[fields[2]] } } - split(classes, cls_array, "\n") - for (idx in cls_array) { - value = cls_array[idx] - pattern = sprintf("%s%s|", pattern, condition_helper("class", value)); - } - sub(/\|$/, ")" blank "*%}$", pattern) - return pattern + return output input } EOF - local source_dir yadm_classes content + local source_dir content source_dir=$(dirname "$input") - yadm_classes=$(join_string $'\n' "${local_classes[@]}") content=$("${AWK_PROGRAM[0]}" \ -v class="$local_class" \ -v arch="$local_arch" \ @@ -465,9 +491,8 @@ EOF -v distro_family="$local_distro_family" \ -v source="$input" \ -v source_dir="$source_dir" \ - -v classes="$yadm_classes" \ "$awk_pgm" \ - "$input") + "$input" "${local_classes[@]}") move_file "$input" "$output" "$content" } diff --git a/yadm.1 b/yadm.1 index 8048f0a..5dd08fd 100644 --- a/yadm.1 +++ b/yadm.1 @@ -1,5 +1,5 @@ .\" vim: set spell so=8: -.TH yadm 1 "17 March 2022" "3.2.1" +.TH yadm 1 "8 November 2024" "3.3.0" .SH NAME @@ -746,7 +746,7 @@ and .BR openssl (1) are supported. gpg is used by default, but openssl can be configured with the -.I yadm.cypher +.I yadm.cipher configuration. To use this feature, a list of patterns must be created and saved as @@ -989,7 +989,7 @@ Commit a new set of encrypted files Report issues or create pull requests at GitHub: -https://github.com/TheLocehiliosan/yadm/issues +https://github.com/yadm-dev/yadm/issues .SH AUTHOR diff --git a/yadm.md b/yadm.md index 73890d8..41cf531 100644 --- a/yadm.md +++ b/yadm.md @@ -1,6 +1,4 @@ - - ## NAME yadm - Yet Another Dotfiles Manager @@ -42,21 +40,21 @@ ## DESCRIPTION - yadm is a tool for managing a collection of files across multiple com- - puters, using a shared Git repository. In addition, yadm provides a - feature to select alternate versions of files for particular systems. - Lastly, yadm supplies the ability to manage a subset of secure files, + yadm is a tool for managing a collection of files across multiple com‐ + puters, using a shared Git repository. In addition, yadm provides a + feature to select alternate versions of files for particular systems. + Lastly, yadm supplies the ability to manage a subset of secure files, which are encrypted before they are included in the repository. ## COMMANDS git-command or git-alias - Any command not internally handled by yadm is passed through to - git(1). Git commands or aliases are invoked with the yadm man- + Any command not internally handled by yadm is passed through to + git(1). Git commands or aliases are invoked with the yadm man‐ aged repository. The working directory for Git commands will be the configured work-tree (usually $HOME). - Dotfiles are managed by using standard git commands; add, com- + Dotfiles are managed by using standard git commands; add, com‐ mit, push, pull, etc. The config command is not passed directly through. Instead use @@ -64,34 +62,34 @@ alt Create symbolic links and process templates for any managed files matching the naming rules described in the ALTERNATES and - TEMPLATES sections. It is usually unnecessary to run this com- + TEMPLATES sections. It is usually unnecessary to run this com‐ mand, as yadm automatically processes alternates by default. - This automatic behavior can be disabled by setting the configu- + This automatic behavior can be disabled by setting the configu‐ ration yadm.auto-alt to "false". bootstrap Execute $HOME/.config/yadm/bootstrap if it exists. clone url - Clone a remote repository for tracking dotfiles. After the con- - tents of the remote repository have been fetched, a "check out" - of the remote HEAD branch is attempted. If there are conflict- - ing files already present in the work-tree, the local version - will be left unmodified and you'll have to review and resolve + Clone a remote repository for tracking dotfiles. After the con‐ + tents of the remote repository have been fetched, a "check out" + of the remote HEAD branch is attempted. If there are conflict‐ + ing files already present in the work-tree, the local version + will be left unmodified and you'll have to review and resolve the difference. - The repository is stored in $HOME/.local/share/yadm/repo.git. + The repository is stored in $HOME/.local/share/yadm/repo.git. By default, $HOME will be used as the work-tree, but this can be overridden with the -w option. yadm can be forced to overwrite an existing repository by providing the -f option. If you want - to use a branch other than the remote HEAD branch you can spec- + to use a branch other than the remote HEAD branch you can spec‐ ify it using the -b option. By default yadm will ask the user - if the bootstrap program should be run (if it exists). The - options --bootstrap or --no-bootstrap will either force the - bootstrap to be run, or prevent it from being run, without - prompting the user. + if the bootstrap program should be run (if it exists). The op‐ + tions --bootstrap or --no-bootstrap will either force the boot‐ + strap to be run, or prevent it from being run, without prompting + the user. - config This command manages configurations for yadm. This command + config This command manages configurations for yadm. This command works exactly the way git-config(1) does. See the CONFIGURATION section for more details. @@ -102,8 +100,8 @@ without extracting them. encrypt - Encrypt all files matching the patterns found in $HOME/.con- - fig/yadm/encrypt. See the ENCRYPTION section for more details. + Encrypt all files matching the patterns found in $HOME/.con‐ + fig/yadm/encrypt. See the ENCRYPTION section for more details. enter Run a sub-shell with all Git variables set. Exit the sub-shell the same way you leave your normal shell (usually with the @@ -116,7 +114,7 @@ of invoking your shell, that command will be run with all of the Git variables exposed to the command's environment. - Emacs Tramp and Magit can manage files by using this configura- + Emacs Tramp and Magit can manage files by using this configura‐ tion: (add-to-list 'tramp-methods @@ -130,33 +128,33 @@ With this config, use (magit-status "/yadm::"). git-crypt options - If git-crypt is installed, this command allows you to pass - options directly to git-crypt, with the environment configured - to use the yadm repository. + If git-crypt is installed, this command allows you to pass op‐ + tions directly to git-crypt, with the environment configured to + use the yadm repository. git-crypt enables transparent encryption and decryption of files - in a git repository. You can read https://github.com/AGWA/git- + in a git repository. You can read https://github.com/AGWA/git- crypt for details. gitconfig - Pass options to the git config command. Since yadm already uses - the config command to manage its own configurations, this com- - mand is provided as a way to change configurations of the repos- + Pass options to the git config command. Since yadm already uses + the config command to manage its own configurations, this com‐ + mand is provided as a way to change configurations of the repos‐ itory managed by yadm. One useful case might be to configure the repository so untracked files are shown in status commands. yadm initially configures its repository so that untracked files - are not shown. If you wish use the default Git behavior (to - show untracked files and directories), you can remove this con- + are not shown. If you wish use the default Git behavior (to + show untracked files and directories), you can remove this con‐ figuration. yadm gitconfig --unset status.showUntrackedFiles help Print a summary of yadm commands. - init Initialize a new, empty repository for tracking dotfiles. The - repository is stored in $HOME/.local/share/yadm/repo.git. By - default, $HOME will be used as the work-tree, but this can be - overridden with the -w option. yadm can be forced to overwrite + init Initialize a new, empty repository for tracking dotfiles. The + repository is stored in $HOME/.local/share/yadm/repo.git. By + default, $HOME will be used as the work-tree, but this can be + overridden with the -w option. yadm can be forced to overwrite an existing repository by providing the -f option. list Print a list of files managed by yadm. The -a option will cause @@ -168,16 +166,16 @@ configs, repo, and switches. The purpose of introspection is to support command line completion. - perms Update permissions as described in the PERMISSIONS section. It - is usually unnecessary to run this command, as yadm automati- - cally processes permissions by default. This automatic behavior - can be disabled by setting the configuration yadm.auto-perms to + perms Update permissions as described in the PERMISSIONS section. It + is usually unnecessary to run this command, as yadm automati‐ + cally processes permissions by default. This automatic behavior + can be disabled by setting the configuration yadm.auto-perms to "false". transcrypt options - If transcrypt is installed, this command allows you to pass - options directly to transcrypt, with the environment configured - to use the yadm repository. + If transcrypt is installed, this command allows you to pass op‐ + tions directly to transcrypt, with the environment configured to + use the yadm repository. transcrypt enables transparent encryption and decryption of files in a git repository. You can read @@ -186,26 +184,26 @@ upgrade Version 3 of yadm uses a different directory for storing data. When you start to use version 3 for the first time, you may see - warnings about moving your data to this new directory. The eas- - iest way to accomplish this is by running "yadm upgrade". This - command will start by moving your yadm repo to the new path. - Next it will move any archive data. If the archive is tracked + warnings about moving your data to this new directory. The eas‐ + iest way to accomplish this is by running "yadm upgrade". This + command will start by moving your yadm repo to the new path. + Next it will move any archive data. If the archive is tracked within your yadm repo, this command will "stage" the renaming of that file in the repo's index. Upgrading will attempt to de-initialize and re-initialize your - submodules. If your submodules cannot be de-initialized, the - upgrade will fail. The most common reason submodules will fail - to de-initialize is because they have local modifications. If - you are willing to lose the local modifications to those submod- - ules, you can use the -f option with the "upgrade" command to - force the de-initialization. + submodules. If your submodules cannot be de-initialized, the up‐ + grade will fail. The most common reason submodules will fail to + de-initialize is because they have local modifications. If you + are willing to lose the local modifications to those submodules, + you can use the -f option with the "upgrade" command to force + the de-initialization. After running "yadm upgrade", you should run "yadm status" to review changes which have been staged, and commit them to your repository. - You can read https://yadm.io/docs/upgrade_from_2 for more infor- + You can read https://yadm.io/docs/upgrade_from_2 for more infor‐ mation. version @@ -213,20 +211,20 @@ ## OPTIONS - yadm supports a set of universal options that alter the paths it uses. - The default paths are documented in the FILES section. Any path speci- - fied by these options must be fully qualified. If you always want to - override one or more of these paths, it may be useful to create an - alias for the yadm command. For example, the following alias could be + yadm supports a set of universal options that alter the paths it uses. + The default paths are documented in the FILES section. Any path speci‐ + fied by these options must be fully qualified. If you always want to + override one or more of these paths, it may be useful to create an + alias for the yadm command. For example, the following alias could be used to override the repository directory. alias yadm='yadm --yadm-repo /alternate/path/to/repo' - The following is the full list of universal options. Each option + The following is the full list of universal options. Each option should be followed by a path. -Y,--yadm-dir - Override the yadm directory. yadm stores its configurations + Override the yadm directory. yadm stores its configurations relative to this directory. --yadm-data @@ -261,24 +259,24 @@ The following is the full list of supported configurations: yadm.alt-copy - If set to "true", alternate files will be copies instead of sym- + If set to "true", alternate files will be copies instead of sym‐ bolic links. This might be desirable, because some systems may not properly support symlinks. yadm.auto-alt - Disable the automatic linking described in the section ALTER- + Disable the automatic linking described in the section ALTER‐ NATES. If disabled, you may still run "yadm alt" manually to - create the alternate links. This feature is enabled by default. + create the alternate links. This feature is enabled by default. yadm.auto-exclude - Disable the automatic exclusion of patterns defined in + Disable the automatic exclusion of patterns defined in $HOME/.config/yadm/encrypt. This feature is enabled by default. yadm.auto-perms - Disable the automatic permission changes described in the sec- + Disable the automatic permission changes described in the sec‐ tion PERMISSIONS. If disabled, you may still run yadm perms - manually to update permissions. This feature is enabled by - default. + manually to update permissions. This feature is enabled by de‐ + fault. yadm.auto-private-dirs Disable the automatic creating of private directories described @@ -287,44 +285,44 @@ yadm.cipher Configure which encryption system is used by the encrypt/decrypt commands. Valid options are "gpg" and "openssl". The default is - "gpg". Detailed information can be found in the section ENCRYP- + "gpg". Detailed information can be found in the section ENCRYP‐ TION. yadm.git-program - Specify an alternate program to use instead of "git". By - default, the first "git" found in $PATH is used. + Specify an alternate program to use instead of "git". By de‐ + fault, the first "git" found in $PATH is used. yadm.gpg-perms - Disable the permission changes to $HOME/.gnupg/*. This feature + Disable the permission changes to $HOME/.gnupg/*. This feature is enabled by default. yadm.gpg-program - Specify an alternate program to use instead of "gpg". By - default, the first "gpg" found in $PATH is used. + Specify an alternate program to use instead of "gpg". By de‐ + fault, the first "gpg" found in $PATH is used. yadm.gpg-recipient Asymmetrically encrypt files with a gpg public/private key pair. Provide a "key ID" to specify which public key to encrypt with. The key must exist in your public keyrings. Multiple recipients - can be specified (separated by space). If left blank or not - provided, symmetric encryption is used instead. If set to - "ASK", gpg will interactively ask for recipients. See the - ENCRYPTION section for more details. This feature is disabled - by default. + can be specified (separated by space). If left blank or not + provided, symmetric encryption is used instead. If set to + "ASK", gpg will interactively ask for recipients. See the EN‐ + CRYPTION section for more details. This feature is disabled by + default. yadm.openssl-ciphername - Specify which cipher should be used by openssl. "aes-256-cbc" + Specify which cipher should be used by openssl. "aes-256-cbc" is used by default. yadm.openssl-old - Newer versions of openssl support the pbkdf2 key derivation - function. This is used by default. If this configuration is set - to "true", openssl operations will use options compatible with - older versions of openssl. If you change this option, you will + Newer versions of openssl support the pbkdf2 key derivation + function. This is used by default. If this configuration is set + to "true", openssl operations will use options compatible with + older versions of openssl. If you change this option, you will need to recreate your encrypted archive. yadm.openssl-program - Specify an alternate program to use instead of "openssl". By + Specify an alternate program to use instead of "openssl". By default, the first "openssl" found in $PATH is used. yadm.ssh-perms @@ -337,17 +335,17 @@ local.class Specify a class for the purpose of symlinking alternate files. - By default, no class will be matched. The local host can be - assigned multiple classes using command: + By default, no class will be matched. The local host can be as‐ + signed multiple classes using command: yadm config --add local.class local.arch - Override the architecture for the purpose of symlinking alter- + Override the architecture for the purpose of symlinking alter‐ nate files. local.hostname - Override the hostname for the purpose of symlinking alternate + Override the hostname for the purpose of symlinking alternate files. local.os @@ -362,24 +360,24 @@ to have an automated way of choosing an alternate version of a file for a different operating system, host, user, etc. - yadm will automatically create a symbolic link to the appropriate ver- + yadm will automatically create a symbolic link to the appropriate ver‐ sion of a file, when a valid suffix is appended to the filename. The suffix contains the conditions that must be met for that file to be used. - The suffix begins with "##", followed by any number of conditions sepa- + The suffix begins with "##", followed by any number of conditions sepa‐ rated by commas. ##[,,...] - Each condition is an attribute/value pair, separated by a period. Some - conditions do not require a "value", and in that case, the period and - value can be omitted. Most attributes can be abbreviated as a single + Each condition is an attribute/value pair, separated by a period. Some + conditions do not require a "value", and in that case, the period and + value can be omitted. Most attributes can be abbreviated as a single letter. [.] - These are the supported attributes, in the order of the weighted prece- + These are the supported attributes, in the order of the weighted prece‐ dence: @@ -392,29 +390,29 @@ calculated by running id -u -n. hostname, h - Valid if the value matches the short hostname. Hostname is cal- + Valid if the value matches the short hostname. Hostname is cal‐ culated by running uname -n, and trimming off any domain. class, c Valid if the value matches the local.class configuration. Class must be manually set using yadm config local.class . See - the CONFIGURATION section for more details about setting - local.class. + the CONFIGURATION section for more details about setting lo‐ + cal.class. distro, d - Valid if the value matches the distro. Distro is calculated by - running lsb_release -si or by inspecting the ID from /etc/os- - release. + Valid if the value matches the distro. Distro is calculated by + running lsb_release -si or by inspecting the ID from /etc/os-re‐ + lease. distro_family, f Valid if the value matches the distro family. Distro family is - calculated by inspecting the ID_LIKE line from /etc/os-release. + calculated by inspecting the ID_LIKE line from /etc/os-release. - os, o Valid if the value matches the OS. OS is calculated by running + os, o Valid if the value matches the OS. OS is calculated by running uname -s. arch, a - Valid if the value matches the architecture. Architecture is + Valid if the value matches the architecture. Architecture is calculated by running uname -m. default @@ -423,31 +421,30 @@ extension, e A special "condition" that doesn't affect the selection process. Its purpose is instead to allow the alternate file to end with a - certain extension to e.g. make editors highlight the content + certain extension to e.g. make editors highlight the content properly. - - NOTE: The OS for "Windows Subsystem for Linux" is reported as "WSL", + NOTE: The OS for "Windows Subsystem for Linux" is reported as "WSL", even though uname identifies as "Linux". - You may use any number of conditions, in any order. An alternate will - only be used if ALL conditions are valid. For all files managed by - yadm's repository or listed in $HOME/.config/yadm/encrypt, if they - match this naming convention, symbolic links will be created for the + You may use any number of conditions, in any order. An alternate will + only be used if ALL conditions are valid. For all files managed by + yadm's repository or listed in $HOME/.config/yadm/encrypt, if they + match this naming convention, symbolic links will be created for the most appropriate version. The "most appropriate" version is determined by calculating a score for each version of a file. A template is always scored higher than any symlink condition. The number of conditions is the next largest factor - in scoring. Files with more conditions will always be favored. Any - invalid condition will disqualify that file completely. + in scoring. Files with more conditions will always be favored. Any in‐ + valid condition will disqualify that file completely. If you don't care to have all versions of alternates stored in the same directory as the generated symlink, you can place them in the $HOME/.config/yadm/alt directory. The generated symlink or processed template will be created using the same relative path. - Alternate linking may best be demonstrated by example. Assume the fol- + Alternate linking may best be demonstrated by example. Assume the fol‐ lowing files are managed by yadm's repository: - $HOME/path/example.txt##default @@ -462,7 +459,7 @@ If running on a Macbook named "host2", yadm will create a symbolic link which looks like this: - $HOME/path/example.txt -> $HOME/path/example.txt##os.Darwin,host- + $HOME/path/example.txt -> $HOME/path/example.txt##os.Darwin,host‐ name.host2 However, on another Mackbook named "host3", yadm will create a symbolic @@ -491,21 +488,21 @@ Links are also created for directories named this way, as long as they have at least one yadm managed file within them (at the top level). - yadm will automatically create these links by default. This can be dis- - abled using the yadm.auto-alt configuration. Even if disabled, links + yadm will automatically create these links by default. This can be dis‐ + abled using the yadm.auto-alt configuration. Even if disabled, links can be manually created by running yadm alt. - Class is a special value which is stored locally on each host (inside - the local repository). To use alternate symlinks using class, you must - set the value of class using the configuration local.class. This is + Class is a special value which is stored locally on each host (inside + the local repository). To use alternate symlinks using class, you must + set the value of class using the configuration local.class. This is set like any other yadm configuration with the yadm config command. The following sets the class to be "Work". yadm config local.class Work Similarly, the values of architecture, os, hostname, and user can be - manually overridden using the configuration options local.arch, - local.os, local.hostname, and local.user. + manually overridden using the configuration options local.arch, lo‐ + cal.os, local.hostname, and local.user. ## TEMPLATES @@ -516,11 +513,11 @@ Supported template processors: default - This is yadm's built-in template processor. This processor is - very basic, with a Jinja-like syntax. The advantage of this pro- - cessor is that it only depends upon awk, which is available on - most *nix systems. To use this processor, specify the value of - "default" or just leave the value off (e.g. "##template"). + This is yadm's built-in template processor. This processor is + very basic, with a Jinja-like syntax. The advantage of this + processor is that it only depends upon awk, which is available + on most *nix systems. To use this processor, specify the value + of "default" or just leave the value off (e.g. "##template"). ESH ESH is a template processor written in POSIX compliant shell. It allows executing shell commands within templates. This can be @@ -537,14 +534,13 @@ envtpl To use the envtpl Jinja template processor, specify the value of "j2" or "envtpl". - - NOTE: Specifying "j2" as the processor will attempt to use j2cli or - envtpl, whichever is available. + NOTE: Specifying "j2" as the processor will attempt to use j2cli or en‐ + vtpl, whichever is available. If the template processor specified is available, templates will be processed to create or overwrite files. - During processing, the following variables are available in the tem- + During processing, the following variables are available in the tem‐ plate: Default Jinja or ESH Description @@ -612,11 +608,11 @@ It can be useful to manage confidential files, like SSH or GPG keys, across multiple systems. However, doing so would put plain text data into a Git repository, which often resides on a public system. yadm can - make it easy to encrypt and decrypt a set of files so the encrypted - version can be maintained in the Git repository. This feature will + make it easy to encrypt and decrypt a set of files so the encrypted + version can be maintained in the Git repository. This feature will only work if a supported tool is available. Both gpg(1) and openssl(1) are supported. gpg is used by default, but openssl can be configured - with the yadm.cypher configuration. + with the yadm.cipher configuration. To use this feature, a list of patterns must be created and saved as $HOME/.config/yadm/encrypt. This list of patterns should be relative @@ -626,7 +622,7 @@ .gnupg/*.gpg Standard filename expansions (*, ?, [) are supported. If you have Bash - version 4, you may use "**" to match all subdirectories. Other shell + version 4, you may use "**" to match all subdirectories. Other shell expansions like brace and tilde are not supported. Spaces in paths are supported, and should not be quoted. If a directory is specified, its contents will be included, but not recursively. Paths beginning with a @@ -635,29 +631,29 @@ The yadm encrypt command will find all files matching the patterns, and prompt for a password. Once a password has confirmed, the matching files will be encrypted and saved as $HOME/.local/share/yadm/archive. - The "encrypt" and "archive" files should be added to the yadm reposi- + The "encrypt" and "archive" files should be added to the yadm reposi‐ tory so they are available across multiple systems. To decrypt these files later, or on another system run yadm decrypt and - provide the correct password. After files are decrypted, permissions + provide the correct password. After files are decrypted, permissions are automatically updated as described in the PERMISSIONS section. - Symmetric encryption is used by default, but asymmetric encryption may + Symmetric encryption is used by default, but asymmetric encryption may be enabled using the yadm.gpg-recipient configuration. - NOTE: It is recommended that you use a private repository when keeping + NOTE: It is recommended that you use a private repository when keeping confidential files, even though they are encrypted. Patterns found in $HOME/.config/yadm/encrypt are automatically added to the repository's info/exclude file every time yadm encrypt is run. - This is to prevent accidentally committing sensitive data to the repos- + This is to prevent accidentally committing sensitive data to the repos‐ itory. This can be disabled using the yadm.auto-exclude configuration. Using transcrypt or git-crypt A completely separate option for encrypting data is to install and use transcrypt or git-crypt. Once installed, you can use these tools by - running yadm transcrypt or yadm git-crypt. These tools enables trans- + running yadm transcrypt or yadm git-crypt. These tools enables trans‐ parent encryption and decryption of files in a git repository. See the following web sites for more information: @@ -665,10 +661,8 @@ - https://github.com/AGWA/git-crypt - - ## PERMISSIONS - When files are checked out of a Git repository, their initial permis- + When files are checked out of a Git repository, their initial permis‐ sions are dependent upon the user's umask. Because of this, yadm will automatically update the permissions of some file paths. The "group" and "others" permissions will be removed from the following files: @@ -681,32 +675,32 @@ - The GPG directory and files, .gnupg/* - yadm will automatically update permissions by default. This can be dis- - abled using the yadm.auto-perms configuration. Even if disabled, per- - missions can be manually updated by running yadm perms. The .ssh - directory processing can be disabled using the yadm.ssh-perms configu- - ration. The .gnupg directory processing can be disabled using the + yadm will automatically update permissions by default. This can be dis‐ + abled using the yadm.auto-perms configuration. Even if disabled, per‐ + missions can be manually updated by running yadm perms. The .ssh di‐ + rectory processing can be disabled using the yadm.ssh-perms configura‐ + tion. The .gnupg directory processing can be disabled using the yadm.gpg-perms configuration. - When cloning a repo which includes data in a .ssh or .gnupg directory, - if those directories do not exist at the time of cloning, yadm will + When cloning a repo which includes data in a .ssh or .gnupg directory, + if those directories do not exist at the time of cloning, yadm will create the directories with mask 0700 prior to merging the fetched data into the work-tree. When running a Git command and .ssh or .gnupg directories do not exist, - yadm will create those directories with mask 0700 prior to running the - Git command. This can be disabled using the yadm.auto-private-dirs con- + yadm will create those directories with mask 0700 prior to running the + Git command. This can be disabled using the yadm.auto-private-dirs con‐ figuration. ## HOOKS - For every command yadm supports, a program can be provided to run - before or after that command. These are referred to as "hooks". yadm + For every command yadm supports, a program can be provided to run be‐ + fore or after that command. These are referred to as "hooks". yadm looks for hooks in the directory $HOME/.config/yadm/hooks. Each hook is named using a prefix of pre_ or post_, followed by the command which - should trigger the hook. For example, to create a hook which is run - after every yadm pull command, create a hook named post_pull. Hooks - must have the executable file permission set. + should trigger the hook. For example, to create a hook which is run af‐ + ter every yadm pull command, create a hook named post_pull. Hooks must + have the executable file permission set. If a pre_ hook is defined, and the hook terminates with a non-zero exit status, yadm will refuse to run the yadm command. For example, if a @@ -737,15 +731,15 @@ ## FILES All of yadm's configurations are relative to the "yadm directory". - yadm uses the "XDG Base Directory Specification" to determine this - directory. If the environment variable $XDG_CONFIG_HOME is defined as - a fully qualified path, this directory will be $XDG_CONFIG_HOME/yadm. + yadm uses the "XDG Base Directory Specification" to determine this di‐ + rectory. If the environment variable $XDG_CONFIG_HOME is defined as a + fully qualified path, this directory will be $XDG_CONFIG_HOME/yadm. Otherwise it will be $HOME/.config/yadm. Similarly, yadm's data files are relative to the "yadm data directory". - yadm uses the "XDG Base Directory Specification" to determine this - directory. If the environment variable $XDG_DATA_HOME is defined as a - fully qualified path, this directory will be $XDG_DATA_HOME/yadm. Oth- + yadm uses the "XDG Base Directory Specification" to determine this di‐ + rectory. If the environment variable $XDG_DATA_HOME is defined as a + fully qualified path, this directory will be $XDG_DATA_HOME/yadm. Oth‐ erwise it will be $HOME/.local/share/yadm. The following are the default paths yadm uses for its own data. Most @@ -753,7 +747,7 @@ section for details. $HOME/.config/yadm - The yadm directory. By default, all configs yadm stores is rela- + The yadm directory. By default, all configs yadm stores is rela‐ tive to this directory. $HOME/.local/share/yadm @@ -765,7 +759,7 @@ $YADM_DIR/alt This is a directory to keep "alternate files" without having - them side-by-side with the resulting symlink or processed tem- + them side-by-side with the resulting symlink or processed tem‐ plate. Alternate files placed in this directory will be created relative to $HOME instead. @@ -802,7 +796,7 @@ ## REPORTING BUGS Report issues or create pull requests at GitHub: - https://github.com/TheLocehiliosan/yadm/issues + https://github.com/yadm-dev/yadm/issues ## AUTHOR @@ -814,5 +808,3 @@ https://yadm.io/ - - diff --git a/yadm.spec b/yadm.spec index 90954db..f1d4a76 100644 --- a/yadm.spec +++ b/yadm.spec @@ -1,7 +1,7 @@ %{!?_pkgdocdir: %global _pkgdocdir %{_docdir}/%{name}-%{version}} Name: yadm Summary: Yet Another Dotfiles Manager -Version: 3.2.1 +Version: 3.3.0 Group: Development/Tools Release: 1%{?dist} URL: https://yadm.io