diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..eee7e01 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq + diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..4977e4b --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,379 @@ +# Introduction + +Thank you for considering contributing to **yadm**. I develop this project in my +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 +unacceptable behavior to . + +Contributions can take many forms, and often don’t require writing code—maybe +something could be documented more clearly, maybe a feature could be more +helpful, maybe installation could be easier. Help is welcome in any of these +areas. + +To contribute, you can: + +* Report [bugs](#reporting-a-bug) +* Request [features/enhancements](#suggesting-a-feature-or-enhancement) +* 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) + +# Reporting a bug + +Notice something amiss? You’re already helping by reporting the problem! Bugs +are tracked using GitHub issues. Here are some steps you can take to help +problems get fixed quickly and effectively: + +### Before submitting an issue + +Please take a quick look to see whether the problem has been reported already +(there’s a list of [open issues][open-issues]). You can try the search function +with some related terms for a cursory check. If you do find a previous report, +please add a comment there instead of opening a new issue. + +### Security issues + +If you have found a security vulnerability, do **NOT** open an issue. + +Any security issues should be emailed directly to . In order to +determine whether you are dealing with a security issue, ask yourself these two +questions: + +* Can I access something that's not mine, or something I shouldn't have access to? +* Can I disable something for other people? + +If the answer to either of those two questions is "yes", then you're probably +dealing with a security issue. + +### Submitting a (great) bug report + +Choose the "[Bug report][new-bug]" issue type. + +Pick a descriptive title that clearly identifies the issue. + +Describe the steps that led to the problem so that we can go through the same +sequence. A clear set of steps to reproduce the problem is key to fixing an +issue. If possible, attach a [`script.gz`](#attaching-a-scriptgz) to the bug +report. + +Describe what you had expected and how that differed from what happened, and +possibly, why. + +Include the version numbers of your operating system, of **yadm**, and of Git. + +### Attaching a script.gz + +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 +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 +$ cd yadm +$ make scripthost version=1.12.0 +Starting scripthost version="1.12.0" (recording script) +root@scripthost:~# ### run commands which +root@scripthost:~# ### demonstrate the problem +root@scripthost:~# ### a succinct set of commands is best +root@scripthost:~# exit +logout + +Script saved to script.gz +$ +``` + +A `script.gz` like this can be useful to developers to make a repeatable test +for the problem. You can attach the `script.gz` file to an issue. Look +[here][attach-help] for help with [attaching a file][attach-help]. + +# Suggesting a feature or enhancement + +Have an idea for an improvement? Creating a feature request is a good way to +communicate it. + +### Before submitting an issue + +Please take a quick look to see whether your idea has been suggested already +(there’s a list of [open issues][open-issues]). You can try the search function +with some related terms for a cursory check. If you do find a previous feature +request, please add a comment there instead of opening a new issue. + +### Submitting a (great) feature request + +Choose the "[Feature request][new-feature]" issue type. + +Summarize your idea with a clear title. + +Describe your suggestion in as much detail as possible. + +Explain alternatives you've considered. + +# Contributing code + +Wow, thank you for considering making a contribution of code! + +### Before you begin + +Please take a quick look to see whether a similar change is already being worked +on. A similar pull request may already exist. If the change is related to an +issue, look to see if that issue has an assignee. + +Consider reaching out before you start working. It's possible developers may +have some ideas and code lying around, and might be able to give you a head +start. + +[Creating a hook][hooks-help] is an easy way to begin adding features to an +already existing **yadm** operation. If the hook works well, it could be the +basis of a **yadm** feature addition. Or it might just be a [useful +hook][contrib-hooks] for someone else. + +### Design principles + +**yadm** was created with a few core design principles in mind. Please adhere to +these principles when making changes. + +* **Single repository** + * **yadm** is designed to maintain dotfiles in a single repository. + +* **Very few dependencies** + * **yadm** should be as portable as possible. This is one of the main + reasons it has only two dependencies (Bash and Git). Features using other + dependencies should gracefully downgrade instead of breaking. For example, + encryption requires GnuPG installed, and displays that information if it + is not. + +* **Sparse configuration** + * **yadm** should require very little configuration, and come with sensible + defaults. Changes requiring users to define meta-data for all of their + dotfiles will not be accepted. + +* **Maintain dotfiles in place** + * The default treatment for tracked data should be to allow it to remain a + file, in the location it is normally kept. + +* **Leverage Git** + * Stay out of the way and let Git do what it’s good at. Git has a deep and + rich set of features for just about every use case. Staying hands off for + almost all Git operations will make **yadm** more flexible and + future-proof. + +### Repository branches and tags + +* `master` + * This branch will always represent the latest release of **yadm**. +* `#.#.#` _(tags)_ + * Every release of **yadm** will have a commit tagged with the version number. +* `develop` + * This branch should be used for the basis of every change. As changes are + accepted, they will be merged into `develop`. +* `release/*` + * These are ephemeral branches used to prepare new releases. +* `hotfix/*` + * These are ephemeral branches used to prepare a patch release, which only + includes bug fixes. +* `gh-pages` + * This branch contains the yadm.io website source. +* `dev-pages` + * This branch should be used for the basis of every website change. As + changes are accepted, they will be merged into dev-pages. +* `netlify/*` + * These branches deploy configurations to Netlify websites. Currently this + is only used to drive redirections for + [bootstrap.yadm.io](https://bootstrap.yadm.io/). + +### GitHub workflow + +1. Fork the [yadm repository][yadm-repo] on GitHub. + +2. Clone your fork locally. + + ```text + $ git clone + ``` + +3. Add the official repository (`upstream`) as a remote repository. + + ```text + $ git remote add upstream https://github.com/TheLocehiliosan/yadm.git + ``` + +4. Verify you can run the test harness. _(This will require dependencies: + `make`, `docker`, and `docker-compose`)_. + + ```text + $ make test + ``` + +5. Create a feature branch, based off the `develop` branch. + + ```text + $ git checkout -b upstream/develop + ``` + +6. Add changes to your feature branch. + +7. If your changes take a few days, be sure to occasionally pull the latest + changes from upstream, to ensure that your local branch is up-to-date. + + ```text + $ git pull --rebase upstream develop + ``` + +8. When your work is done, push your local branch to your fork. + + ```text + $ git push origin + ``` + +9. [Create a pull request][pr-help] using `develop` as the "base". + +### Code conventions + +When updating the yadm code, please follow these guidelines: + +* Code linting + * Bash code should pass the scrutiny of [ShellCheck][shellcheck]. + * Python code must pass the scrutiny of [pylint][] and [flake8][]. + * Any YAML must pass the scrutiny of [yamllint][]. + * Running `make test_syntax.py` is an easy way to run all linters. +* Interface changes + * Any changes to **yadm**'s interface should include a commit that updates + the `yadm.1` man page. + +### Test conventions + +The test system is written in Python 3 using [pytest][]. Tests should be written +for all bugs fixed and features added. To make testing portable and uniform, +tests should be performed via the [yadm/testbed][] docker image. The `Makefile` +has several "make targets" for testing. Running `make` by itself will produce a +help page. + +Please follow these guidelines while writing tests: + +* Organization + * Tests should be kept in the `test/` directory. + * Every test module name should start with `test_`. + * Unit tests, which test individual functions should have names that begin + with `test_unit_`. + * Completely new features should get their own test modules, while updates + to existing features should have updated test modules. +* Efficiency + * Care should be taken to make tests run as efficiently as possible. + * Scope large, unchanging, fixtures appropriately so they do not have to be + recreated multiple times. + +### Commit conventions + +When arranging your commits, please adhere to the following conventions. + +* Commit messages + * Use the "[Tim Pope][tpope-style]" style of commit messages. Here is a + [great guide][commit-style] to writing commit messages. +* Atomic commits + * Please create only [atomic commits][atomic-commits]. +* Signed commits + * All commits must be [cryptographically signed][signing-commits]. + +# Improving documentation + +Wow, thank you for considering making documentation improvements! + +There is overlap between the content of the man page, and the information on the +website. Consider reviewing both sets of documentation, and submitting similar +changes for both to improve consistency. + +### Man page changes + +The man page documentation is contained in the file `yadm.1`. This file is +formatted using [groff man macros][groff-man]. Changes to this file can be +tested using "make targets": + +```text +$ make man +$ make man-wide +$ make man-ps +``` + +While the [markdown version of the man page][yadm-man] is generated from +`yadm.1`, please do not include changes to `yadm.md` within any pull request. +That file is only updated during software releases. + +### Website changes + +The yadm.io website is generated using [Jekyll][jekyll]. The bulk of the +documentation is created as an ordered collection within `_docs`. To make +website testing easy and portable, use the [yadm/jekyll][] docker image. The +`Makefile` has several "make targets" for testing. Running `make` by itself will +produce a help page. + +* `make test`: + Perform tests done by continuous integration. +* `make up`: + Start a container to locally test the website. The test website will be + hosted at http://localhost:4000/ +* `make clean`: + Remove previously built data any any Jekyll containers. + +When making website changes, be sure to adhere to [code](#code-conventions) and +[commit](#commit-conventions) conventions. Use the same [GitHub +workflow](#github-workflow) when creating a pull request. However use the +`dev-pages` branch as a base instead of `develop`. + +# Maintaining packages + +Maintaining installation packages is very important for making **yadm** +accessible to as many people as possible. Thank you for considering contributing +in this way. Please consider the following: + +* Watch releases + * GitHub allows users to "watch" a project for "releases". Doing so will + provide you with notifications when a new version of **yadm** has been + released. +* Include License + * Any package of **yadm** should include the license file from the + repository. +* Dependencies + * Be sure to include dependencies in a manner appropriate to the packaging + system being used. **yadm** won't work very well without Git. :) + +# Answering support questions + +Are you an experienced **yadm** user, with an advanced knowledge of Git? Your +expertise could be useful to someone else who is starting out or struggling with +a problem. Consider reviewing the list of [open support questions][questions] to +see if you can help. + +[atomic-commits]: https://www.google.com/search?q=atomic+commits +[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 +[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 +[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 +[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 +[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/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 new file mode 100644 index 0000000..29dc730 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,7 @@ + diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md new file mode 100644 index 0000000..705dc5a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -0,0 +1,68 @@ +--- +name: Bug report +about: Create a report to help improve yadm +title: '' +labels: bug +assignees: '' + +--- + + +### Describe the bug + +[A clear and concise description of what the bug is.] + +### To reproduce + +Can this be reproduced with the yadm/testbed docker image: [Yes/No] + + +Steps to reproduce the behavior: + +1. Run command '....' +2. Run command '....' +3. Run command '....' +4. See error + +### Expected behavior + +[A clear and concise description of what you expected to happen.] + +### Environment + + - Operating system: [Ubuntu 18.04, yadm/testbed, etc.] + - Version yadm: [found via `yadm version`] + - Version Git: [found via `git --version`] + +### Additional context + +[Add any other context about the problem here.] diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md new file mode 100644 index 0000000..3a211ea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -0,0 +1,29 @@ +--- +name: Feature request +about: Suggest an idea for yadm +title: '' +labels: feature +assignees: '' + +--- + + +### Is your feature request related to a problem? Please describe. + +[A clear and concise description of what the problem is. Ex. I'm always frustrated when ...] + +### Describe the solution you'd like + +[A clear and concise description of what you want to happen.] + +### Describe alternatives you've considered + +[A clear and concise description of any alternative solutions or features you've +considered. For example, have you considered using yadm "hooks" as a solution?] + +### Additional context + +[Add any other context or screenshots about the feature request here.] diff --git a/.github/ISSUE_TEMPLATE/OTHER.md b/.github/ISSUE_TEMPLATE/OTHER.md new file mode 100644 index 0000000..936a4a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/OTHER.md @@ -0,0 +1,23 @@ +--- +name: Other issue +about: Report issues with documentation, packaging, or something else +title: '' +labels: '' +assignees: '' + +--- + + +### This issue is about + +* [ ] Man pages or command-line usage +* [ ] Website documentation +* [ ] Packaging +* [ ] Other + +### Describe the issue + +[A clear and concise description of the issue.] diff --git a/.github/ISSUE_TEMPLATE/SUPPORT.md b/.github/ISSUE_TEMPLATE/SUPPORT.md new file mode 100644 index 0000000..22bd849 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/SUPPORT.md @@ -0,0 +1,36 @@ +--- +name: Support +about: Get help using yadm +title: '' +labels: 'question' +assignees: '' + +--- + + +### This question is about + +* [ ] Installation +* [ ] Initializing / Cloning +* [ ] Alternate files +* [ ] Jinja templates +* [ ] Encryption +* [ ] Bootstrap +* [ ] Hooks +* [ ] Other + +### Describe your question + + +[A clear and concise description of the question.] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d2f12b9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,34 @@ +### What does this PR do? + +[A clear and concise description of what this pull request accomplishes.] + +### What issues does this PR fix or reference? + + +[A list of related issues / pull requests.] + +### Previous Behavior + +[Describe the existing behavior.] + +### New Behavior + +[Describe the behavior, after this PR is applied.] + +### Have [tests][1] been written for this change? + +[Yes / No] + +### Have these commits been [signed with GnuPG][2]? + +[Yes / No] + +--- + +Please review [yadm's Contributing Guide][3] for best practices. + +[1]: https://github.com/TheLocehiliosan/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 diff --git a/.gitignore b/.gitignore index 7aa998b..aa13f8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .DS_Store .jekyll-metadata +.pytest_cache .sass-cache _site +testenv diff --git a/.travis.yml b/.travis.yml index b38f19f..4b4efd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ --- -sudo: required -language: bash +language: minimal services: - docker before_install: - docker pull yadm/testbed:latest script: - - docker run --rm -v "$PWD:/yadm:ro" yadm/testbed + - docker run -t --rm -v "$PWD:/yadm:ro" yadm/testbed diff --git a/Dockerfile b/Dockerfile index 74700c4..985fc7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,45 @@ -FROM ubuntu:yakkety +FROM ubuntu:18.04 MAINTAINER Tim Byrne +# No input during build +ENV DEBIAN_FRONTEND noninteractive + +# UTF8 locale +RUN apt-get update && apt-get install -y locales +RUN locale-gen en_US.UTF-8 +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' + +# Convenience settings for the testbed's root account +RUN echo 'set -o vi' >> /root/.bashrc + # Install prerequisites -RUN apt-get update && apt-get install -y git gnupg1 make shellcheck bats expect curl python-pip lsb-release -RUN pip install envtpl +RUN \ + apt-get update && \ + apt-get install -y \ + curl \ + expect \ + git \ + gnupg1 \ + lsb-release \ + make \ + python3-pip \ + shellcheck=0.4.6-1 \ + vim \ + ; +RUN pip3 install \ + envtpl \ + flake8==3.5.0 \ + pylint==1.9.2 \ + pytest==3.6.4 \ + yamllint==1.15.0 \ + ; # Force GNUPG version 1 at path /usr/bin/gpg RUN ln -fs /usr/bin/gpg1 /usr/bin/gpg +# Create a flag to identify when running inside the yadm testbed +RUN touch /.yadmtestbed + # /yadm will be the work directory for all tests # docker commands should mount the local yadm project as /yadm WORKDIR /yadm diff --git a/LICENSE b/LICENSE index a491495..f288702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,14 +1,674 @@ -yadm - Yet Another Dotfiles Manager -Copyright (C) 2015-2017 Tim Byrne + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 -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 -the Free Software Foundation, version 3 of the License. + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + Preamble -You should have received a copy of the GNU General Public License -along with this program. If not, see . + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile index f544a1d..1242bfd 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,162 @@ +PYTESTS = $(wildcard test/test_*.py) + .PHONY: all -all: yadm.md contrib +all: + @$(MAKE) usage | less + +# Display usage for all make targets +.PHONY: usage +usage: + @echo + @echo 'make TARGET [option=value, ...]' + @echo + @echo 'TESTING' + @echo + @echo ' make test [testargs=ARGS]' + @echo ' - Run all tests. "testargs" can specify a single string of arguments' + @echo ' for py.test.' + @echo + @echo ' make .py [testargs=ARGS]' + @echo ' - Run tests from a specific test file. "testargs" can specify a' + @echo ' single string of arguments for py.test.' + @echo + @echo ' make testhost [version=VERSION]' + @echo ' - Create an ephemeral container for doing adhoc yadm testing. The' + @echo ' HEAD revision of yadm will be used unless "version" is' + @echo ' specified. "version" can be set to any commit, branch, tag, etc.' + @echo ' The targeted "version" will be retrieved from the repo, and' + @echo ' linked into the container as a local volume.' + @echo + @echo ' make scripthost [version=VERSION]' + @echo ' - Create an ephemeral container for demonstrating a bug. After' + @echo ' exiting the shell, a log of the commands used to illustrate the' + @echo ' problem will be written to the file "script.txt". This file can' + @echo ' be useful to developers to make a repeatable test for the' + @echo ' problem.' + @echo + @echo 'LINTING' + @echo + @echo ' make testenv' + @echo ' - Create a python virtual environment with the same dependencies' + @echo " used by yadm's testbed environment. Creating and activating" + @echo ' this environment might be useful if your editor does real time' + @echo ' linting of python files. After creating the virtual environment,' + @echo ' you can activate it by typing:' + @echo + @echo ' source testenv/bin/activate' + @echo + @echo 'MANPAGES' + @echo + @echo ' make man' + @echo ' - View yadm.1 as a standard man page.' + @echo + @echo ' make man-wide' + @echo ' - View yadm.1 as a man page, using all columns of your display.' + @echo + @echo ' make man-ps' + @echo ' - Create a postscript version of the man page.' + @echo + @echo 'FILE GENERATION' + @echo + @echo ' make yadm.md' + @echo ' - Generate the markdown version of the man page (for viewing on' + @echo ' the web).' + @echo + @echo ' make contrib' + @echo ' - Generate the CONTRIBUTORS file, from the repo history.' + @echo + @echo 'UTILITIES' + @echo + @echo ' make sync-clock' + @echo ' - Reset the hardware clock for the docker hypervisor host. This' + @echo ' can be useful for docker engine hosts which are not' + @echo ' Linux-based.' + @echo + +# Make it possible to run make specifying a py.test test file +.PHONY: $(PYTESTS) +$(PYTESTS): + @$(MAKE) test testargs="-k $@ $(testargs)" +%.py: + @$(MAKE) test testargs="-k $@ $(testargs)" + +# Run all tests with additional testargs +.PHONY: test +test: + @if [ -f /.yadmtestbed ]; then \ + cd /yadm && \ + py.test -v $(testargs); \ + else \ + if command -v "docker-compose" >/dev/null 2>&1; then \ + docker-compose run --rm testbed make test testargs="$(testargs)"; \ + else \ + echo "Sorry, this make test requires docker-compose to be installed."; \ + false; \ + fi \ + fi + +.PHONY: testhost +testhost: require-docker + @version=HEAD + @rm -rf /tmp/testhost + @git show $(version):yadm > /tmp/testhost + @chmod a+x /tmp/testhost + @echo Starting testhost version=\"$$version\" + @docker run \ + -w /root \ + --hostname testhost \ + --rm -it \ + -v "/tmp/testhost:/bin/yadm:ro" \ + yadm/testbed:latest \ + bash -l + +.PHONY: scripthost +scripthost: require-docker + @version=HEAD + @rm -rf /tmp/testhost + @git show $(version):yadm > /tmp/testhost + @chmod a+x /tmp/testhost + @echo Starting scripthost version=\"$$version\" \(recording script\) + @printf '' > script.gz + @docker run \ + -w /root \ + --hostname scripthost \ + --rm -it \ + -v "$$PWD/script.gz:/script.gz:rw" \ + -v "/tmp/testhost:/bin/yadm:ro" \ + yadm/testbed:latest \ + bash -c "script /tmp/script -q -c 'bash -l'; gzip < /tmp/script > /script.gz" + @echo + @echo "Script saved to $$PWD/script.gz" + + +.PHONY: testenv +testenv: + @echo 'Creating a local virtual environment in "testenv/"' + @echo + virtualenv --python=python3 testenv + testenv/bin/pip3 install --upgrade pip setuptools + testenv/bin/pip3 install --upgrade \ + flake8==3.5.0 \ + pylint==1.9.2 \ + pytest \ + yamllint==1.15.0 \ + ; + @echo + @echo 'To activate this test environment type:' + @echo ' source testenv/bin/activate' + +.PHONY: man +man: + @groff -man -Tascii ./yadm.1 | less + +.PHONY: man-wide +man-wide: + @man ./yadm.1 + +.PHONY: man-ps +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 @@ -9,52 +166,13 @@ contrib: @echo "CONTRIBUTORS\n" > CONTRIBUTORS @git shortlog -ns master gh-pages dev dev-pages | cut -f2 >> CONTRIBUTORS -.PHONY: pdf -pdf: - @groff -man -Tps ./yadm.1 > yadm.ps - @open yadm.ps - @sleep 1 - @rm yadm.ps +.PHONY: sync-clock +sync-clock: + docker run --rm --privileged alpine hwclock -s -.PHONY: test -test: bats shellcheck - -.PHONY: parallel -parallel: - ls test/*bats | time parallel -q -P0 -- docker run --rm -v "$$PWD:/yadm:ro" yadm/testbed bash -c 'bats {}' - -.PHONY: bats -bats: - @echo Running all bats tests - @GPG_AGENT_INFO= bats test - -.PHONY: shellcheck -shellcheck: - @echo Running shellcheck - @shellcheck --version || true - @shellcheck -s bash yadm bootstrap test/*.bash completion/yadm.bash_completion - @cd test; \ - for bats_file in *bats; do \ - sed 's/^@test.*{/function test() {/' "$$bats_file" > "/tmp/$$bats_file.bash"; \ - shellcheck -s bash "/tmp/$$bats_file.bash"; \ - test_result="$$?"; \ - rm -f "/tmp/$$bats_file.bash"; \ - [ "$$test_result" -ne 0 ] && exit 1; \ - done; true - -.PHONY: testhost -testhost: - @target=HEAD - @rm -rf /tmp/testhost - @git show $(target):yadm > /tmp/testhost - @chmod a+x /tmp/testhost - @echo Starting testhost target=\"$$target\" - @docker run -w /root --hostname testhost --rm -it -v "/tmp/testhost:/bin/yadm:ro" yadm/testbed:latest bash - -.PHONY: man -man: - groff -man -Tascii ./yadm.1 | less - -.PHONY: wide -wide: - man ./yadm.1 +.PHONY: require-docker +require-docker: + @if ! command -v "docker" >/dev/null 2>&1; then \ + echo "Sorry, this make target requires docker to be installed."; \ + false; \ + fi diff --git a/README.md b/README.md index c50b321..a0d681a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,52 @@ -# yadm - Yet Another Dotfiles Manager [![Build Status](https://travis-ci.org/TheLocehiliosan/yadm.svg?branch=master)](https://travis-ci.org/TheLocehiliosan/yadm) +# yadm - Yet Another Dotfiles Manager -Features, usage, examples and installation instructions can be found on the [website](https://thelocehiliosan.github.io/yadm/). +[![Latest Version][releases-badge]][releases-link] +[![Homebrew Version][homebrew-badge]][homebrew-link] +[![Copr Version][copr-badge]][copr-link] +[![Arch Version][aur-badge]][aur-link] +[![License][license-badge]][license-link]
+[![Master Update][master-date]][master-commits] +[![Develop Update][develop-date]][develop-commits] +[![Website Update][website-date]][website-commits]
+[![Master Status][master-badge]][travis-ci] +[![Develop Status][develop-badge]][travis-ci] +[![GH Pages Status][gh-pages-badge]][travis-ci] +[![Dev Pages Status][dev-pages-badge]][travis-ci] -[https://thelocehiliosan.github.io/yadm/](https://thelocehiliosan.github.io/yadm/) +[https://yadm.io/][website-link] - +[**yadm**][website-link] is a tool for managing [dotfiles][]. + +* Based on [Git][], with full range of Git's features +* Supports system-specific alternative files +* Encryption of private data using [GnuPG][] +* Customizable initialization (bootstrapping) + +Features, usage, examples and installation instructions can be found on the +[website][website-link]. + +[Git]: https://git-scm.com/ +[GnuPG]: https://gnupg.org/ +[aur-badge]: https://img.shields.io/aur/version/yadm-git.svg +[aur-link]: https://aur.archlinux.org/packages/yadm-git +[copr-badge]: https://img.shields.io/badge/dynamic/json.svg?label=copr&prefix=v&query=%24..version&url=https%3A%2F%2Fcopr.fedorainfracloud.org%2Fapi_2%2Fbuilds%3Fproject_id%3D7041%26limit%3D1 +[copr-link]: https://copr.fedorainfracloud.org/coprs/thelocehiliosan/yadm/ +[dev-pages-badge]: https://img.shields.io/travis/TheLocehiliosan/yadm/dev-pages.svg?label=dev-pages +[develop-badge]: https://img.shields.io/travis/TheLocehiliosan/yadm/develop.svg?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 +[dotfiles]: https://en.wikipedia.org/wiki/Hidden_file_and_hidden_directory +[gh-pages-badge]: https://img.shields.io/travis/TheLocehiliosan/yadm/gh-pages.svg?label=gh-pages +[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/travis/TheLocehiliosan/yadm/master.svg?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 +[releases-badge]: https://img.shields.io/github/tag/TheLocehiliosan/yadm.svg?label=latest+release +[releases-link]: https://github.com/TheLocehiliosan/yadm/releases +[travis-ci]: https://travis-ci.org/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-link]: https://yadm.io/ diff --git a/contrib/hooks/README.md b/contrib/hooks/README.md new file mode 100644 index 0000000..551f6f0 --- /dev/null +++ b/contrib/hooks/README.md @@ -0,0 +1,14 @@ +## Contributed Hooks + +Although these [hooks][hooks-help] are available as part of the official +**yadm** source tree, they have a somewhat different status. The intention is to +keep interesting and potentially useful hooks here, building a library of +examples that might help others. + +In some cases, an experimental new feature can be build entirely with hooks, and +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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4fe0b86 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +--- +version: '3' +services: + testbed: + volumes: + - .:/yadm:ro + image: yadm/testbed:latest diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..d686a0a --- /dev/null +++ b/pylintrc @@ -0,0 +1,14 @@ +[BASIC] +good-names=pytestmark + +[DESIGN] +max-args=14 +max-locals=27 +max-attributes=8 +max-statements=65 + +[MESSAGES CONTROL] +disable=redefined-outer-name + +[TYPECHECK] +ignored-modules=py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..c7fc1db --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +cache_dir = /tmp +addopts = -ra diff --git a/test/000_unit_syntax.bats b/test/000_unit_syntax.bats deleted file mode 100644 index 2bb1ee3..0000000 --- a/test/000_unit_syntax.bats +++ /dev/null @@ -1,11 +0,0 @@ -load common -load_fixtures - -@test "Syntax check" { - echo " - $T_YADM must parse correctly - " - - #; check the syntax of yadm - bash -n "$T_YADM" -} diff --git a/test/001_unit_paths.bats b/test/001_unit_paths.bats deleted file mode 100644 index bf479e8..0000000 --- a/test/001_unit_paths.bats +++ /dev/null @@ -1,209 +0,0 @@ -load common -load_fixtures - -function configuration_test() { - # shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - status=0 - output=$( process_global_args "$@" ) || { - status=$? - true - } - if [ "$status" == 0 ]; then - process_global_args "$@" - configure_paths - fi - - echo -e "STATUS:$status\nOUTPUT:$output" - echo "CONFIGURED PATHS:" - echo " YADM_DIR:$YADM_DIR" - echo " YADM_REPO:$YADM_REPO" - echo " YADM_CONFIG:$YADM_CONFIG" - echo " YADM_ENCRYPT:$YADM_ENCRYPT" - echo " YADM_ARCHIVE:$YADM_ARCHIVE" - echo "YADM_BOOTSTRAP:$YADM_BOOTSTRAP" - echo " GIT_DIR:$GIT_DIR" -} - -@test "Default paths" { - echo " - Default paths should be defined - YADM_DIR=$DEFAULT_YADM_DIR - YADM_REPO=$DEFAULT_YADM_DIR/$DEFAULT_REPO - YADM_CONFIG=$DEFAULT_YADM_DIR/$DEFAULT_CONFIG - YADM_ENCRYPT=$DEFAULT_YADM_DIR/$DEFAULT_ENCRYPT - YADM_ARCHIVE=$DEFAULT_YADM_DIR/$DEFAULT_ARCHIVE - YADM_BOOTSTRAP=$DEFAULT_YADM_DIR/$DEFAULT_BOOTSTRAP - GIT_DIR=$DEFAULT_YADM_DIR/$DEFAULT_REPO - " - - configuration_test - - [ "$status" == 0 ] - [ "$YADM_DIR" = "$HOME/.yadm" ] - [ "$YADM_REPO" = "$DEFAULT_YADM_DIR/$DEFAULT_REPO" ] - [ "$YADM_CONFIG" = "$DEFAULT_YADM_DIR/$DEFAULT_CONFIG" ] - [ "$YADM_ENCRYPT" = "$DEFAULT_YADM_DIR/$DEFAULT_ENCRYPT" ] - [ "$YADM_ARCHIVE" = "$DEFAULT_YADM_DIR/$DEFAULT_ARCHIVE" ] - [ "$YADM_BOOTSTRAP" = "$DEFAULT_YADM_DIR/$DEFAULT_BOOTSTRAP" ] - [ "$GIT_DIR" = "$DEFAULT_YADM_DIR/$DEFAULT_REPO" ] -} - -@test "Override YADM_DIR" { - echo " - Override YADM_DIR using -Y $T_DIR_YADM - YADM_DIR should become $T_DIR_YADM - " - - TEST_ARGS=(-Y $T_DIR_YADM) - configuration_test "${TEST_ARGS[@]}" - - [ "$status" == 0 ] - [ "$YADM_DIR" = "$T_DIR_YADM" ] - [ "$YADM_REPO" = "$T_DIR_YADM/$DEFAULT_REPO" ] - [ "$YADM_CONFIG" = "$T_DIR_YADM/$DEFAULT_CONFIG" ] - [ "$YADM_ENCRYPT" = "$T_DIR_YADM/$DEFAULT_ENCRYPT" ] - [ "$YADM_ARCHIVE" = "$T_DIR_YADM/$DEFAULT_ARCHIVE" ] - [ "$YADM_BOOTSTRAP" = "$T_DIR_YADM/$DEFAULT_BOOTSTRAP" ] - [ "$GIT_DIR" = "$T_DIR_YADM/$DEFAULT_REPO" ] -} - -@test "Override YADM_DIR (not fully-qualified)" { - echo " - Override YADM_DIR using -Y 'relative/path' - yadm should fail, and report the error - " - - TEST_ARGS=(-Y relative/path) - configuration_test "${TEST_ARGS[@]}" - - [ "$status" == 1 ] - [[ "$output" =~ must\ specify\ a\ fully\ qualified ]] -} - -@test "Override YADM_REPO" { - echo " - Override YADM_REPO using --yadm-repo /custom/repo - YADM_REPO should become /custom/repo - GIT_DIR should become /custom/repo - " - - TEST_ARGS=(--yadm-repo /custom/repo) - configuration_test "${TEST_ARGS[@]}" - - [ "$YADM_REPO" = "/custom/repo" ] - [ "$GIT_DIR" = "/custom/repo" ] -} - -@test "Override YADM_REPO (not fully qualified)" { - echo " - Override YADM_REPO using --yadm-repo relative/repo - yadm should fail, and report the error - " - - TEST_ARGS=(--yadm-repo relative/repo) - configuration_test "${TEST_ARGS[@]}" - - [ "$status" == 1 ] - [[ "$output" =~ must\ specify\ a\ fully\ qualified ]] -} - -@test "Override YADM_CONFIG" { - echo " - Override YADM_CONFIG using --yadm-config /custom/config - YADM_CONFIG should become /custom/config - " - - TEST_ARGS=(--yadm-config /custom/config) - configuration_test "${TEST_ARGS[@]}" - - [ "$YADM_CONFIG" = "/custom/config" ] -} - -@test "Override YADM_CONFIG (not fully qualified)" { - echo " - Override YADM_CONFIG using --yadm-config relative/config - yadm should fail, and report the error - " - - TEST_ARGS=(--yadm-config relative/config) - configuration_test "${TEST_ARGS[@]}" - - [ "$status" == 1 ] - [[ "$output" =~ must\ specify\ a\ fully\ qualified ]] -} - -@test "Override YADM_ENCRYPT" { - echo " - Override YADM_ENCRYPT using --yadm-encrypt /custom/encrypt - YADM_ENCRYPT should become /custom/encrypt - " - - TEST_ARGS=(--yadm-encrypt /custom/encrypt) - configuration_test "${TEST_ARGS[@]}" - - [ "$YADM_ENCRYPT" = "/custom/encrypt" ] -} - -@test "Override YADM_ENCRYPT (not fully qualified)" { - echo " - Override YADM_ENCRYPT using --yadm-encrypt relative/encrypt - yadm should fail, and report the error - " - - TEST_ARGS=(--yadm-encrypt relative/encrypt) - configuration_test "${TEST_ARGS[@]}" - - [ "$status" == 1 ] - [[ "$output" =~ must\ specify\ a\ fully\ qualified ]] -} - -@test "Override YADM_ARCHIVE" { - echo " - Override YADM_ARCHIVE using --yadm-archive /custom/archive - YADM_ARCHIVE should become /custom/archive - " - - TEST_ARGS=(--yadm-archive /custom/archive) - configuration_test "${TEST_ARGS[@]}" - - [ "$YADM_ARCHIVE" = "/custom/archive" ] -} - -@test "Override YADM_ARCHIVE (not fully qualified)" { - echo " - Override YADM_ARCHIVE using --yadm-archive relative/archive - yadm should fail, and report the error - " - - TEST_ARGS=(--yadm-archive relative/archive) - configuration_test "${TEST_ARGS[@]}" - - [ "$status" == 1 ] - [[ "$output" =~ must\ specify\ a\ fully\ qualified ]] -} - -@test "Override YADM_BOOTSTRAP" { - echo " - Override YADM_BOOTSTRAP using --yadm-bootstrap /custom/bootstrap - YADM_BOOTSTRAP should become /custom/bootstrap - " - - TEST_ARGS=(--yadm-bootstrap /custom/bootstrap) - configuration_test "${TEST_ARGS[@]}" - - [ "$YADM_BOOTSTRAP" = "/custom/bootstrap" ] -} - -@test "Override YADM_BOOTSTRAP (not fully qualified)" { - echo " - Override YADM_BOOTSTRAP using --yadm-bootstrap relative/bootstrap - yadm should fail, and report the error - " - - TEST_ARGS=(--yadm-bootstrap relative/bootstrap) - configuration_test "${TEST_ARGS[@]}" - - [ "$status" == 1 ] - [[ "$output" =~ must\ specify\ a\ fully\ qualified ]] -} diff --git a/test/002_unit_gpg_program.bats b/test/002_unit_gpg_program.bats deleted file mode 100644 index 3dc29be..0000000 --- a/test/002_unit_gpg_program.bats +++ /dev/null @@ -1,67 +0,0 @@ -load common -T_YADM_CONFIG=; # populated by load_fixtures -load_fixtures -status=;output=; # populated by bats run() - -setup() { - destroy_tmp - make_parents "$T_YADM_CONFIG" -} - -teardown() { - destroy_tmp -} - -function configuration_test() { - # shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - # shellcheck disable=SC2034 - YADM_CONFIG="$T_YADM_CONFIG" - status=0 - { output=$( require_gpg ) && require_gpg; } || { - status=$? - true - } - - echo -e "STATUS:$status\nGPG_PROGRAM:$GPG_PROGRAM\nOUTPUT:$output" - -} - -@test "Default gpg program" { - echo " - Default gpg program should be 'gpg' - " - - configuration_test - - [ "$status" == 0 ] - [ "$GPG_PROGRAM" = "gpg" ] -} - -@test "Override gpg program (valid program)" { - echo " - Override gpg using yadm.gpg-program - Program should be 'cat' - " - - git config --file="$T_YADM_CONFIG" "yadm.gpg-program" "cat" - - configuration_test - - [ "$status" == 0 ] - [ "$GPG_PROGRAM" = "cat" ] -} - -@test "Override gpg program (invalid program)" { - echo " - Override gpg using yadm.gpg-program - Program should be 'badprogram' - " - - git config --file="$T_YADM_CONFIG" "yadm.gpg-program" "badprogram" - - configuration_test - - [ "$status" == 1 ] - [[ "$output" =~ badprogram ]] -} diff --git a/test/003_unit_git_program.bats b/test/003_unit_git_program.bats deleted file mode 100644 index 2fe31c1..0000000 --- a/test/003_unit_git_program.bats +++ /dev/null @@ -1,67 +0,0 @@ -load common -T_YADM_CONFIG=; # populated by load_fixtures -load_fixtures -status=;output=; # populated by bats run() - -setup() { - destroy_tmp - make_parents "$T_YADM_CONFIG" -} - -teardown() { - destroy_tmp -} - -function configuration_test() { - # shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - # shellcheck disable=SC2034 - YADM_CONFIG="$T_YADM_CONFIG" - status=0 - { output=$( require_git ) && require_git; } || { - status=$? - true - } - - echo -e "STATUS:$status\nGIT_PROGRAM:$GIT_PROGRAM\nOUTPUT:$output" - -} - -@test "Default git program" { - echo " - Default git program should be 'git' - " - - configuration_test - - [ "$status" == 0 ] - [ "$GIT_PROGRAM" = "git" ] -} - -@test "Override git program (valid program)" { - echo " - Override git using yadm.git-program - Program should be 'cat' - " - - git config --file="$T_YADM_CONFIG" "yadm.git-program" "cat" - - configuration_test - - [ "$status" == 0 ] - [ "$GIT_PROGRAM" = "cat" ] -} - -@test "Override git program (invalid program)" { - echo " - Override git using yadm.git-program - Program should be 'badprogram' - " - - git config --file="$T_YADM_CONFIG" "yadm.git-program" "badprogram" - - configuration_test - - [ "$status" == 1 ] - [[ "$output" =~ badprogram ]] -} diff --git a/test/004_unit_bootstrap_available.bats b/test/004_unit_bootstrap_available.bats deleted file mode 100644 index fc4cc0d..0000000 --- a/test/004_unit_bootstrap_available.bats +++ /dev/null @@ -1,66 +0,0 @@ -load common -T_YADM_BOOTSTRAP=; # populated by load_fixtures -load_fixtures -status=; # populated by bats run() - -setup() { - destroy_tmp - make_parents "$T_YADM_BOOTSTRAP" -} - -teardown() { - destroy_tmp -} - -function available_test() { - # shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - # shellcheck disable=SC2034 - YADM_BOOTSTRAP="$T_YADM_BOOTSTRAP" - status=0 - { bootstrap_available; } || { - status=$? - true - } - - echo -e "STATUS:$status" - -} - -@test "Bootstrap missing" { - echo " - When bootstrap command is missing - return 1 - " - - available_test - [ "$status" == 1 ] - -} - -@test "Bootstrap not executable" { - echo " - When bootstrap command is not executable - return 1 - " - - touch "$T_YADM_BOOTSTRAP" - - available_test - [ "$status" == 1 ] - -} - -@test "Bootstrap executable" { - echo " - When bootstrap command is not executable - return 0 - " - - touch "$T_YADM_BOOTSTRAP" - chmod a+x "$T_YADM_BOOTSTRAP" - - available_test - [ "$status" == 0 ] - -} diff --git a/test/005_unit_set_operating_system.bats b/test/005_unit_set_operating_system.bats deleted file mode 100644 index 6bbe2c6..0000000 --- a/test/005_unit_set_operating_system.bats +++ /dev/null @@ -1,76 +0,0 @@ -load common -load_fixtures - -@test "Default OS" { - echo " - By default, the value of OPERATING_SYSTEM should be reported by uname -s - " - - # shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - status=0 - output=$( set_operating_system; echo "$OPERATING_SYSTEM" ) || { - status=$? - true - } - - expected=$(uname -s 2>/dev/null) - - echo "output=$output" - echo "expect=$expected" - - [ "$status" == 0 ] - [ "$output" = "$expected" ] -} - -@test "Detect no WSL" { - echo " - When /proc/version does not contain Microsoft, report uname -s - " - - echo "proc version exists" > "$BATS_TMPDIR/proc_version" - - # shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - # shellcheck disable=SC2034 - PROC_VERSION="$BATS_TMPDIR/proc_version" - status=0 - output=$( set_operating_system; echo "$OPERATING_SYSTEM" ) || { - status=$? - true - } - - expected=$(uname -s 2>/dev/null) - - echo "output=$output" - echo "expect=$expected" - - [ "$status" == 0 ] - [ "$output" = "$expected" ] -} - -@test "Detect WSL" { - echo " - When /proc/version contains Microsoft, report WSL - " - - echo "proc version contains Microsoft in it" > "$BATS_TMPDIR/proc_version" - - # shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - # shellcheck disable=SC2034 - PROC_VERSION="$BATS_TMPDIR/proc_version" - status=0 - output=$( set_operating_system; echo "$OPERATING_SYSTEM" ) || { - status=$? - true - } - - expected="WSL" - - echo "output=$output" - echo "expect=$expected" - - [ "$status" == 0 ] - [ "$output" = "$expected" ] -} diff --git a/test/006_unit_query_distro.bats b/test/006_unit_query_distro.bats deleted file mode 100644 index 639ab29..0000000 --- a/test/006_unit_query_distro.bats +++ /dev/null @@ -1,49 +0,0 @@ -load common -load_fixtures - -@test "Query distro (lsb_release present)" { - echo " - Use value of lsb_release -si - " - - #shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - status=0 - { output=$( query_distro ); } || { - status=$? - true - } - - expected="${T_DISTRO}" - - echo "output=$output" - echo "expect=$expected" - - [ "$status" == 0 ] - [ "$output" = "$expected" ] -} - -@test "Query distro (lsb_release missing)" { - echo " - Empty value if lsb_release is missing - " - - #shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - LSB_RELEASE_PROGRAM="missing_lsb_release" - echo "Using $LSB_RELEASE_PROGRAM as lsb_release" - - status=0 - { output=$( query_distro ); } || { - status=$? - true - } - - expected="" - - echo "output=$output" - echo "expect=$expected" - - [ "$status" == 0 ] - [ "$output" = "$expected" ] -} diff --git a/test/007_unit_parse_encrypt.bats b/test/007_unit_parse_encrypt.bats deleted file mode 100644 index 54ee3a9..0000000 --- a/test/007_unit_parse_encrypt.bats +++ /dev/null @@ -1,318 +0,0 @@ -load common -load_fixtures - -setup() { - # SC2153 is intentional - # shellcheck disable=SC2153 - make_parents "$T_YADM_ENCRYPT" - make_parents "$T_DIR_WORK" - make_parents "$T_DIR_REPO" - mkdir "$T_DIR_WORK" - git init --shared=0600 --bare "$T_DIR_REPO" >/dev/null 2>&1 - GIT_DIR="$T_DIR_REPO" git config core.bare 'false' - GIT_DIR="$T_DIR_REPO" git config core.worktree "$T_DIR_WORK" - GIT_DIR="$T_DIR_REPO" git config yadm.managed 'true' -} - -teardown() { - destroy_tmp -} - -function run_parse() { - # shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - YADM_ENCRYPT="$T_YADM_ENCRYPT" - export YADM_ENCRYPT - GIT_DIR="$T_DIR_REPO" - export GIT_DIR - - # shellcheck disable=SC2034 - - status=0 - { output=$( parse_encrypt) && parse_encrypt; } || { - status=$? - true - } - - if [ "$1" == "twice" ]; then - GIT_DIR="$T_DIR_REPO" parse_encrypt - fi - - echo -e "OUTPUT:$output\n" - echo "ENCRYPT_INCLUDE_FILES:" - echo " Size: ${#ENCRYPT_INCLUDE_FILES[@]}" - echo " Items: ${ENCRYPT_INCLUDE_FILES[*]}" - echo "EXPECT_INCLUDE:" - echo " Size: ${#EXPECT_INCLUDE[@]}" - echo " Items: ${EXPECT_INCLUDE[*]}" -} - -@test "parse_encrypt (not called)" { - echo " - parse_encrypt() is not called - Array should be 'unparsed' - " - - # shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - - echo "ENCRYPT_INCLUDE_FILES=$ENCRYPT_INCLUDE_FILES" - - [ "$ENCRYPT_INCLUDE_FILES" == "unparsed" ] - -} - -@test "parse_encrypt (short-circuit)" { - echo " - Parsing should not happen more than once - " - - run_parse "twice" - echo "PARSE_ENCRYPT_SHORT: $PARSE_ENCRYPT_SHORT" - - [ "$status" == 0 ] - [ "$output" == "" ] - [[ "$PARSE_ENCRYPT_SHORT" =~ not\ reprocessed ]] -} - -@test "parse_encrypt (file missing)" { - echo " - .yadm/encrypt is empty - Array should be empty - " - - EXPECT_INCLUDE=() - - run_parse - - [ "$status" == 0 ] - [ "$output" == "" ] - [ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ] - [ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ] -} - -@test "parse_encrypt (empty file)" { - echo " - .yadm/encrypt is empty - Array should be empty - " - - touch "$T_YADM_ENCRYPT" - - EXPECT_INCLUDE=() - - run_parse - - [ "$status" == 0 ] - [ "$output" == "" ] - [ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ] - [ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ] -} - -@test "parse_encrypt (files)" { - echo " - .yadm/encrypt is references present and missing files - Array should be as expected - " - - echo "file1" > "$T_DIR_WORK/file1" - echo "file3" > "$T_DIR_WORK/file3" - echo "file5" > "$T_DIR_WORK/file5" - - { echo "file1" - echo "file2" - echo "file3" - echo "file4" - echo "file5" - } > "$T_YADM_ENCRYPT" - - EXPECT_INCLUDE=("file1" "file3" "file5") - - run_parse - - [ "$status" == 0 ] - [ "$output" == "" ] - [ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ] - [ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ] -} - -@test "parse_encrypt (files and dirs)" { - echo " - .yadm/encrypt is references present and missing files - .yadm/encrypt is references present and missing dirs - Array should be as expected - " - - mkdir -p "$T_DIR_WORK/dir1" - mkdir -p "$T_DIR_WORK/dir2" - echo "file1" > "$T_DIR_WORK/file1" - echo "file2" > "$T_DIR_WORK/file2" - echo "a" > "$T_DIR_WORK/dir1/a" - echo "b" > "$T_DIR_WORK/dir1/b" - - { echo "file1" - echo "file2" - echo "file3" - echo "dir1" - echo "dir2" - echo "dir3" - } > "$T_YADM_ENCRYPT" - - EXPECT_INCLUDE=("file1" "file2" "dir1" "dir2") - - run_parse - - [ "$status" == 0 ] - [ "$output" == "" ] - [ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ] - [ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ] -} - -@test "parse_encrypt (comments/empty lines)" { - echo " - .yadm/encrypt is references present and missing files - .yadm/encrypt is references present and missing dirs - .yadm/encrypt contains comments / blank lines - Array should be as expected - " - - mkdir -p "$T_DIR_WORK/dir1" - mkdir -p "$T_DIR_WORK/dir2" - echo "file1" > "$T_DIR_WORK/file1" - echo "file2" > "$T_DIR_WORK/file2" - echo "file3" > "$T_DIR_WORK/file3" - echo "a" > "$T_DIR_WORK/dir1/a" - echo "b" > "$T_DIR_WORK/dir1/b" - - { echo "file1" - echo "file2" - echo "#file3" - echo " #file3" - echo "" - echo "dir1" - echo "dir2" - echo "dir3" - } > "$T_YADM_ENCRYPT" - - EXPECT_INCLUDE=("file1" "file2" "dir1" "dir2") - - run_parse - - [ "$status" == 0 ] - [ "$output" == "" ] - [ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ] - [ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ] -} - -@test "parse_encrypt (w/spaces)" { - echo " - .yadm/encrypt is references present and missing files - .yadm/encrypt is references present and missing dirs - .yadm/encrypt references contain spaces - Array should be as expected - " - - mkdir -p "$T_DIR_WORK/di r1" - mkdir -p "$T_DIR_WORK/dir2" - echo "file1" > "$T_DIR_WORK/file1" - echo "fi le2" > "$T_DIR_WORK/fi le2" - echo "file3" > "$T_DIR_WORK/file3" - echo "a" > "$T_DIR_WORK/di r1/a" - echo "b" > "$T_DIR_WORK/di r1/b" - - { echo "file1" - echo "fi le2" - echo "#file3" - echo " #file3" - echo "" - echo "di r1" - echo "dir2" - echo "dir3" - } > "$T_YADM_ENCRYPT" - - EXPECT_INCLUDE=("file1" "fi le2" "di r1" "dir2") - - run_parse - - [ "$status" == 0 ] - [ "$output" == "" ] - [ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ] - [ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ] -} - -@test "parse_encrypt (wildcards)" { - echo " - .yadm/encrypt contains wildcards - Array should be as expected - " - - mkdir -p "$T_DIR_WORK/di r1" - mkdir -p "$T_DIR_WORK/dir2" - echo "file1" > "$T_DIR_WORK/file1" - echo "fi le2" > "$T_DIR_WORK/fi le2" - echo "file2" > "$T_DIR_WORK/file2" - echo "file3" > "$T_DIR_WORK/file3" - echo "a" > "$T_DIR_WORK/di r1/a" - echo "b" > "$T_DIR_WORK/di r1/b" - - { echo "fi*" - echo "#file3" - echo " #file3" - echo "" - echo "#dir2" - echo "di r1" - echo "dir2" - echo "dir3" - } > "$T_YADM_ENCRYPT" - - EXPECT_INCLUDE=("fi le2" "file1" "file2" "file3" "di r1" "dir2") - - run_parse - - [ "$status" == 0 ] - [ "$output" == "" ] - [ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ] - [ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ] -} - -@test "parse_encrypt (excludes)" { - echo " - .yadm/encrypt contains exclusions - Array should be as expected - " - - mkdir -p "$T_DIR_WORK/di r1" - mkdir -p "$T_DIR_WORK/dir2" - mkdir -p "$T_DIR_WORK/dir3" - echo "file1" > "$T_DIR_WORK/file1" - echo "file1.ex" > "$T_DIR_WORK/file1.ex" - echo "fi le2" > "$T_DIR_WORK/fi le2" - echo "file3" > "$T_DIR_WORK/file3" - echo "test" > "$T_DIR_WORK/test" - echo "a.txt" > "$T_DIR_WORK/di r1/a.txt" - echo "b.txt" > "$T_DIR_WORK/di r1/b.txt" - echo "c.inc" > "$T_DIR_WORK/di r1/c.inc" - - { echo "fi*" - echo "#file3" - echo " #file3" - echo "" - echo " #test" - echo "#dir2" - echo "di r1/*" - echo "dir2" - echo "dir3" - echo "dir4" - echo "!*.ex" - echo "!di r1/*.txt" - } > "$T_YADM_ENCRYPT" - - EXPECT_INCLUDE=("fi le2" "file1" "file3" "di r1/c.inc" "dir2" "dir3") - - run_parse - - [ "$status" == 0 ] - [ "$output" == "" ] - [ "${#ENCRYPT_INCLUDE_FILES[@]}" -eq "${#EXPECT_INCLUDE[@]}" ] - [ "${ENCRYPT_INCLUDE_FILES[*]}" == "${EXPECT_INCLUDE[*]}" ] -} diff --git a/test/100_accept_version.bats b/test/100_accept_version.bats deleted file mode 100644 index 962b91f..0000000 --- a/test/100_accept_version.bats +++ /dev/null @@ -1,25 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -@test "Command 'version'" { - echo " - When 'version' command is provided, - Print the current version with format 'yadm x.x.x' - Exit with 0 - " - - #; run yadm with 'version' command - run "$T_YADM" version - - # shellcheck source=/dev/null - - #; load yadm variables (including VERSION) - YADM_TEST=1 source "$T_YADM" - - #; validate status and output - [ $status -eq 0 ] - [ "$output" = "yadm $VERSION" ] - version_regex="^yadm [[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+$" - [[ "$output" =~ $version_regex ]] -} diff --git a/test/101_accept_help.bats b/test/101_accept_help.bats deleted file mode 100644 index 5932379..0000000 --- a/test/101_accept_help.bats +++ /dev/null @@ -1,33 +0,0 @@ -load common -load_fixtures -status=;lines=; #; populated by bats run() - -@test "Missing command" { - echo " - When no command is provided, - Produce usage instructions - Exit with 1 - " - - #; run yadm with no command - run "$T_YADM" - - #; validate status and output - [ $status -eq 1 ] - [[ "${lines[0]}" =~ ^Usage: ]] -} - -@test "Command 'help'" { - echo " - When 'help' command is provided, - Produce usage instructions - Exit with value 1 - " - - #; run yadm with 'help' command - run "$T_YADM" help - - #; validate status and output - [ $status -eq 1 ] - [[ "${lines[0]}" =~ ^Usage: ]] -} diff --git a/test/102_accept_clean.bats b/test/102_accept_clean.bats deleted file mode 100644 index 1ab0e77..0000000 --- a/test/102_accept_clean.bats +++ /dev/null @@ -1,19 +0,0 @@ -load common -load_fixtures -status=;lines=; #; populated by bats run() - -@test "Command 'clean'" { - echo " - When 'clean' command is provided, - Do nothing, this is a dangerous Git command when managing dot files - Report the command as disabled - Exit with 1 - " - - #; run yadm with 'clean' command - run "$T_YADM" clean - - #; validate status and output - [ $status -eq 1 ] - [[ "${lines[0]}" =~ disabled ]] -} diff --git a/test/103_accept_git.bats b/test/103_accept_git.bats deleted file mode 100644 index 61a5a45..0000000 --- a/test/103_accept_git.bats +++ /dev/null @@ -1,117 +0,0 @@ -load common -load_fixtures -status=;output=;lines=; #; populated by bats run() - -IN_REPO=(.bash_profile .vimrc) - -function setup_environment() { - destroy_tmp - build_repo "${IN_REPO[@]}" -} - -@test "Passthru unknown commands to Git" { - echo " - When the command 'bogus' is provided - Report bogus is not a command - Exit with 1 - " - - #; start fresh - setup_environment - - #; run bogus - run "${T_YADM_Y[@]}" bogus - - #; validate status and output - [ "$status" -eq 1 ] - [[ "$output" =~ .bogus..is.not.a.git.command ]] -} - -@test "Git command 'add' - badfile" { - echo " - When the command 'add' is provided - And the file specified does not exist - Exit with 128 - " - - #; start fresh - setup_environment - - #; define a non existig testfile - local testfile="$T_DIR_WORK/does_not_exist" - - #; run add - run "${T_YADM_Y[@]}" add -v "$testfile" - - #; validate status and output - [ "$status" -eq 128 ] - [[ "$output" =~ pathspec.+did.not.match ]] -} - -@test "Git command 'add'" { - echo " - When the command 'add' is provided - Files are added to the index - Exit with 0 - " - - #; start fresh - setup_environment - - #; create a testfile - local testfile="$T_DIR_WORK/testfile" - echo "$testfile" > "$testfile" - - #; run add - run "${T_YADM_Y[@]}" add -v "$testfile" - - #; validate status and output - [ "$status" -eq 0 ] - [ "$output" = "add 'testfile'" ] -} - -@test "Git command 'status'" { - echo " - When the command 'status' is provided - Added files are shown - Exit with 0 - " - - #; run status - run "${T_YADM_Y[@]}" status - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ new\ file:[[:space:]]+testfile ]] -} - -@test "Git command 'commit'" { - echo " - When the command 'commit' is provided - Index is commited - Exit with 0 - " - - #; run commit - run "${T_YADM_Y[@]}" commit -m 'Add testfile' - - #; validate status and output - [ "$status" -eq 0 ] - [[ "${lines[1]}" =~ 1\ file\ changed ]] - [[ "${lines[1]}" =~ 1\ insertion ]] -} - -@test "Git command 'log'" { - echo " - When the command 'log' is provided - Commits are shown - Exit with 0 - " - - #; run log - run "${T_YADM_Y[@]}" log --oneline - - #; validate status and output - [ "$status" -eq 0 ] - [[ "${lines[0]}" =~ Add\ testfile ]] -} diff --git a/test/104_accept_init.bats b/test/104_accept_init.bats deleted file mode 100644 index 1751e7a..0000000 --- a/test/104_accept_init.bats +++ /dev/null @@ -1,178 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -setup() { - destroy_tmp - create_worktree "$T_DIR_WORK" -} - -@test "Command 'init'" { - echo " - When 'init' command is provided, - Create new repo with attributes: - - 0600 permissions - - not bare - - worktree = \$HOME - - showUntrackedFiles = no - - yadm.managed = true - Report the repo as initialized - Exit with 0 - " - - #; run init - run "${T_YADM_Y[@]}" init - - #; validate status and output - [ $status -eq 0 ] - [[ "$output" =~ Initialized ]] - - #; validate repo attributes - test_perms "$T_DIR_REPO" "drw.--.--." - test_repo_attribute "$T_DIR_REPO" core.bare false - test_repo_attribute "$T_DIR_REPO" core.worktree "$HOME" - test_repo_attribute "$T_DIR_REPO" status.showUntrackedFiles no - test_repo_attribute "$T_DIR_REPO" yadm.managed true -} - -@test "Command 'init' -w (alternate worktree)" { - echo " - When 'init' command is provided, - and '-w' is provided, - Create new repo with attributes: - - 0600 permissions - - not bare - - worktree = \$YADM_WORK - - showUntrackedFiles = no - - yadm.managed = true - Report the repo as initialized - Exit with 0 - " - - #; run init - run "${T_YADM_Y[@]}" init -w "$T_DIR_WORK" - - #; validate status and output - [ $status -eq 0 ] - [[ "$output" =~ Initialized ]] - - #; validate repo attributes - test_perms "$T_DIR_REPO" "drw.--.--." - test_repo_attribute "$T_DIR_REPO" core.bare false - test_repo_attribute "$T_DIR_REPO" core.worktree "$T_DIR_WORK" - test_repo_attribute "$T_DIR_REPO" status.showUntrackedFiles no - test_repo_attribute "$T_DIR_REPO" yadm.managed true -} - -@test "Command 'init' (existing repo)" { - echo " - When 'init' command is provided, - and a repo already exists, - Refuse to create a new repo - Exit with 1 - " - - #; create existing repo content - mkdir -p "$T_DIR_REPO" - local testfile="$T_DIR_REPO/testfile" - touch "$testfile" - - #; run init - run "${T_YADM_Y[@]}" init - - #; validate status and output - [ $status -eq 1 ] - [[ "$output" =~ already.exists ]] - - #; verify existing repo is intact - if [ ! -e "$testfile" ]; then - echo "ERROR: existing repo has been changed" - return 1 - fi - -} - -@test "Command 'init' -f (force overwrite repo)" { - echo " - When 'init' command is provided, - and '-f' is provided - and a repo already exists, - Remove existing repo - Create new repo with attributes: - - 0600 permissions - - not bare - - worktree = \$HOME - - showUntrackedFiles = no - - yadm.managed = true - Report the repo as initialized - Exit with 0 - " - - #; create existing repo content - mkdir -p "$T_DIR_REPO" - local testfile="$T_DIR_REPO/testfile" - touch "$testfile" - - #; run init - run "${T_YADM_Y[@]}" init -f - - #; validate status and output - [ $status -eq 0 ] - [[ "$output" =~ Initialized ]] - - #; verify existing repo is gone - if [ -e "$testfile" ]; then - echo "ERROR: existing repo files remain" - return 1 - fi - - #; validate repo attributes - test_perms "$T_DIR_REPO" "drw.--.--." - test_repo_attribute "$T_DIR_REPO" core.bare false - test_repo_attribute "$T_DIR_REPO" core.worktree "$HOME" - test_repo_attribute "$T_DIR_REPO" status.showUntrackedFiles no - test_repo_attribute "$T_DIR_REPO" yadm.managed true -} - -@test "Command 'init' -f -w (force overwrite repo with alternate worktree)" { - echo " - When 'init' command is provided, - and '-f' is provided - and '-w' is provided - and a repo already exists, - Remove existing repo - Create new repo with attributes: - - 0600 permissions - - not bare - - worktree = \$YADM_WORK - - showUntrackedFiles = no - - yadm.managed = true - Report the repo as initialized - Exit with 0 - " - - #; create existing repo content - mkdir -p "$T_DIR_REPO" - local testfile="$T_DIR_REPO/testfile" - touch "$testfile" - - #; run init - run "${T_YADM_Y[@]}" init -f -w "$T_DIR_WORK" - - #; validate status and output - [ $status -eq 0 ] - [[ "$output" =~ Initialized ]] - - #; verify existing repo is gone - if [ -e "$testfile" ]; then - echo "ERROR: existing repo files remain" - return 1 - fi - - #; validate repo attributes - test_perms "$T_DIR_REPO" "drw.--.--." - test_repo_attribute "$T_DIR_REPO" core.bare false - test_repo_attribute "$T_DIR_REPO" core.worktree "$T_DIR_WORK" - test_repo_attribute "$T_DIR_REPO" status.showUntrackedFiles no - test_repo_attribute "$T_DIR_REPO" yadm.managed true -} diff --git a/test/105_accept_clone.bats b/test/105_accept_clone.bats deleted file mode 100644 index 42750b8..0000000 --- a/test/105_accept_clone.bats +++ /dev/null @@ -1,579 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -IN_REPO=(.bash_profile .vimrc) -T_DIR_REMOTE="$T_TMP/remote" -REMOTE_URL="file:///$T_TMP/remote" - -setup() { - destroy_tmp - build_repo "${IN_REPO[@]}" - cp -rp "$T_DIR_REPO" "$T_DIR_REMOTE" -} - -create_bootstrap() { - make_parents "$T_YADM_BOOTSTRAP" - { - echo "#!/bin/bash" - echo "echo Bootstrap successful" - echo "exit 123" - } > "$T_YADM_BOOTSTRAP" - chmod a+x "$T_YADM_BOOTSTRAP" -} - -@test "Command 'clone' (bad remote)" { - echo " - When 'clone' command is provided, - and the remote is bad, - Report error - Remove the YADM_REPO - Exit with 1 - " - - #; remove existing worktree and repo - rm -rf "$T_DIR_WORK" - mkdir -p "$T_DIR_WORK" - rm -rf "$T_DIR_REPO" - - #; run clone - run "${T_YADM_Y[@]}" clone -w "$T_DIR_WORK" "file:///bogus-repo" - - #; validate status and output - [ "$status" -eq 1 ] - [[ "$output" =~ Unable\ to\ fetch\ origin ]] - - #; confirm repo directory is removed - [ ! -d "$T_DIR_REPO" ] -} - -@test "Command 'clone'" { - echo " - When 'clone' command is provided, - Create new repo with attributes: - - 0600 permissions - - not bare - - worktree = \$YADM_WORK - - showUntrackedFiles = no - - yadm.managed = true - Report the repo as cloned - A remote named origin exists - Exit with 0 - " - - #; remove existing worktree and repo - rm -rf "$T_DIR_WORK" - mkdir -p "$T_DIR_WORK" - rm -rf "$T_DIR_REPO" - - #; run clone - run "${T_YADM_Y[@]}" clone -w "$T_DIR_WORK" "$REMOTE_URL" - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ Initialized ]] - - #; validate repo attributes - test_perms "$T_DIR_REPO" "drw.--.--." - test_repo_attribute "$T_DIR_REPO" core.bare false - test_repo_attribute "$T_DIR_REPO" core.worktree "$T_DIR_WORK" - test_repo_attribute "$T_DIR_REPO" status.showUntrackedFiles no - test_repo_attribute "$T_DIR_REPO" yadm.managed true - - #; test the remote - local remote_output - remote_output=$(GIT_DIR="$T_DIR_REPO" git remote show) - [ "$remote_output" = "origin" ] -} - -@test "Command 'clone' (existing repo)" { - echo " - When 'clone' command is provided, - and a repo already exists, - Report error - Exit with 1 - " - - #; run clone - run "${T_YADM_Y[@]}" clone -w "$T_DIR_WORK" "$REMOTE_URL" - - #; validate status and output - [ "$status" -eq 1 ] - [[ "$output" =~ Git\ repo\ already\ exists ]] -} - -@test "Command 'clone' -f (force overwrite)" { - echo " - When 'clone' command is provided, - and '-f' is provided, - and a repo already exists, - Overwrite the repo with attributes: - - 0600 permissions - - not bare - - worktree = \$YADM_WORK - - showUntrackedFiles = no - - yadm.managed = true - Report the repo as cloned - A remote named origin exists - Exit with 0 - " - - #; remove existing worktree - rm -rf "$T_DIR_WORK" - mkdir -p "$T_DIR_WORK" - - #; run clone - run "${T_YADM_Y[@]}" clone -w "$T_DIR_WORK" -f "$REMOTE_URL" - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ Initialized ]] - - #; validate repo attributes - test_perms "$T_DIR_REPO" "drw.--.--." - test_repo_attribute "$T_DIR_REPO" core.bare false - test_repo_attribute "$T_DIR_REPO" core.worktree "$T_DIR_WORK" - test_repo_attribute "$T_DIR_REPO" status.showUntrackedFiles no - test_repo_attribute "$T_DIR_REPO" yadm.managed true - - #; test the remote - local remote_output - remote_output=$(GIT_DIR="$T_DIR_REPO" git remote show) - [ "$remote_output" = "origin" ] -} - -@test "Command 'clone' (existing conflicts)" { - echo " - When 'clone' command is provided, - and '-f' is provided, - and a repo already exists, - Overwrite the repo with attributes: - - 0600 permissions - - not bare - - worktree = \$YADM_WORK - - showUntrackedFiles = no - - yadm.managed = true - Report the repo as cloned - A remote named origin exists - Exit with 0 - " - - #; remove existing repo - rm -rf "$T_DIR_REPO" - - #; cause a conflict - echo "conflict" >> "$T_DIR_WORK/.bash_profile" - - #; run clone - run "${T_YADM_Y[@]}" clone -w "$T_DIR_WORK" "$REMOTE_URL" - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ Initialized ]] - - #; validate merging note - [[ "$output" =~ Merging\ origin/master\ failed ]] - [[ "$output" =~ NOTE ]] - - #; validate repo attributes - test_perms "$T_DIR_REPO" "drw.--.--." - test_repo_attribute "$T_DIR_REPO" core.bare false - test_repo_attribute "$T_DIR_REPO" core.worktree "$T_DIR_WORK" - test_repo_attribute "$T_DIR_REPO" status.showUntrackedFiles no - test_repo_attribute "$T_DIR_REPO" yadm.managed true - - #; test the remote - local remote_output - remote_output=$(GIT_DIR="$T_DIR_REPO" git remote show) - [ "$remote_output" = "origin" ] - - #; confirm yadm repo is clean - cd "$T_DIR_WORK" ||: - clean_status=$("${T_YADM_Y[@]}" status -uno --porcelain) - echo "clean_status:'$clean_status'" - [ -z "$clean_status" ] - - #; confirm conflicts are stashed - existing_stash=$("${T_YADM_Y[@]}" stash list) - echo "existing_stash:'$existing_stash'" - [[ "$existing_stash" =~ Conflicts\ preserved ]] - - stashed_conflicts=$("${T_YADM_Y[@]}" stash show -p) - echo "stashed_conflicts:'$stashed_conflicts'" - [[ "$stashed_conflicts" =~ \+conflict ]] - -} - -@test "Command 'clone' (force bootstrap, missing)" { - echo " - When 'clone' command is provided, - with the --bootstrap parameter - and bootstrap does not exists - Create new repo with attributes: - - 0600 permissions - - not bare - - worktree = \$YADM_WORK - - showUntrackedFiles = no - - yadm.managed = true - Report the repo as cloned - A remote named origin exists - Exit with 0 - " - - #; remove existing worktree and repo - rm -rf "$T_DIR_WORK" - mkdir -p "$T_DIR_WORK" - rm -rf "$T_DIR_REPO" - - #; run clone - run "${T_YADM_Y[@]}" clone --bootstrap -w "$T_DIR_WORK" "$REMOTE_URL" - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ Initialized ]] - - #; validate repo attributes - test_perms "$T_DIR_REPO" "drw.--.--." - test_repo_attribute "$T_DIR_REPO" core.bare false - test_repo_attribute "$T_DIR_REPO" core.worktree "$T_DIR_WORK" - test_repo_attribute "$T_DIR_REPO" status.showUntrackedFiles no - test_repo_attribute "$T_DIR_REPO" yadm.managed true - - #; test the remote - local remote_output - remote_output=$(GIT_DIR="$T_DIR_REPO" git remote show) - [ "$remote_output" = "origin" ] -} - -@test "Command 'clone' (force bootstrap, existing)" { - echo " - When 'clone' command is provided, - with the --bootstrap parameter - and bootstrap exists - Create new repo with attributes: - - 0600 permissions - - not bare - - worktree = \$YADM_WORK - - showUntrackedFiles = no - - yadm.managed = true - Report the repo as cloned - A remote named origin exists - Run the bootstrap - Exit with bootstrap's exit code - " - - #; remove existing worktree and repo - rm -rf "$T_DIR_WORK" - mkdir -p "$T_DIR_WORK" - rm -rf "$T_DIR_REPO" - - #; create the bootstrap - create_bootstrap - - #; run clone - run "${T_YADM_Y[@]}" clone --bootstrap -w "$T_DIR_WORK" "$REMOTE_URL" - - #; validate status and output - [ "$status" -eq 123 ] - [[ "$output" =~ Initialized ]] - [[ "$output" =~ Bootstrap\ successful ]] - - #; validate repo attributes - test_perms "$T_DIR_REPO" "drw.--.--." - test_repo_attribute "$T_DIR_REPO" core.bare false - test_repo_attribute "$T_DIR_REPO" core.worktree "$T_DIR_WORK" - test_repo_attribute "$T_DIR_REPO" status.showUntrackedFiles no - test_repo_attribute "$T_DIR_REPO" yadm.managed true - - #; test the remote - local remote_output - remote_output=$(GIT_DIR="$T_DIR_REPO" git remote show) - [ "$remote_output" = "origin" ] -} - -@test "Command 'clone' (prevent bootstrap)" { - echo " - When 'clone' command is provided, - with the --no-bootstrap parameter - and bootstrap exists - Create new repo with attributes: - - 0600 permissions - - not bare - - worktree = \$YADM_WORK - - showUntrackedFiles = no - - yadm.managed = true - Report the repo as cloned - A remote named origin exists - Do NOT run bootstrap - Exit with 0 - " - - #; remove existing worktree and repo - rm -rf "$T_DIR_WORK" - mkdir -p "$T_DIR_WORK" - rm -rf "$T_DIR_REPO" - - #; create the bootstrap - create_bootstrap - - #; run clone - run "${T_YADM_Y[@]}" clone --no-bootstrap -w "$T_DIR_WORK" "$REMOTE_URL" - - #; validate status and output - [ "$status" -eq 0 ] - [[ $output =~ Initialized ]] - [[ ! $output =~ Bootstrap\ successful ]] - - #; validate repo attributes - test_perms "$T_DIR_REPO" "drw.--.--." - test_repo_attribute "$T_DIR_REPO" core.bare false - test_repo_attribute "$T_DIR_REPO" core.worktree "$T_DIR_WORK" - test_repo_attribute "$T_DIR_REPO" status.showUntrackedFiles no - test_repo_attribute "$T_DIR_REPO" yadm.managed true - - #; test the remote - local remote_output - remote_output=$(GIT_DIR="$T_DIR_REPO" git remote show) - [ "$remote_output" = "origin" ] -} - -@test "Command 'clone' (existing bootstrap, answer n)" { - echo " - When 'clone' command is provided, - and bootstrap exists - Create new repo with attributes: - - 0600 permissions - - not bare - - worktree = \$YADM_WORK - - showUntrackedFiles = no - - yadm.managed = true - Report the repo as cloned - A remote named origin exists - Do NOT run bootstrap - Exit with 0 - " - - #; remove existing worktree and repo - rm -rf "$T_DIR_WORK" - mkdir -p "$T_DIR_WORK" - rm -rf "$T_DIR_REPO" - - #; create the bootstrap - create_bootstrap - - #; run clone - run expect < "$T_YADM_CONFIG" - - #; run config - run "${T_YADM_Y[@]}" config "$T_KEY" - - #; validate status and output - [ $status -eq 0 ] - if [ "$output" != "$T_VALUE" ]; then - echo "ERROR: Incorrect value returned. Expected '$T_VALUE', got '$output'" - return 1 - fi -} - -@test "Command 'config' (update)" { - echo " - When 'config' command is provided, - and an attribute is provided - and the attribute is already configured - Report no output - Update configuration file - Exit with 0 - " - - #; manually load a value into the configuration - make_parents "$T_YADM_CONFIG" - echo -e "${T_EXPECTED}_with_extra_data" > "$T_YADM_CONFIG" - - #; run config - run "${T_YADM_Y[@]}" config "$T_KEY" "$T_VALUE" - - #; validate status and output - [ $status -eq 0 ] - [ "$output" = "" ] - - #; validate configuration - local config - config=$(cat "$T_YADM_CONFIG") - local expected - expected=$(echo -e "$T_EXPECTED") - if [ "$config" != "$expected" ]; then - echo "ERROR: Config does not match expected" - echo "$config" - return 1 - fi -} - -@test "Command 'config' (local read)" { - echo " - When 'config' command is provided, - and an attribute is provided - and the attribute is configured - and the attribute is local.* - Fetch the value from the repo config - Report the requested value - Exit with 0 - " - - #; write local attributes - build_repo - for loption in class os hostname user; do - GIT_DIR="$T_DIR_REPO" git config "local.$loption" "custom_$loption" - done - - #; run config - for loption in class os hostname user; do - run "${T_YADM_Y[@]}" config "local.$loption" - #; validate status and output - [ $status -eq 0 ] - if [ "$output" != "custom_$loption" ]; then - echo "ERROR: Incorrect value returned. Expected 'custom_$loption', got '$output'" - return 1 - fi - done - -} - -@test "Command 'config' (local write)" { - echo " - When 'config' command is provided, - and an attribute is provided - and a value is provided - and the attribute is local.* - Report no output - Write the value to the repo config - Exit with 0 - " - - build_repo - local expected - local linecount - expected="[local]\n" - linecount=1 - for loption in class os hostname user; do - #; update expected - expected="$expected\t$loption = custom_$loption\n" - ((linecount+=1)) - #; write local attributes - run "${T_YADM_Y[@]}" config "local.$loption" "custom_$loption" - - #; validate status and output - [ $status -eq 0 ] - [ "$output" = "" ] - done - - #; validate data - local config - config=$(tail "-$linecount" "$T_DIR_REPO/config") - expected=$(echo -ne "$expected") - if [ "$config" != "$expected" ]; then - echo "ERROR: Config does not match expected" - echo -e "$config" - echo -e "EXPECTED:\n$expected" - return 1 - fi - -} diff --git a/test/107_accept_list.bats b/test/107_accept_list.bats deleted file mode 100644 index 3cc61d3..0000000 --- a/test/107_accept_list.bats +++ /dev/null @@ -1,93 +0,0 @@ -load common -load_fixtures -status=;lines=; #; populated by bats run() - -IN_REPO=(.bash_profile .hammerspoon/init.lua .vimrc) -SUBDIR=".hammerspoon" -IN_SUBDIR=(init.lua) - -function setup() { - destroy_tmp - build_repo "${IN_REPO[@]}" -} - -@test "Command 'list' -a" { - echo " - When 'list' command is provided, - and '-a' is provided, - List tracked files - Exit with 0 - " - - #; run list -a - run "${T_YADM_Y[@]}" list -a - - #; validate status and output - [ "$status" -eq 0 ] - local line=0 - for f in "${IN_REPO[@]}"; do - [ "${lines[$line]}" = "$f" ] - ((line++)) || true - done -} - -@test "Command 'list' (outside of worktree)" { - echo " - When 'list' command is provided, - and while outside of the worktree - List tracked files - Exit with 0 - " - - #; run list - run "${T_YADM_Y[@]}" list - - #; validate status and output - [ "$status" -eq 0 ] - local line=0 - for f in "${IN_REPO[@]}"; do - [ "${lines[$line]}" = "$f" ] - ((line++)) || true - done -} - -@test "Command 'list' (in root of worktree)" { - echo " - When 'list' command is provided, - and while in root of the worktree - List tracked files - Exit with 0 - " - - #; run list - run bash -c "(cd '$T_DIR_WORK'; ${T_YADM_Y[*]} list)" - - #; validate status and output - [ "$status" -eq 0 ] - local line=0 - for f in "${IN_REPO[@]}"; do - [ "${lines[$line]}" = "$f" ] - ((line++)) || true - done -} - -@test "Command 'list' (in subdirectory of worktree)" { - echo " - When 'list' command is provided, - and while in subdirectory of the worktree - List tracked files for current directory - Exit with 0 - " - - #; run list - run bash -c "(cd '$T_DIR_WORK/$SUBDIR'; ${T_YADM_Y[*]} list)" - - #; validate status and output - [ "$status" -eq 0 ] - local line=0 - for f in "${IN_SUBDIR[@]}"; do - echo "'${lines[$line]}' = '$f'" - [ "${lines[$line]}" = "$f" ] - ((line++)) || true - done -} diff --git a/test/108_accept_alt.bats b/test/108_accept_alt.bats deleted file mode 100644 index 5ebf9c8..0000000 --- a/test/108_accept_alt.bats +++ /dev/null @@ -1,415 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -IN_REPO=(alt* "dir one") -export TEST_TREE_WITH_ALT=1 -EXCLUDED_NAME="excluded-base" - -function create_encrypt() { - for efile in "encrypted-base##" "encrypted-system##$T_SYS" "encrypted-host##$T_SYS.$T_HOST" "encrypted-user##$T_SYS.$T_HOST.$T_USER"; do - echo "$efile" >> "$T_YADM_ENCRYPT" - echo "$efile" >> "$T_DIR_WORK/$efile" - mkdir -p "$T_DIR_WORK/dir one/$efile" - echo "dir one/$efile/file1" >> "$T_YADM_ENCRYPT" - echo "dir one/$efile/file1" >> "$T_DIR_WORK/dir one/$efile/file1" - done - - echo "$EXCLUDED_NAME##" >> "$T_YADM_ENCRYPT" - echo "!$EXCLUDED_NAME##" >> "$T_YADM_ENCRYPT" - echo "$EXCLUDED_NAME##" >> "$T_DIR_WORK/$EXCLUDED_NAME##" -} - -setup() { - destroy_tmp - build_repo "${IN_REPO[@]}" - create_encrypt -} - -function test_alt() { - local alt_type="$1" - local test_overwrite="$2" - local auto_alt="$3" - - #; detemine test parameters - case $alt_type in - base) - link_name="alt-base" - link_match="$link_name##" - ;; - system) - link_name="alt-system" - link_match="$link_name##$T_SYS" - ;; - host) - link_name="alt-host" - link_match="$link_name##$T_SYS.$T_HOST" - ;; - user) - link_name="alt-user" - link_match="$link_name##$T_SYS.$T_HOST.$T_USER" - ;; - encrypted_base) - link_name="encrypted-base" - link_match="$link_name##" - ;; - encrypted_system) - link_name="encrypted-system" - link_match="$link_name##$T_SYS" - ;; - encrypted_host) - link_name="encrypted-host" - link_match="$link_name##$T_SYS.$T_HOST" - ;; - encrypted_user) - link_name="encrypted-user" - link_match="$link_name##$T_SYS.$T_HOST.$T_USER" - ;; - override_system) - link_name="alt-override-system" - link_match="$link_name##custom_system" - ;; - override_host) - link_name="alt-override-host" - link_match="$link_name##$T_SYS.custom_host" - ;; - override_user) - link_name="alt-override-user" - link_match="$link_name##$T_SYS.$T_HOST.custom_user" - ;; - class_aaa) - link_name="alt-system" - link_match="$link_name##aaa" - ;; - class_zzz) - link_name="alt-system" - link_match="$link_name##zzz" - ;; - class_AAA) - link_name="alt-system" - link_match="$link_name##AAA" - ;; - class_ZZZ) - link_name="alt-system" - link_match="$link_name##ZZZ" - ;; - esac - dir_link_name="dir one/${link_name}" - dir_link_match="dir one/${link_match}" - - if [ "$test_overwrite" = "true" ]; then - #; create incorrect links (to overwrite) - ln -nfs "$T_DIR_WORK/dir2/file2" "$T_DIR_WORK/$link_name" - ln -nfs "$T_DIR_WORK/dir2" "$T_DIR_WORK/$dir_link_name" - else - #; verify link doesn't already exist - if [ -L "$T_DIR_WORK/$link_name" ] || [ -L "$T_DIR_WORK/$dir_link_name" ]; then - echo "ERROR: Link already exists before running yadm" - return 1 - fi - fi - - #; configure yadm.auto_alt=false - if [ "$auto_alt" = "false" ]; then - git config --file="$T_YADM_CONFIG" yadm.auto-alt false - fi - - #; run yadm (alt or status) - if [ -z "$auto_alt" ]; then - run "${T_YADM_Y[@]}" alt - #; validate status and output - echo "TEST:Link Name:$link_name" - echo "TEST:DIR Link Name:$dir_link_name" - if [ "$status" != 0 ] || [[ ! "$output" =~ Linking.+$link_name ]] || [[ ! "$output" =~ Linking.+$dir_link_name ]]; then - echo "OUTPUT:$output" - echo "STATUS:$status" - echo "ERROR: Could not confirm status and output of alt command" - return 1; - fi - else - #; running any passed through Git command should trigger auto-alt - run "${T_YADM_Y[@]}" status - if [ -n "$auto_alt" ] && [[ "$output" =~ Linking.+$link_name ]] && [[ "$output" =~ Linking.+$dir_link_name ]]; then - echo "ERROR: Reporting of link should not happen" - return 1 - fi - fi - - if [ -L "$T_DIR_WORK/$EXCLUDED_NAME" ] ; then - echo "ERROR: Found link: $T_DIR_WORK/$EXCLUDED_NAME" - echo "ERROR: Excluded files should not be linked" - return 1 - fi - - #; validate link content - if [[ "$alt_type" =~ none ]] || [ "$auto_alt" = "false" ]; then - #; no link should be present - if [ -L "$T_DIR_WORK/$link_name" ] || [ -L "$T_DIR_WORK/$dir_link_name" ]; then - echo "ERROR: Links should not exist" - return 1 - fi - else - #; correct link should be present - local link_content - local dir_link_content - link_content=$(cat "$T_DIR_WORK/$link_name") - dir_link_content=$(cat "$T_DIR_WORK/$dir_link_name/file1") - if [ "$link_content" != "$link_match" ] || [ "$dir_link_content" != "$dir_link_match/file1" ]; then - echo "link_content: $link_content" - echo "dir_link_content: $dir_link_content" - echo "ERROR: Link content is not correct" - return 1 - fi - fi -} - -@test "Command 'alt' (select base)" { - echo " - When the command 'alt' is provided - and file matches only ## - Report the linking - Verify correct file is linked - Exit with 0 - " - - test_alt 'base' 'false' '' -} - -@test "Command 'alt' (select system)" { - echo " - When the command 'alt' is provided - and file matches only ##SYSTEM - Report the linking - Verify correct file is linked - Exit with 0 - " - - test_alt 'system' 'false' '' -} - -@test "Command 'alt' (select host)" { - echo " - When the command 'alt' is provided - and file matches only ##SYSTEM.HOST - Report the linking - Verify correct file is linked - Exit with 0 - " - - test_alt 'host' 'false' '' -} - -@test "Command 'alt' (select user)" { - echo " - When the command 'alt' is provided - and file matches only ##SYSTEM.HOST.USER - Report the linking - Verify correct file is linked - Exit with 0 - " - - test_alt 'user' 'false' '' -} - -@test "Command 'alt' (select none)" { - echo " - When the command 'alt' is provided - and no file matches - Verify there is no link - Exit with 0 - " - - test_alt 'none' 'false' '' -} - -@test "Command 'alt' (select class - aaa)" { - echo " - When the command 'alt' is provided - and file matches only ##CLASS - aaa - Report the linking - Verify correct file is linked - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.class aaa - - test_alt 'class_aaa' 'false' '' -} - -@test "Command 'alt' (select class - zzz)" { - echo " - When the command 'alt' is provided - and file matches only ##CLASS - zzz - Report the linking - Verify correct file is linked - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.class zzz - - test_alt 'class_zzz' 'false' '' -} - -@test "Command 'alt' (select class - AAA)" { - echo " - When the command 'alt' is provided - and file matches only ##CLASS - AAA - Report the linking - Verify correct file is linked - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.class AAA - - test_alt 'class_AAA' 'false' '' -} - -@test "Command 'alt' (select class - ZZZ)" { - echo " - When the command 'alt' is provided - and file matches only ##CLASS - ZZZ - Report the linking - Verify correct file is linked - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.class ZZZ - - test_alt 'class_ZZZ' 'false' '' -} - -@test "Command 'auto-alt' (enabled)" { - echo " - When a command possibly changes the repo - and auto-alt is configured true - automatically process alternates - report no linking (not loud) - verify alternate created - " - - test_alt 'base' 'false' 'true' -} - -@test "Command 'auto-alt' (disabled)" { - echo " - When a command possibly changes the repo - and auto-alt is configured false - do no linking - verify no links - " - - test_alt 'base' 'false' 'false' -} - -@test "Command 'alt' (overwrite existing link)" { - echo " - When the command 'alt' is provided - and the link exists, and is wrong - Report the linking - Verify correct file is linked - Exit with 0 - " - - test_alt 'base' 'true' '' -} - -@test "Command 'alt' (select encrypted base)" { - echo " - When the command 'alt' is provided - and encrypted file matches only ## - Report the linking - Verify correct encrypted file is linked - Exit with 0 - " - - test_alt 'encrypted_base' 'false' '' -} - -@test "Command 'alt' (select encrypted system)" { - echo " - When the command 'alt' is provided - and encrypted file matches only ##SYSTEM - Report the linking - Verify correct encrypted file is linked - Exit with 0 - " - - test_alt 'encrypted_system' 'false' '' -} - -@test "Command 'alt' (select encrypted host)" { - echo " - When the command 'alt' is provided - and encrypted file matches only ##SYSTEM.HOST - Report the linking - Verify correct encrypted file is linked - Exit with 0 - " - - test_alt 'encrypted_host' 'false' '' -} - -@test "Command 'alt' (select encrypted user)" { - echo " - When the command 'alt' is provided - and encrypted file matches only ##SYSTEM.HOST.USER - Report the linking - Verify correct encrypted file is linked - Exit with 0 - " - - test_alt 'encrypted_user' 'false' '' -} - -@test "Command 'alt' (select encrypted none)" { - echo " - When the command 'alt' is provided - and no encrypted file matches - Verify there is no link - Exit with 0 - " - - test_alt 'encrypted_none' 'false' '' -} - -@test "Command 'alt' (override-system)" { - echo " - When the command 'alt' is provided - and file matches only ##SYSTEM - after setting local.os - Report the linking - Verify correct file is linked - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.os custom_system - test_alt 'override_system' 'false' '' -} - -@test "Command 'alt' (override-host)" { - echo " - When the command 'alt' is provided - and file matches only ##SYSTEM.HOST - after setting local.hostname - Report the linking - Verify correct file is linked - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.hostname custom_host - test_alt 'override_host' 'false' '' -} - -@test "Command 'alt' (override-user)" { - echo " - When the command 'alt' is provided - and file matches only ##SYSTEM.HOST.USER - after setting local.user - Report the linking - Verify correct file is linked - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.user custom_user - test_alt 'override_user' 'false' '' -} diff --git a/test/109_accept_encryption.bats b/test/109_accept_encryption.bats deleted file mode 100644 index 439f44c..0000000 --- a/test/109_accept_encryption.bats +++ /dev/null @@ -1,900 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -T_PASSWD="ExamplePassword" -T_ARCHIVE_SYMMETRIC="$T_TMP/build_archive.symmetric" -T_ARCHIVE_ASYMMETRIC="$T_TMP/build_archive.asymmetric" -T_KEY_NAME="yadm-test1" -T_KEY_FINGERPRINT="F8BBFC746C58945442349BCEBA54FFD04C599B1A" -T_RECIPIENT_GOOD="[yadm]\n\tgpg-recipient = yadm-test1" -T_RECIPIENT_BAD="[yadm]\n\tgpg-recipient = invalid" -T_RECIPIENT_ASK="[yadm]\n\tgpg-recipient = ASK" - -#; use gpg1 if it's available -T_GPG_PROGRAM="gpg" -if command -v gpg1 >/dev/null 2>&1; then - T_GPG_PROGRAM="gpg1" -fi - -function import_keys() { - "$T_GPG_PROGRAM" --import "test/test_key" >/dev/null 2>&1 || true - "$T_GPG_PROGRAM" --import-ownertrust < "test/ownertrust.txt" >/dev/null 2>&1 -} - -function remove_keys() { - "$T_GPG_PROGRAM" --batch --yes --delete-secret-keys "$T_KEY_FINGERPRINT" >/dev/null 2>&1 || true - "$T_GPG_PROGRAM" --batch --yes --delete-key "$T_KEY_FINGERPRINT" >/dev/null 2>&1 || true -} - -setup() { - #; start fresh - destroy_tmp - - #; import test keys - import_keys - - #; create a worktree & repo - build_repo - - #; define a YADM_ENCRYPT - make_parents "$T_YADM_ENCRYPT" - echo -e ".ssh/*.key\n.gnupg/*.gpg" > "$T_YADM_ENCRYPT" - - #; create a YADM_ARCHIVE - ( - if cd "$T_DIR_WORK"; then - # shellcheck disable=2013 - # (globbing is desired) - for f in $(sort "$T_YADM_ENCRYPT"); do - tar rf "$T_TMP/build_archive.tar" "$f" - echo "$f" >> "$T_TMP/archived_files" - done - fi - ) - - #; encrypt YADM_ARCHIVE (symmetric) - expect </dev/null - set timeout 2; - spawn "$T_GPG_PROGRAM" --yes -c --output "$T_ARCHIVE_SYMMETRIC" "$T_TMP/build_archive.tar" - expect "passphrase:" {send "$T_PASSWD\n"} - expect "passphrase:" {send "$T_PASSWD\n"} - expect "$" - foreach {pid spawnid os_error_flag value} [wait] break -EOF - - #; encrypt YADM_ARCHIVE (asymmetric) - "$T_GPG_PROGRAM" --yes --batch -e -r "$T_KEY_NAME" --output "$T_ARCHIVE_ASYMMETRIC" "$T_TMP/build_archive.tar" - - #; configure yadm to use T_GPG_PROGRAM - git config --file="$T_YADM_CONFIG" yadm.gpg-program "$T_GPG_PROGRAM" -} - -teardown() { - remove_keys -} - -function validate_archive() { - #; inventory what's in the archive - if [ "$1" = "symmetric" ]; then - expect </dev/null - set timeout 2; - spawn bash -c "($T_GPG_PROGRAM -q -d '$T_YADM_ARCHIVE' || echo 1) | tar t | sort > $T_TMP/archive_list" - expect "passphrase:" {send "$T_PASSWD\n"} - expect "$" - foreach {pid spawnid os_error_flag value} [wait] break -EOF - else - "$T_GPG_PROGRAM" -q -d "$T_YADM_ARCHIVE" | tar t | sort > "$T_TMP/archive_list" - fi - - excluded="$2" - - #; inventory what is expected in the archive - ( - if cd "$T_DIR_WORK"; then - # shellcheck disable=2013 - # (globbing is desired) - while IFS='' read -r glob || [ -n "$glob" ]; do - if [[ ! $glob =~ ^# && ! $glob =~ ^[[:space:]]*$ ]] ; then - if [[ ! $glob =~ ^!(.+) ]] ; then - local IFS=$'\n' - for matching_file in $glob; do - if [ -e "$matching_file" ]; then - if [ "$matching_file" != "$excluded" ]; then - if [ -d "$matching_file" ]; then - echo "$matching_file/" - for subfile in "$matching_file"/*; do - echo "$subfile" - done - else - echo "$matching_file" - fi - fi - fi - done - fi - fi - done < "$T_YADM_ENCRYPT" | sort > "$T_TMP/expected_list" - fi - ) - - #; compare the archive vs expected - if ! cmp -s "$T_TMP/archive_list" "$T_TMP/expected_list"; then - echo "ERROR: Archive does not contain the correct files" - echo "Contains:" - cat "$T_TMP/archive_list" - echo "Expected:" - cat "$T_TMP/expected_list" - return 1 - fi - return 0 -} - -function validate_extraction() { - #; test each file which was archived - while IFS= read -r f; do - local contents - contents=$(cat "$T_DIR_WORK/$f") - if [ "$contents" != "$f" ]; then - echo "ERROR: Contents of $T_DIR_WORK/$f is incorrect" - return 1 - fi - done < "$T_TMP/archived_files" - return 0 -} - -@test "Command 'encrypt' (missing YADM_ENCRYPT)" { - echo " - When 'encrypt' command is provided, - and YADM_ENCRYPT does not exist - Report problem - Exit with 1 - " - - #; remove YADM_ENCRYPT - rm -f "$T_YADM_ENCRYPT" - - #; run encrypt - run "${T_YADM_Y[@]}" encrypt - - #; validate status and output - [ "$status" -eq 1 ] - [[ "$output" =~ does\ not\ exist ]] -} - -@test "Command 'encrypt' (mismatched password)" { - echo " - When 'encrypt' command is provided, - and YADM_ENCRYPT is present - and the provided passwords do not match - Report problem - Exit with 1 - " - - #; run encrypt - run expect <> "$T_YADM_ENCRYPT" - - #; run encrypt - run expect < "$T_YADM_ENCRYPT" - - #; validate the archive - validate_archive symmetric -} - -@test "Command 'encrypt' (empty lines and space lines in YADM_ENCRYPT)" { - echo " - When 'encrypt' command is provided, - and YADM_ENCRYPT is present - Create YADM_ARCHIVE - Report the archive created - Archive should be valid - Exit with 0 - " - - #; add empty lines to YADM_ARCHIVE - local original_encrypt - original_encrypt=$(cat "$T_YADM_ENCRYPT") - echo -e " \n\n \n" >> "$T_YADM_ENCRYPT" - - #; run encrypt - run expect < "$T_YADM_ENCRYPT" - - #; validate the archive - validate_archive symmetric -} - -@test "Command 'encrypt' (paths with spaces/globs in YADM_ENCRYPT)" { - echo " - When 'encrypt' command is provided, - and YADM_ENCRYPT is present - Create YADM_ARCHIVE - Report the archive created - Archive should be valid - Exit with 0 - " - - #; add paths with spaces to YADM_ARCHIVE - local original_encrypt - original_encrypt=$(cat "$T_YADM_ENCRYPT") - echo -e "space test/file*" >> "$T_YADM_ENCRYPT" - - #; run encrypt - run expect <> "$T_YADM_ENCRYPT" - echo -e "!.ssh/sec*.pub" >> "$T_YADM_ENCRYPT" - - #; run encrypt - run expect <> "$T_YADM_ENCRYPT" - - #; run encrypt - run expect < "$T_YADM_ARCHIVE" - - #; run encrypt - run expect <> "$T_DIR_WORK/$f" - done < "$T_TMP/archived_files" - - #; run decrypt - run expect < "$T_YADM_CONFIG" - - #; run encrypt - run "${T_YADM_Y[@]}" encrypt - - #; validate status and output - [ "$status" -eq 1 ] - [[ "$output" =~ public\ key\ not\ found ]] || [[ "$output" =~ No\ public\ key ]] - [[ "$output" =~ Unable\ to\ write ]] -} - - -@test "Command 'encrypt' (asymmetric)" { - echo " - When 'encrypt' command is provided, - and YADM_ENCRYPT is present - and yadm.gpg-recipient refers to a valid private key - Create YADM_ARCHIVE - Report the archive created - Archive should be valid - Exit with 0 - " - - #; manually set yadm.gpg-recipient in configuration - make_parents "$T_YADM_CONFIG" - echo -e "$T_RECIPIENT_GOOD" > "$T_YADM_CONFIG" - - #; run encrypt - run "${T_YADM_Y[@]}" encrypt - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ Wrote\ new\ file:.+$T_YADM_ARCHIVE ]] - - #; validate the archive - validate_archive asymmetric -} - -@test "Command 'encrypt' (asymmetric, overwrite)" { - echo " - When 'encrypt' command is provided, - and YADM_ENCRYPT is present - and yadm.gpg-recipient refers to a valid private key - and YADM_ARCHIVE already exists - Overwrite YADM_ARCHIVE - Report the archive created - Archive should be valid - Exit with 0 - " - - #; manually set yadm.gpg-recipient in configuration - make_parents "$T_YADM_CONFIG" - echo -e "$T_RECIPIENT_GOOD" > "$T_YADM_CONFIG" - - #; Explicitly create an invalid archive - echo "EXISTING ARCHIVE" > "$T_YADM_ARCHIVE" - - #; run encrypt - run "${T_YADM_Y[@]}" encrypt - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ Wrote\ new\ file:.+$T_YADM_ARCHIVE ]] - - #; validate the archive - validate_archive asymmetric -} - -@test "Command 'encrypt' (asymmetric, ask)" { - echo " - When 'encrypt' command is provided, - and YADM_ENCRYPT is present - and yadm.gpg-recipient is set to ASK - Ask for recipient - Create YADM_ARCHIVE - Report the archive created - Archive should be valid - Exit with 0 - " - - #; manually set yadm.gpg-recipient in configuration - make_parents "$T_YADM_CONFIG" - echo -e "$T_RECIPIENT_ASK" > "$T_YADM_CONFIG" - - #; run encrypt - run expect < "$T_YADM_CONFIG" - - #; run decrypt - run "${T_YADM_Y[@]}" decrypt - - #; validate status and output - [ "$status" -eq 1 ] - [[ "$output" =~ does\ not\ exist ]] -} - -@test "Command 'decrypt' (asymmetric, missing key)" { - echo " - When 'decrypt' command is provided, - and yadm.gpg-recipient refers to a valid private key - and YADM_ARCHIVE is present - and the private key is not present - Report problem - Exit with 1 - " - - #; manually set yadm.gpg-recipient in configuration - make_parents "$T_YADM_CONFIG" - echo -e "$T_RECIPIENT_GOOD" > "$T_YADM_CONFIG" - - #; use the asymmetric archive - cp -f "$T_ARCHIVE_ASYMMETRIC" "$T_YADM_ARCHIVE" - - #; remove the private key - remove_keys - - #; run decrypt - run "${T_YADM_Y[@]}" decrypt - - #; validate status and output - [ "$status" -eq 1 ] - [[ "$output" =~ decryption\ failed ]] - [[ "$output" =~ Unable\ to\ extract ]] -} - -@test "Command 'decrypt' -l (asymmetric, missing key)" { - echo " - When 'decrypt' command is provided, - and '-l' is provided, - and yadm.gpg-recipient refers to a valid private key - and YADM_ARCHIVE is present - and the private key is not present - Report problem - Exit with 1 - " - - #; manually set yadm.gpg-recipient in configuration - make_parents "$T_YADM_CONFIG" - echo -e "$T_RECIPIENT_GOOD" > "$T_YADM_CONFIG" - - #; use the asymmetric archive - cp -f "$T_ARCHIVE_ASYMMETRIC" "$T_YADM_ARCHIVE" - - #; remove the private key - remove_keys - - #; run decrypt - run "${T_YADM_Y[@]}" decrypt - - #; validate status and output - [ "$status" -eq 1 ] - [[ "$output" =~ decryption\ failed ]] - [[ "$output" =~ Unable\ to\ extract ]] -} - -@test "Command 'decrypt' (asymmetric)" { - echo " - When 'decrypt' command is provided, - and yadm.gpg-recipient refers to a valid private key - and YADM_ARCHIVE is present - Report the data created - Data should be valid - Exit with 0 - " - - #; manually set yadm.gpg-recipient in configuration - make_parents "$T_YADM_CONFIG" - echo -e "$T_RECIPIENT_GOOD" > "$T_YADM_CONFIG" - - #; use the asymmetric archive - cp -f "$T_ARCHIVE_ASYMMETRIC" "$T_YADM_ARCHIVE" - - #; empty the worktree - rm -rf "$T_DIR_WORK" - mkdir -p "$T_DIR_WORK" - - #; run decrypt - run "${T_YADM_Y[@]}" decrypt - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ All\ files\ decrypted ]] - - #; validate the extracted files - validate_extraction -} - -@test "Command 'decrypt' (asymmetric, overwrite)" { - echo " - When 'decrypt' command is provided, - and yadm.gpg-recipient refers to a valid private key - and YADM_ARCHIVE is present - and archived content already exists - Report the data overwritten - Data should be valid - Exit with 0 - " - - #; manually set yadm.gpg-recipient in configuration - make_parents "$T_YADM_CONFIG" - echo -e "$T_RECIPIENT_GOOD" > "$T_YADM_CONFIG" - - #; use the asymmetric archive - cp -f "$T_ARCHIVE_ASYMMETRIC" "$T_YADM_ARCHIVE" - - #; alter the values of the archived files - while IFS= read -r f; do - echo "changed" >> "$T_DIR_WORK/$f" - done < "$T_TMP/archived_files" - - #; run decrypt - run "${T_YADM_Y[@]}" decrypt - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ All\ files\ decrypted ]] - - #; validate the extracted files - validate_extraction -} - -@test "Command 'decrypt' -l (asymmetric)" { - echo " - When 'decrypt' command is provided, - and '-l' is provided, - and yadm.gpg-recipient refers to a valid private key - and YADM_ARCHIVE is present - Report the contents of YADM_ARCHIVE - Exit with 0 - " - - #; manually set yadm.gpg-recipient in configuration - make_parents "$T_YADM_CONFIG" - echo -e "$T_RECIPIENT_GOOD" > "$T_YADM_CONFIG" - - #; use the asymmetric archive - cp -f "$T_ARCHIVE_ASYMMETRIC" "$T_YADM_ARCHIVE" - - #; run decrypt - run "${T_YADM_Y[@]}" decrypt -l - - #; validate status - [ "$status" -eq 0 ] - - #; validate every file is listed in output - while IFS= read -r f; do - if [[ ! "$output" =~ $f ]]; then - echo "ERROR: Did not find '$f' in output" - return 1 - fi - done < "$T_TMP/archived_files" - -} diff --git a/test/110_accept_perms.bats b/test/110_accept_perms.bats deleted file mode 100644 index a68b1a5..0000000 --- a/test/110_accept_perms.bats +++ /dev/null @@ -1,173 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -setup() { - destroy_tmp - build_repo -} - -function is_restricted() { - local p - for p in "${restricted[@]}"; do [ "$p" = "$1" ] && return 0; done - return 1 -} - -function validate_perms() { - local perms="$*" - - #; determine which paths should have restricted permissions - restricted=() - local p - for p in $perms; do - case $p in - ssh) - restricted=("${restricted[@]}" $T_DIR_WORK/.ssh $T_DIR_WORK/.ssh/*) - ;; - gpg) - restricted=("${restricted[@]}" $T_DIR_WORK/.gnupg $T_DIR_WORK/.gnupg/*) - ;; - *) - restricted=("${restricted[@]}" $T_DIR_WORK/$p) - ;; - esac - done - - #; validate permissions of each path in the worktere - local testpath - while IFS= read -r -d '' testpath; do - local perm_regex="....rwxrwx" - if is_restricted "$testpath"; then - perm_regex="....------" - fi - test_perms "$testpath" "$perm_regex" || return 1 - done < <(find "$T_DIR_WORK" -print0) - -} - -@test "Command 'perms'" { - echo " - When the command 'perms' is provided - Update permissions for ssh/gpg - Verify correct permissions - Exit with 0 - " - - #; run perms - run "${T_YADM_Y[@]}" perms - - #; validate status and output - [ "$status" -eq 0 ] - [ "$output" = "" ] - - #; validate permissions - validate_perms ssh gpg -} - -@test "Command 'perms' (with encrypt)" { - echo " - When the command 'perms' is provided - And YADM_ENCRYPT is present - Update permissions for ssh/gpg/encrypt - Support comments in YADM_ENCRYPT - Verify correct permissions - Exit with 0 - " - - #; this version has a comment in it - echo -e "#.vimrc\n.tmux.conf\n.hammerspoon/*\n!.tmux.conf" > "$T_YADM_ENCRYPT" - - #; run perms - run "${T_YADM_Y[@]}" perms - - #; validate status and output - [ "$status" -eq 0 ] - [ "$output" = "" ] - - #; validate permissions - validate_perms ssh gpg ".hammerspoon/*" -} - -@test "Command 'perms' (ssh-perms=false)" { - echo " - When the command 'perms' is provided - And yadm.ssh-perms=false - Update permissions for gpg only - Verify correct permissions - Exit with 0 - " - - #; configure yadm.ssh-perms - git config --file="$T_YADM_CONFIG" "yadm.ssh-perms" "false" - - #; run perms - run "${T_YADM_Y[@]}" perms - - #; validate status and output - [ "$status" -eq 0 ] - [ "$output" = "" ] - - #; validate permissions - validate_perms gpg -} - -@test "Command 'perms' (gpg-perms=false)" { - echo " - When the command 'perms' is provided - And yadm.gpg-perms=false - Update permissions for ssh only - Verify correct permissions - Exit with 0 - " - - #; configure yadm.gpg-perms - git config --file="$T_YADM_CONFIG" "yadm.gpg-perms" "false" - - #; run perms - run "${T_YADM_Y[@]}" perms - - #; validate status and output - [ "$status" -eq 0 ] - [ "$output" = "" ] - - #; validate permissions - validate_perms ssh -} - -@test "Command 'auto-perms' (enabled)" { - echo " - When a command possibly changes the repo - Update permissions for ssh/gpg - Verify correct permissions - " - - #; run status - run "${T_YADM_Y[@]}" status - - #; validate status - [ "$status" -eq 0 ] - - #; validate permissions - validate_perms ssh gpg -} - -@test "Command 'auto-perms' (disabled)" { - echo " - When a command possibly changes the repo - And yadm.auto-perms=false - Take no action - Verify permissions are intact - " - - #; configure yadm.auto-perms - git config --file="$T_YADM_CONFIG" "yadm.auto-perms" "false" - - #; run status - run "${T_YADM_Y[@]}" status - - #; validate status - [ "$status" -eq 0 ] - - #; validate permissions - validate_perms -} diff --git a/test/111_accept_wildcard_alt.bats b/test/111_accept_wildcard_alt.bats deleted file mode 100644 index dcae81f..0000000 --- a/test/111_accept_wildcard_alt.bats +++ /dev/null @@ -1,223 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -IN_REPO=(wild*) -export TEST_TREE_WITH_WILD=1 - -setup() { - destroy_tmp - build_repo "${IN_REPO[@]}" -} - -function test_alt() { - local link_name="$1" - local link_match="$2" - - #; run yadm alt - run "${T_YADM_Y[@]}" alt - #; validate status and output - if [ "$status" != 0 ] || [[ ! "$output" =~ Linking.+$link_name ]]; then - echo "OUTPUT:$output" - echo "ERROR: Could not confirm status and output of alt command" - return 1; - fi - - #; correct link should be present - local link_content - link_content=$(cat "$T_DIR_WORK/$link_name") - if [ "$link_content" != "$link_match" ]; then - echo "OUTPUT:$output" - echo "ERROR: Link content is not correct" - return 1 - fi -} - -@test "Command 'alt' (wild none)" { - echo " - When the command 'alt' is provided - and file matches only ## - Report the linking - Verify correct file is linked - Exit with 0 - " - - test_alt 'wild-none' 'wild-none##' -} - -@test "Command 'alt' (wild system)" { - echo " - When the command 'alt' is provided - and file matches only ##SYSTEM - with possible wildcards - Report the linking - Verify correct file is linked - Exit with 0 - " - - for WILD_S in 'local' 'wild'; do - local s_base="wild-system-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - local match="${s_base}##${WILD_S}" - echo test_alt "$s_base" "$match" - test_alt "$s_base" "$match" - done -} - -@test "Command 'alt' (wild class)" { - echo " - When the command 'alt' is provided - and file matches only ##CLASS - with possible wildcards - Report the linking - Verify correct file is linked - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.class set_class - - for WILD_C in 'local' 'wild'; do - local c_base="wild-class-$WILD_C" - case $WILD_C in local) WILD_C="set_class";; wild) WILD_C="%";; esac - local match="${c_base}##${WILD_C}" - echo test_alt "$c_base" "$match" - test_alt "$c_base" "$match" - done -} - -@test "Command 'alt' (wild host)" { - echo " - When the command 'alt' is provided - and file matches only ##SYSTEM.HOST - with possible wildcards - Report the linking - Verify correct file is linked - Exit with 0 - " - - for WILD_S in 'local' 'wild'; do - local s_base="wild-host-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - for WILD_H in 'local' 'wild'; do - local h_base="${s_base}-$WILD_H" - case $WILD_H in local) WILD_H="$T_HOST";; wild) WILD_H="%";; esac - local match="${h_base}##${WILD_S}.${WILD_H}" - echo test_alt "$h_base" "$match" - test_alt "$h_base" "$match" - done - done -} - -@test "Command 'alt' (wild class-system)" { - echo " - When the command 'alt' is provided - and file matches only ##CLASS.SYSTEM - with possible wildcards - Report the linking - Verify correct file is linked - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.class set_class - - for WILD_C in 'local' 'wild'; do - local c_base="wild-class-system-$WILD_C" - case $WILD_C in local) WILD_C="set_class";; wild) WILD_C="%";; esac - for WILD_S in 'local' 'wild'; do - local s_base="${c_base}-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - local match="${s_base}##${WILD_C}.${WILD_S}" - echo test_alt "$s_base" "$match" - test_alt "$s_base" "$match" - done - done -} - -@test "Command 'alt' (wild user)" { - echo " - When the command 'alt' is provided - and file matches only ##SYSTEM.HOST.USER - with possible wildcards - Report the linking - Verify correct file is linked - Exit with 0 - " - - for WILD_S in 'local' 'wild'; do - local s_base="wild-user-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - for WILD_H in 'local' 'wild'; do - local h_base="${s_base}-$WILD_H" - case $WILD_H in local) WILD_H="$T_HOST";; wild) WILD_H="%";; esac - for WILD_U in 'local' 'wild'; do - local u_base="${h_base}-$WILD_U" - case $WILD_U in local) WILD_U="$T_USER";; wild) WILD_U="%";; esac - local match="${u_base}##${WILD_S}.${WILD_H}.${WILD_U}" - echo test_alt "$u_base" "$match" - test_alt "$u_base" "$match" - done - done - done -} - -@test "Command 'alt' (wild class-system-host)" { - echo " - When the command 'alt' is provided - and file matches only ##CLASS.SYSTEM.HOST - with possible wildcards - Report the linking - Verify correct file is linked - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.class set_class - - for WILD_C in 'local' 'wild'; do - local c_base="wild-class-system-host-$WILD_C" - case $WILD_C in local) WILD_C="set_class";; wild) WILD_C="%";; esac - for WILD_S in 'local' 'wild'; do - local s_base="${c_base}-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - for WILD_H in 'local' 'wild'; do - local h_base="${s_base}-$WILD_H" - case $WILD_H in local) WILD_H="$T_HOST";; wild) WILD_H="%";; esac - local match="${h_base}##${WILD_C}.${WILD_S}.${WILD_H}" - echo test_alt "$h_base" "$match" - test_alt "$h_base" "$match" - done - done - done -} - -@test "Command 'alt' (wild class-system-host-user)" { - echo " - When the command 'alt' is provided - and file matches only ##CLASS.SYSTEM.HOST.USER - with possible wildcards - Report the linking - Verify correct file is linked - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.class set_class - - for WILD_C in 'local' 'wild'; do - local c_base="wild-class-system-host-user-$WILD_C" - case $WILD_C in local) WILD_C="set_class";; wild) WILD_C="%";; esac - for WILD_S in 'local' 'wild'; do - local s_base="${c_base}-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - for WILD_H in 'local' 'wild'; do - local h_base="${s_base}-$WILD_H" - case $WILD_H in local) WILD_H="$T_HOST";; wild) WILD_H="%";; esac - for WILD_U in 'local' 'wild'; do - local u_base="${h_base}-$WILD_U" - case $WILD_U in local) WILD_U="$T_USER";; wild) WILD_U="%";; esac - local match="${u_base}##${WILD_C}.${WILD_S}.${WILD_H}.${WILD_U}" - echo test_alt "$u_base" "$match" - test_alt "$u_base" "$match" - done - done - done - done -} diff --git a/test/112_accept_bootstrap.bats b/test/112_accept_bootstrap.bats deleted file mode 100644 index 3f23df2..0000000 --- a/test/112_accept_bootstrap.bats +++ /dev/null @@ -1,78 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -setup() { - destroy_tmp - build_repo -} - -@test "Command 'bootstrap' (missing file)" { - echo " - When 'bootstrap' command is provided, - and the bootstrap file is missing - Report error - Exit with 1 - " - - #; run clone - run "${T_YADM_Y[@]}" bootstrap - echo "STATUS:$status" - echo "OUTPUT:$output" - - #; validate status and output - [[ "$output" =~ Cannot\ execute\ bootstrap ]] - [ "$status" -eq 1 ] - -} - -@test "Command 'bootstrap' (not executable)" { - echo " - When 'bootstrap' command is provided, - and the bootstrap file is present - but is not executable - Report error - Exit with 1 - " - - touch "$T_YADM_BOOTSTRAP" - - #; run clone - run "${T_YADM_Y[@]}" bootstrap - echo "STATUS:$status" - echo "OUTPUT:$output" - - #; validate status and output - [[ "$output" =~ is\ not\ an\ executable\ program ]] - [ "$status" -eq 1 ] - -} - -@test "Command 'bootstrap' (bootstrap run)" { - echo " - When 'bootstrap' command is provided, - and the bootstrap file is present - and is executable - Announce the execution - Execute bootstrap - Exit with the exit code of bootstrap - " - - { - echo "#!/bin/bash" - echo "echo Bootstrap successful" - echo "exit 123" - } > "$T_YADM_BOOTSTRAP" - chmod a+x "$T_YADM_BOOTSTRAP" - - #; run clone - run "${T_YADM_Y[@]}" bootstrap - echo "STATUS:$status" - echo "OUTPUT:$output" - - #; validate status and output - [[ "$output" =~ Executing\ $T_YADM_BOOTSTRAP ]] - [[ "$output" =~ Bootstrap\ successful ]] - [ "$status" -eq 123 ] - -} diff --git a/test/113_accept_jinja_alt.bats b/test/113_accept_jinja_alt.bats deleted file mode 100644 index 0f73af9..0000000 --- a/test/113_accept_jinja_alt.bats +++ /dev/null @@ -1,203 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -IN_REPO=(alt* "dir one") -export TEST_TREE_WITH_ALT=1 - - -setup() { - destroy_tmp - build_repo "${IN_REPO[@]}" - echo "excluded-encrypt##yadm.j2" > "$T_YADM_ENCRYPT" - echo "included-encrypt##yadm.j2" >> "$T_YADM_ENCRYPT" - echo "!excluded-encrypt*" >> "$T_YADM_ENCRYPT" - echo "included-encrypt" > "$T_DIR_WORK/included-encrypt##yadm.j2" - echo "excluded-encrypt" > "$T_DIR_WORK/excluded-encrypt##yadm.j2" -} - - -function test_alt() { - local alt_type="$1" - local test_overwrite="$2" - local auto_alt="$3" - - #; detemine test parameters - case $alt_type in - base) - real_name="alt-jinja" - file_content_match="-${T_SYS}-${T_HOST}-${T_USER}-${T_DISTRO}" - ;; - override_all) - real_name="alt-jinja" - file_content_match="custom_class-custom_system-custom_host-custom_user-${T_DISTRO}" - ;; - encrypt) - real_name="included-encrypt" - file_content_match="included-encrypt" - missing_name="excluded-encrypt" - ;; - esac - - if [ "$test_overwrite" = "true" ] ; then - #; create incorrect links (to overwrite) - echo "BAD_CONTENT" "$T_DIR_WORK/$real_name" - else - #; verify real file doesn't already exist - if [ -e "$T_DIR_WORK/$real_name" ] ; then - echo "ERROR: real file already exists before running yadm" - return 1 - fi - fi - - #; configure yadm.auto_alt=false - if [ "$auto_alt" = "false" ]; then - git config --file="$T_YADM_CONFIG" yadm.auto-alt false - fi - - #; run yadm (alt or status) - if [ -z "$auto_alt" ]; then - run "${T_YADM_Y[@]}" alt - #; validate status and output - if [ "$status" != 0 ] || [[ ! "$output" =~ Creating.+$real_name ]]; then - echo "OUTPUT:$output" - echo "ERROR: Could not confirm status and output of alt command" - return 1; - fi - else - #; running any passed through Git command should trigger auto-alt - run "${T_YADM_Y[@]}" status - if [ -n "$auto_alt" ] && [[ "$output" =~ Creating.+$real_name ]]; then - echo "ERROR: Reporting of jinja processing should not happen" - return 1 - fi - fi - - if [ -n "$missing_name" ] && [ -f "$T_DIR_WORK/$missing_name" ]; then - echo "ERROR: File should not have been created '$missing_name'" - return 1 - fi - - #; validate link content - if [[ "$alt_type" =~ none ]] || [ "$auto_alt" = "false" ]; then - #; no real file should be present - if [ -L "$T_DIR_WORK/$real_name" ] ; then - echo "ERROR: Real file should not exist" - return 1 - fi - else - #; correct real file should be present - local file_content - file_content=$(cat "$T_DIR_WORK/$real_name") - if [ "$file_content" != "$file_content_match" ]; then - echo "file_content: ${file_content}" - echo "expected_content: ${file_content_match}" - echo "ERROR: Link content is not correct" - return 1 - fi - fi -} - -@test "Command 'alt' (envtpl missing)" { - echo " - When the command 'alt' is provided - and file matches ##yadm.j2 - Report jinja template as unprocessed - Exit with 0 - " - - # shellcheck source=/dev/null - YADM_TEST=1 source "$T_YADM" - process_global_args -Y "$T_DIR_YADM" - set_operating_system - configure_paths - - status=0 - output=$( ENVTPL_PROGRAM='envtpl_missing' main alt ) || { - status=$? - true - } - - [ $status -eq 0 ] - [[ "$output" =~ envtpl.not.available ]] -} - -@test "Command 'alt' (select jinja)" { - echo " - When the command 'alt' is provided - and file matches ##yadm.j2 - Report jinja template processing - Verify that the correct content is written - Exit with 0 - " - - test_alt 'base' 'false' '' -} - -@test "Command 'auto-alt' (enabled)" { - echo " - When a command possibly changes the repo - and auto-alt is configured true - and file matches ##yadm.j2 - automatically process alternates - report no linking (not loud) - Verify that the correct content is written - " - - test_alt 'base' 'false' 'true' -} - -@test "Command 'auto-alt' (disabled)" { - echo " - When a command possibly changes the repo - and auto-alt is configured false - and file matches ##yadm.j2 - Report no jinja template processing - Verify no content - " - - test_alt 'base' 'false' 'false' -} - -@test "Command 'alt' (overwrite existing content)" { - echo " - When the command 'alt' is provided - and file matches ##yadm.j2 - and the real file exists, and is wrong - Report jinja template processing - Verify that the correct content is written - Exit with 0 - " - - test_alt 'base' 'true' '' -} - -@test "Command 'alt' (overwritten settings)" { - echo " - When the command 'alt' is provided - and file matches ##yadm.j2 - after setting local.* - Report jinja template processing - Verify that the correct content is written - Exit with 0 - " - - GIT_DIR="$T_DIR_REPO" git config local.os custom_system - GIT_DIR="$T_DIR_REPO" git config local.user custom_user - GIT_DIR="$T_DIR_REPO" git config local.hostname custom_host - GIT_DIR="$T_DIR_REPO" git config local.class custom_class - test_alt 'override_all' 'false' '' -} - -@test "Command 'alt' (select jinja within .yadm/encrypt)" { - echo " - When the command 'alt' is provided - and file matches ##yadm.j2 within .yadm/encrypt - and file excluded within .yadm/encrypt - Report jinja template processing - Verify that the correct content is written - Exit with 0 - " - - test_alt 'encrypt' 'false' '' -} diff --git a/test/114_accept_enter.bats b/test/114_accept_enter.bats deleted file mode 100644 index b13dc35..0000000 --- a/test/114_accept_enter.bats +++ /dev/null @@ -1,66 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -setup() { - build_repo -} - -@test "Command 'enter' (SHELL not set)" { - echo " - When 'enter' command is provided, - And SHELL is not set - Report error - Exit with 1 - " - - SHELL= - export SHELL - run "${T_YADM_Y[@]}" enter - - #; validate status and output - [ $status -eq 1 ] - [[ "$output" =~ does.not.refer.to.an.executable ]] -} - -@test "Command 'enter' (SHELL not executable)" { - echo " - When 'enter' command is provided, - And SHELL is not executable - Report error - Exit with 1 - " - - touch "$T_TMP/badshell" - SHELL="$T_TMP/badshell" - export SHELL - run "${T_YADM_Y[@]}" enter - - #; validate status and output - [ $status -eq 1 ] - [[ "$output" =~ does.not.refer.to.an.executable ]] -} - -@test "Command 'enter' (SHELL executable)" { - echo " - When 'enter' command is provided, - And SHELL is set - Execute SHELL command - Expose GIT variables - Set prompt variables - Announce entering/leaving shell - Exit with 0 - " - - SHELL=$(command -v env) - export SHELL - run "${T_YADM_Y[@]}" enter - - #; validate status and output - [ $status -eq 0 ] - [[ "$output" =~ GIT_DIR= ]] - [[ "$output" =~ PROMPT=yadm.shell ]] - [[ "$output" =~ PS1=yadm.shell ]] - [[ "$output" =~ Entering.yadm.repo ]] - [[ "$output" =~ Leaving.yadm.repo ]] -} diff --git a/test/115_accept_introspect.bats b/test/115_accept_introspect.bats deleted file mode 100644 index 56131f2..0000000 --- a/test/115_accept_introspect.bats +++ /dev/null @@ -1,99 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -function count_introspect() { - local category="$1" - local expected_status="$2" - local expected_words="$3" - local expected_regex="$4" - - run "${T_YADM_Y[@]}" introspect "$category" - local output_words - output_words=$(wc -w <<< "$output") - - if [ "$status" -ne "$expected_status" ]; then - echo "ERROR: Unexpected exit code (expected $expected_status, got $status)" - return 1; - fi - - if [ "$output_words" -ne "$expected_words" ]; then - echo "ERROR: Unexpected number of output words (expected $expected_words, got $output_words)" - return 1; - fi - - if [ -n "$expected_regex" ]; then - if [[ ! "$output" =~ $expected_regex ]]; then - echo "OUTPUT:$output" - echo "ERROR: Output does not match regex: $expected_regex" - return 1; - fi - fi - -} - -@test "Command 'introspect' (no category)" { - echo " - When 'introspect' command is provided, - And no category is provided - Produce no output - Exit with 0 - " - - count_introspect "" 0 0 -} - -@test "Command 'introspect' (invalid category)" { - echo " - When 'introspect' command is provided, - And an invalid category is provided - Produce no output - Exit with 0 - " - - count_introspect "invalid_cat" 0 0 -} - -@test "Command 'introspect' (commands)" { - echo " - When 'introspect' command is provided, - And category 'commands' is provided - Produce command list - Exit with 0 - " - - count_introspect "commands" 0 15 'version' -} - -@test "Command 'introspect' (configs)" { - echo " - When 'introspect' command is provided, - And category 'configs' is provided - Produce switch list - Exit with 0 - " - - count_introspect "configs" 0 13 'yadm\.auto-alt' -} - -@test "Command 'introspect' (repo)" { - echo " - When 'introspect' command is provided, - And category 'repo' is provided - Output repo - Exit with 0 - " - - count_introspect "repo" 0 1 "$T_DIR_REPO" -} - -@test "Command 'introspect' (switches)" { - echo " - When 'introspect' command is provided, - And category 'switches' is provided - Produce switch list - Exit with 0 - " - - count_introspect "switches" 0 7 '--yadm-dir' -} diff --git a/test/116_accept_cygwin_copy.bats b/test/116_accept_cygwin_copy.bats deleted file mode 100644 index 8d1ff04..0000000 --- a/test/116_accept_cygwin_copy.bats +++ /dev/null @@ -1,131 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -IN_REPO=(alt*) -export TEST_TREE_WITH_CYGWIN=1 -export SIMULATED_CYGWIN="CYGWIN_NT-6.1-WOW64" - -setup() { - destroy_tmp - build_repo "${IN_REPO[@]}" -} - -test_alt() { - local cygwin_copy="$1" - local is_cygwin="$2" - local expect_link="$3" - local preexisting_link="$4" - - case "$cygwin_copy" in - true|false) - git config --file="$T_YADM_CONFIG" "yadm.cygwin-copy" "$cygwin_copy" - ;; - esac - - if [ "$is_cygwin" = "true" ]; then - echo '#!/bin/sh' > "$T_TMP/uname" - echo "echo $SIMULATED_CYGWIN" >> "$T_TMP/uname" - chmod a+x "$T_TMP/uname" - fi - - local expected_content - expected_content="$T_DIR_WORK/alt-test##$(PATH="$T_TMP:$PATH" uname -s)" - - if [ "$preexisting_link" = 'symlink' ]; then - ln -s "$expected_content" "$T_DIR_WORK/alt-test" - elif [ "$preexisting_link" = 'file' ]; then - touch "$T_DIR_WORK/alt-test" - fi - - PATH="$T_TMP:$PATH" run "${T_YADM_Y[@]}" alt - - echo "Alt output:$output" - echo "Alt status:$status" - - if [ -L "$T_DIR_WORK/alt-test" ] && [ "$expect_link" != 'true' ] ; then - echo "ERROR: Alt should be a simple file, but isn't" - return 1 - fi - if [ ! -L "$T_DIR_WORK/alt-test" ] && [ "$expect_link" = 'true' ] ; then - echo "ERROR: Alt should use symlink, but doesn't" - return 1 - fi - - if ! diff "$T_DIR_WORK/alt-test" "$expected_content"; then - echo "ERROR: Alt contains different data than expected" - return 1 - fi -} - -@test "Option 'yadm.cygwin-copy' (unset, non-cygwin)" { - echo " - When the option 'yadm.cygwin-copy' is unset - and the OS is not CYGWIN - Verify alternate is a symlink - " - test_alt 'unset' 'false' 'true' -} - -@test "Option 'yadm.cygwin-copy' (true, non-cygwin)" { - echo " - When the option 'yadm.cygwin-copy' is true - and the OS is not CYGWIN - Verify alternate is a symlink - " - test_alt 'true' 'false' 'true' -} - -@test "Option 'yadm.cygwin-copy' (false, non-cygwin)" { - echo " - When the option 'yadm.cygwin-copy' is false - and the OS is not CYGWIN - Verify alternate is a symlink - " - test_alt 'false' 'false' 'true' -} - -@test "Option 'yadm.cygwin-copy' (unset, cygwin)" { - echo " - When the option 'yadm.cygwin-copy' is unset - and the OS is CYGWIN - Verify alternate is a symlink - " - test_alt 'unset' 'true' 'true' -} - -@test "Option 'yadm.cygwin-copy' (true, cygwin)" { - echo " - When the option 'yadm.cygwin-copy' is true - and the OS is CYGWIN - Verify alternate is a copy - " - test_alt 'true' 'true' 'false' -} - -@test "Option 'yadm.cygwin-copy' (false, cygwin)" { - echo " - When the option 'yadm.cygwin-copy' is false - and the OS is CYGWIN - Verify alternate is a symlink - " - test_alt 'false' 'true' 'true' -} - -@test "Option 'yadm.cygwin-copy' (preexisting symlink) " { - echo " - When the option 'yadm.cygwin-copy' is true - and the OS is CYGWIN - Verify alternate is a copy - " - test_alt 'true' 'true' 'false' 'symlink' -} - -@test "Option 'yadm.cygwin-copy' (preexisting file) " { - echo " - When the option 'yadm.cygwin-copy' is true - and the OS is CYGWIN - Verify alternate is a copy - " - test_alt 'true' 'true' 'false' 'file' -} diff --git a/test/117_accept_hooks.bats b/test/117_accept_hooks.bats deleted file mode 100644 index 5e8f348..0000000 --- a/test/117_accept_hooks.bats +++ /dev/null @@ -1,181 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -version_regex="yadm [[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+" - -setup() { - destroy_tmp - build_repo - mkdir -p "$T_DIR_HOOKS" -} - -function create_hook() { - hook_name="$1" - hook_exit="$2" - hook_file="$T_DIR_HOOKS/$hook_name" - { - echo "#!/bin/sh" - echo "echo ran $hook_name" - echo "env" - echo "exit $hook_exit" - } > "$hook_file" - chmod a+x "$hook_file" -} - -@test "Hooks (no hook)" { - echo " - When no hook is present - do no not run the hook - run command - Exit with 0 - " - - #; run yadm with no command - run "${T_YADM_Y[@]}" version - - [ $status -eq 0 ] - [[ "$output" =~ $version_regex ]] -} - -@test "Hooks (successful pre hook)" { - echo " - When hook is present - run hook - run command - Exit with 0 - " - - create_hook "pre_version" "0" - - #; run yadm with no command - run "${T_YADM_Y[@]}" version - - [ $status -eq 0 ] - [[ "$output" =~ ran\ pre_version ]] - [[ "$output" =~ $version_regex ]] -} - -@test "Hooks (unsuccessful pre hook)" { - echo " - When hook is present - run hook - report hook failure - do no not run command - Exit with 13 - " - - create_hook "pre_version" "13" - - #; run yadm with no command - run "${T_YADM_Y[@]}" version - - [ $status -eq 13 ] - [[ "$output" =~ ran\ pre_version ]] - [[ "$output" =~ pre_version\ was\ not\ successful ]] - [[ ! "$output" =~ $version_regex ]] -} - -@test "Hooks (successful post hook)" { - echo " - When hook is present - run command - run hook - Exit with 0 - " - - create_hook "post_version" "0" - - #; run yadm with no command - run "${T_YADM_Y[@]}" version - - [ $status -eq 0 ] - [[ "$output" =~ $version_regex ]] - [[ "$output" =~ ran\ post_version ]] -} - -@test "Hooks (unsuccessful post hook)" { - echo " - When hook is present - run command - run hook - Exit with 0 - " - - create_hook "post_version" "13" - - #; run yadm with no command - run "${T_YADM_Y[@]}" version - - [ $status -eq 0 ] - [[ "$output" =~ $version_regex ]] - [[ "$output" =~ ran\ post_version ]] -} - -@test "Hooks (successful pre hook + post hook)" { - echo " - When hook is present - run hook - run command - run hook - Exit with 0 - " - - create_hook "pre_version" "0" - create_hook "post_version" "0" - - #; run yadm with no command - run "${T_YADM_Y[@]}" version - - [ $status -eq 0 ] - [[ "$output" =~ ran\ pre_version ]] - [[ "$output" =~ $version_regex ]] - [[ "$output" =~ ran\ post_version ]] -} - -@test "Hooks (unsuccessful pre hook + post hook)" { - echo " - When hook is present - run hook - report hook failure - do no not run command - do no not run post hook - Exit with 13 - " - - create_hook "pre_version" "13" - create_hook "post_version" "0" - - #; run yadm with no command - run "${T_YADM_Y[@]}" version - - [ $status -eq 13 ] - [[ "$output" =~ ran\ pre_version ]] - [[ "$output" =~ pre_version\ was\ not\ successful ]] - [[ ! "$output" =~ $version_regex ]] - [[ ! "$output" =~ ran\ post_version ]] -} - -@test "Hooks (environment variables)" { - echo " - When hook is present - run command - run hook - hook should have access to environment variables - Exit with 0 - " - - create_hook "post_version" "0" - - #; run yadm with no command - run "${T_YADM_Y[@]}" version extra_args - - [ $status -eq 0 ] - [[ "$output" =~ $version_regex ]] - [[ "$output" =~ ran\ post_version ]] - [[ "$output" =~ YADM_HOOK_COMMAND=version ]] - [[ "$output" =~ YADM_HOOK_EXIT=0 ]] - [[ "$output" =~ YADM_HOOK_FULL_COMMAND=version\ extra_args ]] - [[ "$output" =~ YADM_HOOK_REPO=${T_DIR_REPO} ]] - [[ "$output" =~ YADM_HOOK_WORK=${T_DIR_WORK} ]] -} diff --git a/test/118_accept_assert_private_dirs.bats b/test/118_accept_assert_private_dirs.bats deleted file mode 100644 index 151a2e0..0000000 --- a/test/118_accept_assert_private_dirs.bats +++ /dev/null @@ -1,102 +0,0 @@ -load common -load_fixtures -status=;output=; #; populated by bats run() - -IN_REPO=(.bash_profile .vimrc) - -setup() { - destroy_tmp - build_repo "${IN_REPO[@]}" - rm -rf "$T_DIR_WORK" - mkdir -p "$T_DIR_WORK" -} - -@test "Private dirs (private dirs missing)" { - echo " - When a git command is run - And private directories are missing - Create private directories prior to command - " - - #; confirm directories are missing at start - [ ! -e "$T_DIR_WORK/.gnupg" ] - [ ! -e "$T_DIR_WORK/.ssh" ] - - #; run status - export DEBUG=yes - run "${T_YADM_Y[@]}" status - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ On\ branch\ master ]] - - #; confirm private directories are created - [ -d "$T_DIR_WORK/.gnupg" ] - test_perms "$T_DIR_WORK/.gnupg" "drwx------" - [ -d "$T_DIR_WORK/.ssh" ] - test_perms "$T_DIR_WORK/.ssh" "drwx------" - - #; confirm directories are created before command is run - [[ "$output" =~ Creating.+/.gnupg/.+Creating.+/.ssh/.+Running\ git\ command\ git\ status ]] -} - -@test "Private dirs (private dirs missing / yadm.auto-private-dirs=false)" { - echo " - When a git command is run - And private directories are missing - But auto-private-dirs is false - Do not create private dirs - " - - #; confirm directories are missing at start - [ ! -e "$T_DIR_WORK/.gnupg" ] - [ ! -e "$T_DIR_WORK/.ssh" ] - - #; set configuration - run "${T_YADM_Y[@]}" config --bool "yadm.auto-private-dirs" "false" - - #; run status - run "${T_YADM_Y[@]}" status - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ On\ branch\ master ]] - - #; confirm private directories are not created - [ ! -e "$T_DIR_WORK/.gnupg" ] - [ ! -e "$T_DIR_WORK/.ssh" ] -} - -@test "Private dirs (private dirs exist / yadm.auto-perms=false)" { - echo " - When a git command is run - And private directories exist - And yadm is configured not to auto update perms - Do not alter directories - " - - #shellcheck disable=SC2174 - mkdir -m 0777 -p "$T_DIR_WORK/.gnupg" "$T_DIR_WORK/.ssh" - - #; confirm directories are preset and open - [ -d "$T_DIR_WORK/.gnupg" ] - test_perms "$T_DIR_WORK/.gnupg" "drwxrwxrwx" - [ -d "$T_DIR_WORK/.ssh" ] - test_perms "$T_DIR_WORK/.ssh" "drwxrwxrwx" - - #; set configuration - run "${T_YADM_Y[@]}" config --bool "yadm.auto-perms" "false" - - #; run status - run "${T_YADM_Y[@]}" status - - #; validate status and output - [ "$status" -eq 0 ] - [[ "$output" =~ On\ branch\ master ]] - - #; confirm directories are still preset and open - [ -d "$T_DIR_WORK/.gnupg" ] - test_perms "$T_DIR_WORK/.gnupg" "drwxrwxrwx" - [ -d "$T_DIR_WORK/.ssh" ] - test_perms "$T_DIR_WORK/.ssh" "drwxrwxrwx" -} diff --git a/test/common.bash b/test/common.bash deleted file mode 100644 index 32ba8e1..0000000 --- a/test/common.bash +++ /dev/null @@ -1,384 +0,0 @@ - -#; common fixtures -function load_fixtures() { - export DEFAULT_YADM_DIR="$HOME/.yadm" - export DEFAULT_REPO="repo.git" - export DEFAULT_CONFIG="config" - export DEFAULT_ENCRYPT="encrypt" - export DEFAULT_ARCHIVE="files.gpg" - export DEFAULT_BOOTSTRAP="bootstrap" - - export T_YADM="$PWD/yadm" - export T_TMP="$BATS_TMPDIR/ytmp" - export T_DIR_YADM="$T_TMP/.yadm" - export T_DIR_WORK="$T_TMP/yadm-work" - export T_DIR_REPO="$T_DIR_YADM/repo.git" - export T_DIR_HOOKS="$T_DIR_YADM/hooks" - export T_YADM_CONFIG="$T_DIR_YADM/config" - export T_YADM_ENCRYPT="$T_DIR_YADM/encrypt" - export T_YADM_ARCHIVE="$T_DIR_YADM/files.gpg" - export T_YADM_BOOTSTRAP="$T_DIR_YADM/bootstrap" - - export T_YADM_Y - T_YADM_Y=( "$T_YADM" -Y "$T_DIR_YADM" ) - - export T_SYS - T_SYS=$(uname -s) - export T_HOST - T_HOST=$(hostname -s) - export T_USER - T_USER=$(id -u -n) - export T_DISTRO - T_DISTRO=$(lsb_release -si 2>/dev/null || true) -} - -function configure_git() { - (git config user.name || git config --global user.name 'test') >/dev/null - (git config user.email || git config --global user.email 'test@test.test') > /dev/null -} - -function make_parents() { - local parent_dir - parent_dir=$(dirname "$@") - mkdir -p "$parent_dir" -} - -function test_perms() { - local test_path="$1" - local regex="$2" - local ls - ls=$(ls -ld "$test_path") - local perms="${ls:0:10}" - if [[ ! $perms =~ $regex ]]; then - echo "ERROR: Found permissions $perms for $test_path" - return 1 - fi - return 0 -} - -function test_repo_attribute() { - local repo_dir="$1" - local attribute="$2" - local expected="$3" - local actual - actual=$(GIT_DIR="$repo_dir" git config --local "$attribute") - if [ "$actual" != "$expected" ]; then - echo "ERROR: repo attribute $attribute set to $actual" - return 1 - fi - return 0 -} - -#; create worktree at path -function create_worktree() { - local DIR_WORKTREE="$1" - if [ -z "$DIR_WORKTREE" ]; then - echo "ERROR: create_worktree() called without a path" - return 1 - fi - - if [[ ! "$DIR_WORKTREE" =~ ^$T_TMP ]]; then - echo "ERROR: create_worktree() called with a path outside of $T_TMP" - return 1 - fi - - #; remove any existing data - rm -rf "$DIR_WORKTREE" - - #; create some standard files - if [ ! -z "$TEST_TREE_WITH_ALT" ] ; then - for f in \ - "alt-none##S" \ - "alt-none##S.H" \ - "alt-none##S.H.U" \ - "alt-base##" \ - "alt-base##S" \ - "alt-base##S.H" \ - "alt-base##S.H.U" \ - "alt-system##" \ - "alt-system##S" \ - "alt-system##S.H" \ - "alt-system##S.H.U" \ - "alt-system##$T_SYS" \ - "alt-system##AAA" \ - "alt-system##ZZZ" \ - "alt-system##aaa" \ - "alt-system##zzz" \ - "alt-host##" \ - "alt-host##S" \ - "alt-host##S.H" \ - "alt-host##S.H.U" \ - "alt-host##$T_SYS.$T_HOST" \ - "alt-host##${T_SYS}_${T_HOST}" \ - "alt-user##" \ - "alt-user##S" \ - "alt-user##S.H" \ - "alt-user##S.H.U" \ - "alt-user##$T_SYS.$T_HOST.$T_USER" \ - "alt-user##${T_SYS}_${T_HOST}_${T_USER}" \ - "alt-override-system##" \ - "alt-override-system##$T_SYS" \ - "alt-override-system##custom_system" \ - "alt-override-host##" \ - "alt-override-host##$T_SYS.$T_HOST" \ - "alt-override-host##$T_SYS.custom_host" \ - "alt-override-user##" \ - "alt-override-user##S.H.U" \ - "alt-override-user##$T_SYS.$T_HOST.custom_user" \ - "dir one/alt-none##S/file1" \ - "dir one/alt-none##S/file2" \ - "dir one/alt-none##S.H/file1" \ - "dir one/alt-none##S.H/file2" \ - "dir one/alt-none##S.H.U/file1" \ - "dir one/alt-none##S.H.U/file2" \ - "dir one/alt-base##/file1" \ - "dir one/alt-base##/file2" \ - "dir one/alt-base##S/file1" \ - "dir one/alt-base##S/file2" \ - "dir one/alt-base##S.H/file1" \ - "dir one/alt-base##S.H/file2" \ - "dir one/alt-base##S.H.U/file1" \ - "dir one/alt-base##S.H.U/file2" \ - "dir one/alt-system##/file1" \ - "dir one/alt-system##/file2" \ - "dir one/alt-system##S/file1" \ - "dir one/alt-system##S/file2" \ - "dir one/alt-system##S.H/file1" \ - "dir one/alt-system##S.H/file2" \ - "dir one/alt-system##S.H.U/file1" \ - "dir one/alt-system##S.H.U/file2" \ - "dir one/alt-system##$T_SYS/file1" \ - "dir one/alt-system##$T_SYS/file2" \ - "dir one/alt-system##AAA/file1" \ - "dir one/alt-system##AAA/file2" \ - "dir one/alt-system##ZZZ/file1" \ - "dir one/alt-system##ZZZ/file2" \ - "dir one/alt-system##aaa/file1" \ - "dir one/alt-system##aaa/file2" \ - "dir one/alt-system##zzz/file1" \ - "dir one/alt-system##zzz/file2" \ - "dir one/alt-host##/file1" \ - "dir one/alt-host##/file2" \ - "dir one/alt-host##S/file1" \ - "dir one/alt-host##S/file2" \ - "dir one/alt-host##S.H/file1" \ - "dir one/alt-host##S.H/file2" \ - "dir one/alt-host##S.H.U/file1" \ - "dir one/alt-host##S.H.U/file2" \ - "dir one/alt-host##$T_SYS.$T_HOST/file1" \ - "dir one/alt-host##$T_SYS.$T_HOST/file2" \ - "dir one/alt-host##${T_SYS}_${T_HOST}/file1" \ - "dir one/alt-host##${T_SYS}_${T_HOST}/file2" \ - "dir one/alt-user##/file1" \ - "dir one/alt-user##/file2" \ - "dir one/alt-user##S/file1" \ - "dir one/alt-user##S/file2" \ - "dir one/alt-user##S.H/file1" \ - "dir one/alt-user##S.H/file2" \ - "dir one/alt-user##S.H.U/file1" \ - "dir one/alt-user##S.H.U/file2" \ - "dir one/alt-user##$T_SYS.$T_HOST.$T_USER/file1" \ - "dir one/alt-user##$T_SYS.$T_HOST.$T_USER/file2" \ - "dir one/alt-user##${T_SYS}_${T_HOST}_${T_USER}/file1" \ - "dir one/alt-user##${T_SYS}_${T_HOST}_${T_USER}/file2" \ - "dir one/alt-override-system##/file1" \ - "dir one/alt-override-system##/file2" \ - "dir one/alt-override-system##$T_SYS/file1" \ - "dir one/alt-override-system##$T_SYS/file2" \ - "dir one/alt-override-system##custom_system/file1" \ - "dir one/alt-override-system##custom_system/file2" \ - "dir one/alt-override-host##/file1" \ - "dir one/alt-override-host##/file2" \ - "dir one/alt-override-host##$T_SYS.$T_HOST/file1" \ - "dir one/alt-override-host##$T_SYS.$T_HOST/file2" \ - "dir one/alt-override-host##$T_SYS.custom_host/file1" \ - "dir one/alt-override-host##$T_SYS.custom_host/file2" \ - "dir one/alt-override-user##/file1" \ - "dir one/alt-override-user##/file2" \ - "dir one/alt-override-user##S.H.U/file1" \ - "dir one/alt-override-user##S.H.U/file2" \ - "dir one/alt-override-user##$T_SYS.$T_HOST.custom_user/file1" \ - "dir one/alt-override-user##$T_SYS.$T_HOST.custom_user/file2" \ - "dir2/file2" \ - ; - do - make_parents "$DIR_WORKTREE/$f" - echo "$f" > "$DIR_WORKTREE/$f" - done - echo "{{ YADM_CLASS }}-{{ YADM_OS }}-{{ YADM_HOSTNAME }}-{{ YADM_USER }}-{{ YADM_DISTRO }}" > "$DIR_WORKTREE/alt-jinja##yadm.j2" - fi - - #; for some cygwin tests - if [ ! -z "$TEST_TREE_WITH_CYGWIN" ] ; then - for f in \ - "alt-test##" \ - "alt-test##$T_SYS" \ - "alt-test##$SIMULATED_CYGWIN" \ - ; - do - make_parents "$DIR_WORKTREE/$f" - echo "$f" > "$DIR_WORKTREE/$f" - done - fi - - if [ ! -z "$TEST_TREE_WITH_WILD" ] ; then - #; wildcard test data - yes this is a big mess :( - #; none - for f in "wild-none##"; do - make_parents "$DIR_WORKTREE/$f" - echo "$f" > "$DIR_WORKTREE/$f" - done - #; system - for WILD_S in 'local' 'wild' 'other'; do - local s_base="wild-system-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - local f="${s_base}##${WILD_S}" - make_parents "$DIR_WORKTREE/$f" - echo "$f" > "$DIR_WORKTREE/$f" - done - #; system.host - for WILD_S in 'local' 'wild' 'other'; do - local s_base="wild-host-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - for WILD_H in 'local' 'wild' 'other'; do - local h_base="${s_base}-$WILD_H" - case $WILD_H in local) WILD_H="$T_HOST";; wild) WILD_H="%";; esac - local f="${h_base}##${WILD_S}.${WILD_H}" - make_parents "$DIR_WORKTREE/$f" - echo "$f" > "$DIR_WORKTREE/$f" - done - done - #; system.host.user - for WILD_S in 'local' 'wild' 'other'; do - local s_base="wild-user-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - for WILD_H in 'local' 'wild' 'other'; do - local h_base="${s_base}-$WILD_H" - case $WILD_H in local) WILD_H="$T_HOST";; wild) WILD_H="%";; esac - for WILD_U in 'local' 'wild' 'other'; do - local u_base="${h_base}-$WILD_U" - case $WILD_U in local) WILD_U="$T_USER";; wild) WILD_U="%";; esac - local f="${u_base}##${WILD_S}.${WILD_H}.${WILD_U}" - make_parents "$DIR_WORKTREE/$f" - echo "$f" > "$DIR_WORKTREE/$f" - done - done - done - #; class - for WILD_C in 'local' 'wild' 'other'; do - local c_base="wild-class-$WILD_C" - case $WILD_C in local) WILD_C="set_class";; wild) WILD_C="%";; esac - local f="${c_base}##${WILD_C}" - make_parents "$DIR_WORKTREE/$f" - echo "$f" > "$DIR_WORKTREE/$f" - done - #; class.system - for WILD_C in 'local' 'wild' 'other'; do - local c_base="wild-class-system-$WILD_C" - case $WILD_C in local) WILD_C="set_class";; wild) WILD_C="%";; esac - for WILD_S in 'local' 'wild' 'other'; do - local s_base="${c_base}-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - local f="${s_base}##${WILD_C}.${WILD_S}" - make_parents "$DIR_WORKTREE/$f" - echo "$f" > "$DIR_WORKTREE/$f" - done - done - #; class.system.host - for WILD_C in 'local' 'wild' 'other'; do - local c_base="wild-class-system-host-$WILD_C" - case $WILD_C in local) WILD_C="set_class";; wild) WILD_C="%";; esac - for WILD_S in 'local' 'wild' 'other'; do - local s_base="${c_base}-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - for WILD_H in 'local' 'wild' 'other'; do - local h_base="${s_base}-$WILD_H" - case $WILD_H in local) WILD_H="$T_HOST";; wild) WILD_H="%";; esac - local f="${h_base}##${WILD_C}.${WILD_S}.${WILD_H}" - make_parents "$DIR_WORKTREE/$f" - echo "$f" > "$DIR_WORKTREE/$f" - done - done - done - #; class.system.host.user - for WILD_C in 'local' 'wild' 'other'; do - local c_base="wild-class-system-host-user-$WILD_C" - case $WILD_C in local) WILD_C="set_class";; wild) WILD_C="%";; esac - for WILD_S in 'local' 'wild' 'other'; do - local s_base="${c_base}-$WILD_S" - case $WILD_S in local) WILD_S="$T_SYS";; wild) WILD_S="%";; esac - for WILD_H in 'local' 'wild' 'other'; do - local h_base="${s_base}-$WILD_H" - case $WILD_H in local) WILD_H="$T_HOST";; wild) WILD_H="%";; esac - for WILD_U in 'local' 'wild' 'other'; do - local u_base="${h_base}-$WILD_U" - case $WILD_U in local) WILD_U="$T_USER";; wild) WILD_U="%";; esac - local f="${u_base}##${WILD_C}.${WILD_S}.${WILD_H}.${WILD_U}" - make_parents "$DIR_WORKTREE/$f" - echo "$f" > "$DIR_WORKTREE/$f" - done - done - done - done - fi - for f in \ - .bash_profile \ - .gnupg/gpg.conf \ - .gnupg/pubring.gpg \ - .gnupg/secring.gpg \ - .hammerspoon/init.lua \ - .ssh/config \ - .ssh/secret.key \ - .ssh/secret.pub \ - .tmux.conf \ - .vimrc \ - "space test/file one" \ - "space test/file two" \ - ; - do - make_parents "$DIR_WORKTREE/$f" - echo "$f" > "$DIR_WORKTREE/$f" - done - - #; change all perms (so permission updates can be observed) - find "$DIR_WORKTREE" -exec chmod 0777 '{}' ';' - -} - -#; create a repo in T_DIR_REPO -function build_repo() { - local files_to_add=( "$@" ) - - #; create a worktree - create_worktree "$T_DIR_WORK" - - #; remove the repo if it exists - if [ -e "$T_DIR_REPO" ]; then - rm -rf "$T_DIR_REPO" - fi - - #; create the repo - git init --shared=0600 --bare "$T_DIR_REPO" >/dev/null 2>&1 - - #; standard repo config - GIT_DIR="$T_DIR_REPO" git config core.bare 'false' - GIT_DIR="$T_DIR_REPO" git config core.worktree "$T_DIR_WORK" - GIT_DIR="$T_DIR_REPO" git config status.showUntrackedFiles no - GIT_DIR="$T_DIR_REPO" git config yadm.managed 'true' - - if [ ${#files_to_add[@]} -ne 0 ]; then - for f in "${files_to_add[@]}"; do - GIT_DIR="$T_DIR_REPO" git add "$T_DIR_WORK/$f" >/dev/null - done - GIT_DIR="$T_DIR_REPO" git commit -m 'Create repo template' >/dev/null - fi - -} - -#; remove all tmp files -function destroy_tmp() { - load_fixtures - rm -rf "$T_TMP" -} - -configure_git diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..0b1f904 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,565 @@ +"""Global tests configuration and fixtures""" + +import collections +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 pytest + + +@pytest.fixture(scope='session') +def shellcheck_version(): + """Version of shellcheck supported""" + return '0.4.6' + + +@pytest.fixture(scope='session') +def pylint_version(): + """Version of pylint supported""" + return '1.9.2' + + +@pytest.fixture(scope='session') +def flake8_version(): + """Version of flake8 supported""" + return '3.5.0' + + +@pytest.fixture(scope='session') +def yamllint_version(): + """Version of yamllint supported""" + return '1.15.0' + + +@pytest.fixture(scope='session') +def tst_user(): + """Test session's user id""" + return pwd.getpwuid(os.getuid()).pw_name + + +@pytest.fixture(scope='session') +def tst_host(): + """Test session's short hostname value""" + return platform.node().split('.')[0] + + +@pytest.fixture(scope='session') +def tst_distro(runner): + """Test session's distro""" + distro = '' + try: + run = runner(command=['lsb_release', '-si'], report=False) + distro = run.out.strip() + except BaseException: + pass + return distro + + +@pytest.fixture(scope='session') +def tst_sys(): + """Test session's uname value""" + return platform.system() + + +@pytest.fixture(scope='session') +def cygwin_sys(): + """CYGWIN uname id""" + return 'CYGWIN_NT-6.1-WOW64' + + +@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', + 'gitconfig', + 'help', + 'init', + 'introspect', + 'list', + 'perms', + 'version', + ] + + +@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.class', + 'local.hostname', + 'local.os', + 'local.user', + 'yadm.auto-alt', + 'yadm.auto-perms', + 'yadm.auto-private-dirs', + 'yadm.cygwin-copy', + 'yadm.git-program', + 'yadm.gpg-perms', + 'yadm.gpg-program', + 'yadm.gpg-recipient', + 'yadm.ssh-perms', + ] + + +@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-dir', + '--yadm-encrypt', + '--yadm-repo', + '-Y', + ] + + +@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.')] + + +class Runner(object): + """Class for running commands + + Within yadm tests, this object should be used when running commands that + require: + + * Acting on the status code + * Parsing the output of the command + * Passing input to the command + + 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): + if shell: + self.command = ' '.join([str(cmd) for cmd in command]) + else: + self.command = command + self.inp = inp + self.wrap(expect) + process = Popen( + self.command, + stdin=PIPE, + stdout=PIPE, + stderr=PIPE, + shell=shell, + cwd=cwd, + env=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() + self.success = self.code == 0 + self.failure = self.code != 0 + if report: + self.report() + + def __repr__(self): + return f'Runner({self.command})' + + def report(self): + """Print code/stdout/stderr""" + 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}') + + 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' + 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') + self.inp = expect_script + print(f'EXPECT:{expect_script}') + self.command = ['expect'] + + +@pytest.fixture(scope='session') +def runner(): + """Class for running commands""" + return Runner + + +@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"') + return None + + +@pytest.fixture() +def repo_config(runner, paths): + """Function to query a yadm repo configuration value""" + + def query_func(key): + """Query a yadm repo configuration value""" + run = runner( + command=('git', 'config', '--local', key), + env={'GIT_DIR': paths.repo}, + report=False, + ) + return run.out.rstrip() + + return query_func + + +@pytest.fixture(scope='session') +def yadm(): + """Path to yadm program to be tested""" + full_path = os.path.realpath('yadm') + assert os.path.isfile(full_path), "yadm program file isn't present" + return full_path + + +@pytest.fixture() +def paths(tmpdir, yadm): + """Function scoped test paths""" + dir_root = tmpdir.mkdir('root') + dir_work = dir_root.mkdir('work') + dir_yadm = dir_root.mkdir('yadm') + dir_repo = dir_yadm.mkdir('repo.git') + dir_hooks = dir_yadm.mkdir('hooks') + dir_remote = dir_root.mkdir('remote') + file_archive = dir_yadm.join('files.gpg') + file_bootstrap = dir_yadm.join('bootstrap') + file_config = dir_yadm.join('config') + file_encrypt = dir_yadm.join('encrypt') + paths = collections.namedtuple( + 'Paths', [ + 'pgm', + 'root', + 'work', + 'yadm', + 'repo', + 'hooks', + 'remote', + 'archive', + 'bootstrap', + 'config', + 'encrypt', + ]) + return paths( + yadm, + dir_root, + dir_work, + dir_yadm, + dir_repo, + dir_hooks, + dir_remote, + file_archive, + file_bootstrap, + file_config, + file_encrypt, + ) + + +@pytest.fixture() +def yadm_y(paths): + """Generate custom command_list function""" + def command_list(*args): + """Produce params for running yadm with -Y""" + return [paths.pgm, '-Y', str(paths.yadm)] + list(args) + return command_list + + +class DataFile(object): + """Datafile object""" + + def __init__(self, path, tracked=True, private=False): + self.__path = path + self.__parent = None + self.__tracked = tracked + self.__private = private + + @property + def path(self): + """Path property""" + return self.__path + + @property + def relative(self): + """Relative path property""" + if self.__parent: + return self.__parent.join(self.path) + raise BaseException('Unable to provide relative path, no parent') + + @property + def tracked(self): + """Tracked property""" + return self.__tracked + + @property + def private(self): + """Private property""" + return self.__private + + def relative_to(self, parent): + """Update all relative paths to this py.path""" + self.__parent = parent + return + + +class DataSet(object): + """Dataset object""" + + def __init__(self): + self.__files = list() + self.__dirs = list() + self.__tracked_dirs = list() + self.__private_dirs = list() + self.__relpath = None + + def __repr__(self): + return ( + f'[DS with {len(self)} files; ' + f'{len(self.tracked)} tracked, ' + f'{len(self.private)} private]' + ) + + def __iter__(self): + return iter(self.__files) + + def __len__(self): + return len(self.__files) + + def __contains__(self, datafile): + if [f for f in self.__files if f.path == datafile]: + return True + if datafile in self.__files: + return True + return False + + @property + def files(self): + """List of DataFiles in DataSet""" + return list(self.__files) + + @property + def tracked(self): + """List of tracked DataFiles in DataSet""" + return [f for f in self.__files if f.tracked] + + @property + def private(self): + """List of private DataFiles in DataSet""" + return [f for f in self.__files if f.private] + + @property + def dirs(self): + """List of directories in DataSet""" + return list(self.__dirs) + + @property + def plain_dirs(self): + """List of directories in DataSet not starting with '.'""" + 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('.')] + + @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('.')] + + @property + def private_dirs(self): + """List of directories in DataSet considered 'private'""" + return list(self.__private_dirs) + + def add_file(self, path, tracked=True, private=False): + """Add file to data set""" + if path not in self: + datafile = DataFile(path, tracked, private) + if self.__relpath: + datafile.relative_to(self.__relpath) + self.__files.append(datafile) + + dname = os.path.dirname(path) + if dname and dname not in self.__dirs: + self.__dirs.append(dname) + if tracked: + self.__tracked_dirs.append(dname) + if private: + self.__private_dirs.append(dname) + + def relative_to(self, relpath): + """Update all relative paths to this py.path""" + self.__relpath = relpath + for datafile in self.files: + datafile.relative_to(self.__relpath) + return + + +@pytest.fixture(scope='session') +def ds1_dset(tst_sys, cygwin_sys): + """Meta-data for dataset one files""" + dset = DataSet() + dset.add_file('t1') + dset.add_file('d1/t2') + dset.add_file(f'test_alt##S') + dset.add_file(f'test_alt##S.H') + dset.add_file(f'test_alt##S.H.U') + dset.add_file(f'test_alt##C.S.H.U') + dset.add_file(f'test alt/test alt##S') + dset.add_file(f'test alt/test alt##S.H') + dset.add_file(f'test alt/test alt##S.H.U') + dset.add_file(f'test alt/test alt##C.S.H.U') + dset.add_file(f'test_cygwin_copy##{tst_sys}') + dset.add_file(f'test_cygwin_copy##{cygwin_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') +def ds1_data(tmpdir_factory, config_git, ds1_dset, runner): + """A set of test data, worktree & repo""" + # pylint: disable=unused-argument + # This is ignored because + # @pytest.mark.usefixtures('config_git') + # cannot be applied to another fixture. + + data = tmpdir_factory.mktemp('ds1') + + work = data.mkdir('work') + for datafile in ds1_dset: + work.join(datafile.path).write(datafile.path, ensure=True) + + 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) + + 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)) + return None + + +@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)) + env = os.environ.copy() + env['GIT_DIR'] = str(paths.repo) + runner( + command=['git', 'config', 'core.worktree', str(paths.work)], + env=env, + report=False) + return None + + +@pytest.fixture() +def ds1_copy(ds1_work_copy, ds1_repo_copy): + """Function scoped copy of ds1_data""" + # pylint: disable=unused-argument + # This is ignored because + # @pytest.mark.usefixtures('ds1_work_copy', 'ds1_repo_copy') + # cannot be applied to another fixture. + return None + + +@pytest.fixture() +def ds1(ds1_work_copy, paths, ds1_dset): + """Function scoped ds1_dset w/paths""" + # pylint: disable=unused-argument + # This is ignored because + # @pytest.mark.usefixtures('ds1_copy') + # cannot be applied to another fixture. + dscopy = copy.deepcopy(ds1_dset) + dscopy.relative_to(copy.deepcopy(paths.work)) + return dscopy diff --git a/test/pylintrc b/test/pylintrc new file mode 120000 index 0000000..05334af --- /dev/null +++ b/test/pylintrc @@ -0,0 +1 @@ +../pylintrc \ No newline at end of file diff --git a/test/test_alt.py b/test/test_alt.py new file mode 100644 index 0000000..10315ae --- /dev/null +++ b/test/test_alt.py @@ -0,0 +1,440 @@ +"""Test alt""" + +import os +import re +import string +import py +import pytest +import utils + +# These test IDs are broken. During the writing of these tests, problems have +# been discovered in the way yadm orders matching files. +BROKEN_TEST_IDS = [ + 'test_wild[tracked-##C.S.H.U-C-S%-H%-U]', + 'test_wild[tracked-##C.S.H.U-C-S-H%-U]', + 'test_wild[encrypted-##C.S.H.U-C-S%-H%-U]', + 'test_wild[encrypted-##C.S.H.U-C-S-H%-U]', + ] + +PRECEDENCE = [ + '##', + '##$tst_sys', + '##$tst_sys.$tst_host', + '##$tst_sys.$tst_host.$tst_user', + '##$tst_class', + '##$tst_class.$tst_sys', + '##$tst_class.$tst_sys.$tst_host', + '##$tst_class.$tst_sys.$tst_host.$tst_user', + ] + +WILD_TEMPLATES = [ + '##$tst_class', + '##$tst_class.$tst_sys', + '##$tst_class.$tst_sys.$tst_host', + '##$tst_class.$tst_sys.$tst_host.$tst_user', + ] + +TEST_PATHS = [utils.ALT_FILE1, utils.ALT_FILE2, utils.ALT_DIR] + +WILD_TESTED = set() + + +@pytest.mark.parametrize('precedence_index', range(len(PRECEDENCE))) +@pytest.mark.parametrize( + 'tracked, encrypt, exclude', [ + (False, False, False), + (True, False, False), + (False, True, False), + (False, True, True), + ], ids=[ + 'untracked', + 'tracked', + 'encrypted', + 'excluded', + ]) +@pytest.mark.usefixtures('ds1_copy') +def test_alt(runner, yadm_y, paths, + tst_sys, tst_host, tst_user, + tracked, encrypt, exclude, + precedence_index): + """Test alternate linking + + This test is done by iterating for the number of templates in PRECEDENCE. + With each iteration, another file is left off the list. So with each + iteration, the template with the "highest precedence" is left out. The file + using the highest precedence should be the one linked. + """ + + # set the class + tst_class = 'testclass' + utils.set_local(paths, 'class', tst_class) + + # process the templates in PRECEDENCE + precedence = list() + for template in PRECEDENCE: + precedence.append( + string.Template(template).substitute( + tst_class=tst_class, + tst_host=tst_host, + tst_sys=tst_sys, + tst_user=tst_user, + ) + ) + + # create files using a subset of files + for suffix in precedence[0:precedence_index+1]: + utils.create_alt_files(paths, suffix, tracked=tracked, + encrypt=encrypt, exclude=exclude) + + # run alt to trigger linking + run = runner(yadm_y('alt')) + assert run.success + assert run.err == '' + linked = linked_list(run.out) + + # assert the proper linking has occurred + for file_path in TEST_PATHS: + source_file = file_path + precedence[precedence_index] + if tracked or (encrypt and not exclude): + assert paths.work.join(file_path).islink() + target = py.path.local(paths.work.join(file_path).readlink()) + if target.isfile(): + assert paths.work.join(file_path).read() == source_file + assert str(paths.work.join(source_file)) in linked + else: + assert paths.work.join(file_path).join( + utils.CONTAINED).read() == source_file + assert str(paths.work.join(source_file)) in linked + else: + assert not paths.work.join(file_path).exists() + assert str(paths.work.join(source_file)) not in linked + + +def short_template(template): + """Translate template into something short for test IDs""" + return string.Template(template).substitute( + tst_class='C', + tst_host='H', + tst_sys='S', + tst_user='U', + ) + + +@pytest.mark.parametrize('wild_user', [True, False], ids=['U%', 'U']) +@pytest.mark.parametrize('wild_host', [True, False], ids=['H%', 'H']) +@pytest.mark.parametrize('wild_sys', [True, False], ids=['S%', 'S']) +@pytest.mark.parametrize('wild_class', [True, False], ids=['C%', 'C']) +@pytest.mark.parametrize('template', WILD_TEMPLATES, ids=short_template) +@pytest.mark.parametrize( + 'tracked, encrypt', [ + (True, False), + (False, True), + ], ids=[ + 'tracked', + 'encrypted', + ]) +@pytest.mark.usefixtures('ds1_copy') +def test_wild(request, runner, yadm_y, paths, + tst_sys, tst_host, tst_user, + tracked, encrypt, + wild_class, wild_host, wild_sys, wild_user, + template): + """Test wild linking + + These tests are done by creating permutations of the possible files using + WILD_TEMPLATES. Each case is then tested (while skipping the already tested + permutations for efficiency). + """ + + if request.node.name in BROKEN_TEST_IDS: + pytest.xfail( + 'This test is known to be broken. ' + 'This bug needs to be fixed.') + + tst_class = 'testclass' + + # determine the "wild" version of the suffix + str_class = '%' if wild_class else tst_class + str_host = '%' if wild_host else tst_host + str_sys = '%' if wild_sys else tst_sys + str_user = '%' if wild_user else tst_user + wild_suffix = string.Template(template).substitute( + tst_class=str_class, + tst_host=str_host, + tst_sys=str_sys, + tst_user=str_user, + ) + + # determine the "standard" version of the suffix + std_suffix = string.Template(template).substitute( + tst_class=tst_class, + tst_host=tst_host, + tst_sys=tst_sys, + tst_user=tst_user, + ) + + # skip over duplicate tests (this seems to be the simplest way to cover the + # permutations of tests, while skipping duplicates.) + test_key = f'{tracked}{encrypt}{wild_suffix}{std_suffix}' + if test_key in WILD_TESTED: + return + WILD_TESTED.add(test_key) + + # set the class + utils.set_local(paths, 'class', tst_class) + + # create files using the wild suffix + utils.create_alt_files(paths, wild_suffix, tracked=tracked, + encrypt=encrypt, exclude=False) + + # run alt to trigger linking + run = runner(yadm_y('alt')) + assert run.success + assert run.err == '' + linked = linked_list(run.out) + + # assert the proper linking has occurred + for file_path in TEST_PATHS: + source_file = file_path + wild_suffix + assert paths.work.join(file_path).islink() + target = py.path.local(paths.work.join(file_path).readlink()) + if target.isfile(): + assert paths.work.join(file_path).read() == source_file + assert str(paths.work.join(source_file)) in linked + else: + assert paths.work.join(file_path).join( + utils.CONTAINED).read() == source_file + assert str(paths.work.join(source_file)) in linked + + # create files using the standard suffix + utils.create_alt_files(paths, std_suffix, tracked=tracked, + encrypt=encrypt, exclude=False) + + # run alt to trigger linking + run = runner(yadm_y('alt')) + assert run.success + assert run.err == '' + linked = linked_list(run.out) + + # assert the proper linking has occurred + for file_path in TEST_PATHS: + source_file = file_path + std_suffix + assert paths.work.join(file_path).islink() + target = py.path.local(paths.work.join(file_path).readlink()) + if target.isfile(): + assert paths.work.join(file_path).read() == source_file + assert str(paths.work.join(source_file)) in linked + else: + assert paths.work.join(file_path).join( + utils.CONTAINED).read() == source_file + assert str(paths.work.join(source_file)) in linked + + +@pytest.mark.usefixtures('ds1_copy') +def test_local_override(runner, yadm_y, paths, + tst_sys, tst_host, tst_user): + """Test local overrides""" + + # define local overrides + utils.set_local(paths, 'class', 'or-class') + utils.set_local(paths, 'hostname', 'or-hostname') + utils.set_local(paths, 'os', 'or-os') + utils.set_local(paths, 'user', 'or-user') + + # create files, the first would normally be the most specific version + # however, the second is the overridden version which should be preferred. + utils.create_alt_files( + paths, f'##or-class.{tst_sys}.{tst_host}.{tst_user}') + utils.create_alt_files( + paths, '##or-class.or-os.or-hostname.or-user') + + # run alt to trigger linking + run = runner(yadm_y('alt')) + assert run.success + assert run.err == '' + linked = linked_list(run.out) + + # assert the proper linking has occurred + for file_path in TEST_PATHS: + source_file = file_path + '##or-class.or-os.or-hostname.or-user' + assert paths.work.join(file_path).islink() + target = py.path.local(paths.work.join(file_path).readlink()) + if target.isfile(): + assert paths.work.join(file_path).read() == source_file + assert str(paths.work.join(source_file)) in linked + else: + assert paths.work.join(file_path).join( + utils.CONTAINED).read() == source_file + assert str(paths.work.join(source_file)) in linked + + +@pytest.mark.parametrize('suffix', ['AAA', 'ZZZ', 'aaa', 'zzz']) +@pytest.mark.usefixtures('ds1_copy') +def test_class_case(runner, yadm_y, paths, tst_sys, suffix): + """Test range of class cases""" + + # set the class + utils.set_local(paths, 'class', suffix) + + # create files + endings = [suffix] + if tst_sys == 'Linux': + # Only create all of these side-by-side on Linux, which is + # unquestionably case-sensitive. This would break tests on + # case-insensitive systems. + endings = ['AAA', 'ZZZ', 'aaa', 'zzz'] + for ending in endings: + utils.create_alt_files(paths, f'##{ending}') + + # run alt to trigger linking + run = runner(yadm_y('alt')) + assert run.success + assert run.err == '' + linked = linked_list(run.out) + + # assert the proper linking has occurred + for file_path in TEST_PATHS: + source_file = file_path + f'##{suffix}' + assert paths.work.join(file_path).islink() + target = py.path.local(paths.work.join(file_path).readlink()) + if target.isfile(): + assert paths.work.join(file_path).read() == source_file + assert str(paths.work.join(source_file)) in linked + else: + assert paths.work.join(file_path).join( + utils.CONTAINED).read() == source_file + assert str(paths.work.join(source_file)) in linked + + +@pytest.mark.parametrize('autoalt', [None, 'true', 'false']) +@pytest.mark.usefixtures('ds1_copy') +def test_auto_alt(runner, yadm_y, paths, autoalt): + """Test setting auto-alt""" + + # set the value of auto-alt + if autoalt: + os.system(' '.join(yadm_y('config', 'yadm.auto-alt', autoalt))) + + # create file + suffix = '##' + utils.create_alt_files(paths, suffix) + + # run status to possibly trigger linking + run = runner(yadm_y('status')) + assert run.success + assert run.err == '' + linked = linked_list(run.out) + + # assert the proper linking has occurred + for file_path in TEST_PATHS: + source_file = file_path + suffix + if autoalt == 'false': + assert not paths.work.join(file_path).exists() + else: + assert paths.work.join(file_path).islink() + target = py.path.local(paths.work.join(file_path).readlink()) + if target.isfile(): + assert paths.work.join(file_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(file_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.parametrize('delimiter', ['.', '_']) +@pytest.mark.usefixtures('ds1_copy') +def test_delimiter(runner, yadm_y, paths, + tst_sys, tst_host, tst_user, delimiter): + """Test delimiters used""" + + suffix = '##' + delimiter.join([tst_sys, tst_host, tst_user]) + + # create file + utils.create_alt_files(paths, suffix) + + # run alt to trigger linking + run = runner(yadm_y('alt')) + assert run.success + assert run.err == '' + linked = linked_list(run.out) + + # assert the proper linking has occurred + # only a delimiter of '.' is valid + for file_path in TEST_PATHS: + source_file = file_path + suffix + if delimiter == '.': + assert paths.work.join(file_path).islink() + target = py.path.local(paths.work.join(file_path).readlink()) + if target.isfile(): + assert paths.work.join(file_path).read() == source_file + assert str(paths.work.join(source_file)) in linked + else: + assert paths.work.join(file_path).join( + utils.CONTAINED).read() == source_file + assert str(paths.work.join(source_file)) in linked + else: + assert not paths.work.join(file_path).exists() + assert str(paths.work.join(source_file)) not in linked + + +@pytest.mark.usefixtures('ds1_copy') +def test_invalid_links_removed(runner, yadm_y, paths): + """Links to invalid alternative files are removed + + This test ensures that when an already linked alternative becomes invalid + due to a change in class, the alternate link is removed. + """ + + # set the class + tst_class = 'testclass' + utils.set_local(paths, 'class', tst_class) + + # create files which match the test class + utils.create_alt_files(paths, f'##{tst_class}') + + # run alt to trigger linking + run = runner(yadm_y('alt')) + assert run.success + assert run.err == '' + linked = linked_list(run.out) + + # assert the proper linking has occurred + for file_path in TEST_PATHS: + source_file = file_path + '##' + tst_class + assert paths.work.join(file_path).islink() + target = py.path.local(paths.work.join(file_path).readlink()) + if target.isfile(): + assert paths.work.join(file_path).read() == source_file + assert str(paths.work.join(source_file)) in linked + else: + assert paths.work.join(file_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') + + # run alt to trigger linking + run = runner(yadm_y('alt')) + assert run.success + assert run.err == '' + linked = linked_list(run.out) + + # assert the linking is removed + for file_path in TEST_PATHS: + source_file = file_path + '##' + tst_class + assert not paths.work.join(file_path).exists() + assert str(paths.work.join(source_file)) not in linked + + +def linked_list(output): + """Parse output, and return list of linked files""" + linked = dict() + for line in output.splitlines(): + match = re.match('Linking (.+) to (.+)$', line) + if match: + linked[match.group(2)] = match.group(1) + return linked.values() diff --git a/test/test_assert_private_dirs.py b/test/test_assert_private_dirs.py new file mode 100644 index 0000000..65cb0b7 --- /dev/null +++ b/test/test_assert_private_dirs.py @@ -0,0 +1,106 @@ +"""Test asserting private directories""" + +import os +import re +import pytest + +pytestmark = pytest.mark.usefixtures('ds1_copy') +PRIVATE_DIRS = ['.gnupg', '.ssh'] + + +def test_pdirs_missing(runner, yadm_y, paths): + """Private dirs (private dirs missing) + + When a git command is run + And private directories are missing + Create private directories prior to command + """ + + # confirm directories are missing at start + for pdir in PRIVATE_DIRS: + path = paths.work.join(pdir) + if path.exists(): + path.remove() + assert not path.exists() + + # run status + run = runner(command=yadm_y('status'), env={'DEBUG': 'yes'}) + assert run.success + assert run.err == '' + assert 'On branch master' in run.out + + # confirm directories are created + # and are protected + for pdir in PRIVATE_DIRS: + path = paths.work.join(pdir) + assert path.exists() + assert oct(path.stat().mode).endswith('00'), 'Directory is not secured' + + # confirm directories are created before command is run: + assert re.search( + r'Creating.+\.gnupg.+Creating.+\.ssh.+Running git command git status', + run.out, re.DOTALL), 'directories created before command is run' + + +def test_pdirs_missing_apd_false(runner, yadm_y, paths): + """Private dirs (private dirs missing / yadm.auto-private-dirs=false) + + When a git command is run + And private directories are missing + But auto-private-dirs is false + Do not create private dirs + """ + + # confirm directories are missing at start + for pdir in PRIVATE_DIRS: + path = paths.work.join(pdir) + if path.exists(): + path.remove() + assert not path.exists() + + # set configuration + os.system(' '.join(yadm_y( + 'config', '--bool', 'yadm.auto-private-dirs', 'false'))) + + # run status + run = runner(command=yadm_y('status')) + assert run.success + assert run.err == '' + assert 'On branch master' in run.out + + # confirm directories are STILL missing + for pdir in PRIVATE_DIRS: + assert not paths.work.join(pdir).exists() + + +def test_pdirs_exist_apd_false(runner, yadm_y, paths): + """Private dirs (private dirs exist / yadm.auto-perms=false) + + When a git command is run + And private directories exist + And yadm is configured not to auto update perms + Do not alter directories + """ + + # create permissive directories + for pdir in PRIVATE_DIRS: + path = paths.work.join(pdir) + if not path.isdir(): + path.mkdir() + path.chmod(0o777) + assert oct(path.stat().mode).endswith('77'), 'Directory is secure.' + + # set configuration + os.system(' '.join(yadm_y( + 'config', '--bool', 'yadm.auto-perms', 'false'))) + + # run status + run = runner(command=yadm_y('status')) + assert run.success + 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' diff --git a/test/test_bootstrap.py b/test/test_bootstrap.py new file mode 100644 index 0000000..2adbe33 --- /dev/null +++ b/test/test_bootstrap.py @@ -0,0 +1,31 @@ +"""Test bootstrap""" + +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_y, paths, exists, executable, code, expect): + """Test bootstrap command""" + if exists: + paths.bootstrap.write('') + if executable: + paths.bootstrap.write( + '#!/bin/bash\n' + f'echo {expect}\n' + f'exit {code}\n' + ) + paths.bootstrap.chmod(0o775) + run = runner(command=yadm_y('bootstrap')) + assert run.code == code + assert run.err == '' + assert expect in run.out diff --git a/test/test_clean.py b/test/test_clean.py new file mode 100644 index 0000000..9a2221a --- /dev/null +++ b/test/test_clean.py @@ -0,0 +1,11 @@ +"""Test clean""" + + +def test_clean_command(runner, yadm_y): + """Run with clean command""" + run = runner(command=yadm_y('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.err == '' + assert 'disabled' in run.out diff --git a/test/test_clone.py b/test/test_clone.py new file mode 100644 index 0000000..90febe1 --- /dev/null +++ b/test/test_clone.py @@ -0,0 +1,274 @@ +"""Test clone""" + +import os +import re +import pytest + +BOOTSTRAP_CODE = 123 +BOOTSTRAP_MSG = 'Bootstrap successful' + + +@pytest.mark.usefixtures('remote') +@pytest.mark.parametrize( + '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_y, repo_config, ds1, + good_remote, repo_exists, force, conflicts): + """Test basic clone operation""" + + # determine remote url + remote_url = f'file://{paths.remote}' + if not good_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') + + if conflicts: + ds1.tracked[0].relative.write('conflict') + assert ds1.tracked[0].relative.exists() + + # run the clone command + args = ['clone', '-w', paths.work] + if force: + args += ['-f'] + args += [remote_url] + run = runner(command=yadm_y(*args)) + + if not good_remote: + # clone should fail + assert run.failure + assert run.err != '' + assert 'Unable to fetch origin' in run.out + assert not paths.repo.exists() + elif repo_exists and not force: + # can't overwrite data + assert run.failure + assert run.err == '' + assert 'Git repo already exists' in run.out + else: + # clone should succeed, and repo should be configured properly + assert successful_clone(run, paths, repo_config) + + # ensure conflicts are handled properly + if conflicts: + assert 'NOTE' in run.out + assert 'Merging origin/master failed' in run.out + assert 'Conflicts preserved' in run.out + + # confirm correct Git origin + 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 + + # ensure conflicts are really preserved + if conflicts: + # test to see if the work tree is actually "clean" + run = runner( + command=yadm_y('status', '-uno', '--porcelain'), + cwd=paths.work) + assert run.success + assert run.err == '' + assert run.out == '', 'worktree has unexpected changes' + + # test to see if the conflicts are stashed + run = runner(command=yadm_y('stash', 'list'), cwd=paths.work) + assert run.success + assert run.err == '' + assert 'Conflicts preserved' in run.out, 'conflicts not stashed' + + # verify content of the stashed conflicts + run = runner(command=yadm_y('stash', 'show', '-p'), cwd=paths.work) + assert run.success + assert run.err == '' + assert '\n+conflict' in run.out, 'conflicts not stashed' + + # another force-related assertion + if old_repo: + if force: + assert not old_repo.exists() + else: + assert old_repo.exists() + + +@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_y, 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] + if bs_param: + args += [bs_param] + args += [f'file://{paths.remote}'] + expect = [] + if answer: + expect.append(('Would you like to execute it now', answer)) + run = runner(command=yadm_y(*args), expect=expect) + + if answer: + assert 'Would you like to execute it now' in run.out + + expected_code = 0 + if bs_exists and bs_param != '--no-bootstrap': + expected_code = BOOTSTRAP_CODE + + if answer == 'y': + expected_code = BOOTSTRAP_CODE + assert BOOTSTRAP_MSG in run.out + elif answer == 'n': + expected_code = 0 + assert BOOTSTRAP_MSG not in run.out + + assert successful_clone(run, paths, repo_config, expected_code) + + if not bs_exists: + assert BOOTSTRAP_MSG not in run.out + + +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.chmod(0o775) + assert paths.bootstrap.exists() + else: + assert not paths.bootstrap.exists() + + +@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_y, 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') + 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}') + if not pdir.exists(): + pdir.mkdir() + pfile = pdir.join('existing') + pfile.write('existing') + pdir.chmod(0o777) + pfile.chmod(0o777) + else: + paths.work.remove() + paths.work.mkdir() + + run = runner( + yadm_y('clone', '-d', '-w', paths.work, f'file://{paths.remote}')) + + assert successful_clone(run, paths, repo_config) + 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-merge private dir perms drwxrwxrwx.+.{private_type}', + run.out) + assert re.search( + f'post-merge private dir perms drwxrwxrwx.+.{private_type}', + run.out) + else: + # private directories which are created, should be done prior to + # merging, and with secure permissions. + assert 'initial private dir perms' not in run.out + assert re.search( + f'pre-merge private dir perms drwx------.+.{private_type}', + run.out) + assert re.search( + f'post-merge 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') + + +def successful_clone(run, paths, repo_config, expected_code=0): + """Assert clone is successful""" + assert run.code == expected_code + assert 'Initialized' in run.out + 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 + + +@pytest.fixture() +def remote(paths, ds1_repo_copy): + """Function scoped remote (based on ds1)""" + # pylint: disable=unused-argument + # This is ignored because + # @pytest.mark.usefixtures('ds1_remote_copy') + # cannot be applied to another fixture. + paths.remote.remove() + paths.repo.move(paths.remote) + return None diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 0000000..4e44b1c --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,139 @@ +"""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}' + + +def test_config_no_params(runner, yadm_y, supported_configs): + """No parameters + + Display instructions + Display supported configs + Exit with 0 + """ + + run = runner(yadm_y('config')) + + assert run.success + assert run.err == '' + assert 'Please read the CONFIGURATION section' in run.out + for config in supported_configs: + assert config in run.out + + +def test_config_read_missing(runner, yadm_y): + """Read missing attribute + + Display an empty value + Exit with 0 + """ + + run = runner(yadm_y('config', TEST_KEY)) + + assert run.success + assert run.err == '' + assert run.out == '' + + +def test_config_write(runner, yadm_y, paths): + """Write attribute + + Display no output + Update configuration file + Exit with 0 + """ + + run = runner(yadm_y('config', TEST_KEY, TEST_VALUE)) + + assert run.success + assert run.err == '' + assert run.out == '' + assert paths.config.read().strip() == TEST_FILE + + +def test_config_read(runner, yadm_y, paths): + """Read attribute + + Display value + Exit with 0 + """ + + paths.config.write(TEST_FILE) + run = runner(yadm_y('config', TEST_KEY)) + + assert run.success + assert run.err == '' + assert run.out.strip() == TEST_VALUE + + +def test_config_update(runner, yadm_y, paths): + """Update attribute + + Display no output + Update configuration file + Exit with 0 + """ + + paths.config.write(TEST_FILE) + + run = runner(yadm_y('config', TEST_KEY, TEST_VALUE + 'extra')) + + assert run.success + assert run.err == '' + assert run.out == '' + + assert paths.config.read().strip() == TEST_FILE + 'extra' + + +@pytest.mark.usefixtures('ds1_repo_copy') +def test_config_local_read(runner, yadm_y, paths, supported_local_configs): + """Read local attribute + + Display value from the repo config + Exit with 0 + """ + + # populate test values + for config in supported_local_configs: + 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_y('config', config)) + assert run.success + assert run.err == '' + assert run.out.strip() == f'value_of_{config}' + + +@pytest.mark.usefixtures('ds1_repo_copy') +def test_config_local_write(runner, yadm_y, paths, supported_local_configs): + """Write local attribute + + Display no output + Write value to the repo config + Exit with 0 + """ + + # run yadm config + for config in supported_local_configs: + run = runner(yadm_y('config', config, f'value_of_{config}')) + assert run.success + 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}) + assert run.success + assert run.err == '' + assert run.out.strip() == f'value_of_{config}' diff --git a/test/test_cygwin_copy.py b/test/test_cygwin_copy.py new file mode 100644 index 0000000..82b08ba --- /dev/null +++ b/test/test_cygwin_copy.py @@ -0,0 +1,59 @@ +"""Test yadm.cygwin_copy""" + +import os +import pytest + + +@pytest.mark.parametrize( + 'setting, is_cygwin, expect_link, pre_existing', [ + (None, False, True, None), + (True, False, True, None), + (False, False, True, None), + (None, True, True, None), + (True, True, False, None), + (False, True, True, None), + (True, True, False, 'link'), + (True, True, False, 'file'), + ], + ids=[ + 'unset, non-cygwin', + 'true, non-cygwin', + 'false, non-cygwin', + 'unset, cygwin', + 'true, cygwin', + 'false, cygwin', + 'pre-existing symlink', + 'pre-existing file', + ]) +@pytest.mark.usefixtures('ds1_copy') +def test_cygwin_copy( + runner, yadm_y, paths, cygwin_sys, tst_sys, + setting, is_cygwin, expect_link, pre_existing): + """Test yadm.cygwin_copy""" + + if setting is not None: + os.system(' '.join(yadm_y('config', 'yadm.cygwin-copy', str(setting)))) + + expected_content = f'test_cygwin_copy##{tst_sys}' + alt_path = paths.work.join('test_cygwin_copy') + if pre_existing == 'symlink': + alt_path.mklinkto(expected_content) + elif pre_existing == 'file': + alt_path.write('wrong content') + + uname_path = paths.root.join('tmp').mkdir() + if is_cygwin: + uname = uname_path.join('uname') + uname.write(f'#!/bin/sh\necho "{cygwin_sys}"\n') + uname.chmod(0o777) + expected_content = f'test_cygwin_copy##{cygwin_sys}' + env = os.environ.copy() + env['PATH'] = ':'.join([str(uname_path), env['PATH']]) + + run = runner(yadm_y('alt'), env=env) + assert run.success + 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_encryption.py b/test/test_encryption.py new file mode 100644 index 0000000..f107ad5 --- /dev/null +++ b/test/test_encryption.py @@ -0,0 +1,392 @@ +"""Test encryption""" + +import os +import pipes +import pytest + +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') + + +def add_asymmetric_key(): + """Add asymmetric key""" + os.system(f'gpg --import {pipes.quote(KEY_FILE)}') + os.system(f'gpg --import-ownertrust < {pipes.quote(KEY_TRUST)}') + + +def remove_asymmetric_key(): + """Remove asymmetric key""" + os.system( + f'gpg --batch --yes ' + f'--delete-secret-keys {pipes.quote(KEY_FINGERPRINT)}') + os.system(f'gpg --batch --yes --delete-key {pipes.quote(KEY_FINGERPRINT)}') + + +@pytest.fixture +def asymmetric_key(): + """Fixture for asymmetric key, removed in teardown""" + add_asymmetric_key() + yield KEY_NAME + remove_asymmetric_key() + + +@pytest.fixture +def encrypt_targets(yadm_y, paths): + """Fixture for setting up data to encrypt + + This fixture: + * inits an empty repo + * creates test files in the work tree + * creates a ".yadm/encrypt" file for testing: + * standard files + * standard globs + * directories + * comments + * empty lines and lines with just space + * exclusions + * returns a list of expected encrypted files + """ + + # init empty yadm repo + os.system(' '.join(yadm_y('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') + + # 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') + + # blank lines + 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') + + # 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* + + return expected + + +@pytest.fixture(scope='session') +def decrypt_targets(tmpdir_factory, runner): + """Fixture for setting data to decrypt + + This fixture: + * creates symmetric/asymmetric encrypted archives + * 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') + + 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') + + run = runner( + ['tar', 'cvf', '-'] + + expected + + ['|', 'gpg', '--batch', '--yes', '-c'] + + ['--passphrase', pipes.quote(PASSPHRASE)] + + ['--output', pipes.quote(str(symmetric))], + cwd=tmpdir, + shell=True) + assert run.success + + add_asymmetric_key() + run = runner( + ['tar', 'cvf', '-'] + + expected + + ['|', 'gpg', '--batch', '--yes', '-e'] + + ['-r', pipes.quote(KEY_NAME)] + + ['--output', pipes.quote(str(asymmetric))], + cwd=tmpdir, + shell=True) + assert run.success + remove_asymmetric_key() + + return { + 'asymmetric': asymmetric, + 'expected': expected, + 'symmetric': symmetric, + } + + +@pytest.mark.parametrize( + 'mismatched_phrase', [False, True], + ids=['matching_phrase', 'mismatched_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_y, paths, encrypt_targets, + overwrite, missing_encrypt, mismatched_phrase): + """Test symmetric encryption""" + + if missing_encrypt: + paths.encrypt.remove() + + matched_phrase = PASSPHRASE + if mismatched_phrase: + matched_phrase = 'mismatched' + + if overwrite: + paths.archive.write('existing archive') + + run = runner(yadm_y('encrypt'), expect=[ + ('passphrase:', PASSPHRASE), + ('passphrase:', matched_phrase), + ]) + + if missing_encrypt or mismatched_phrase: + assert run.failure + else: + assert run.success + assert run.err == '' + + if missing_encrypt: + assert 'does not exist' in run.out + elif mismatched_phrase: + assert 'invalid passphrase' in run.out + else: + assert encrypted_data_valid(runner, paths.archive, encrypt_targets) + + +@pytest.mark.parametrize( + 'wrong_phrase', [False, True], + ids=['correct_phrase', 'wrong_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_y, paths, decrypt_targets, + dolist, archive_exists, wrong_phrase): + """Test decryption""" + + # init empty yadm repo + os.system(' '.join(yadm_y('init', '-w', str(paths.work), '-f'))) + + phrase = PASSPHRASE + if wrong_phrase: + phrase = 'wrong-phrase' + + if archive_exists: + decrypt_targets['symmetric'].copy(paths.archive) + + # to test overwriting + paths.work.join('decrypt1').write('pre-existing file') + + args = [] + + if dolist: + args.append('-l') + run = runner(yadm_y('decrypt') + args, expect=[('passphrase:', phrase)]) + + if archive_exists and not wrong_phrase: + assert run.success + assert run.err == '' + if dolist: + 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']: + 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_y, paths, encrypt_targets, + overwrite, key_exists, ask): + """Test asymmetric encryption""" + + # specify encryption recipient + if ask: + os.system(' '.join(yadm_y('config', 'yadm.gpg-recipient', 'ASK'))) + expect = [('Enter the user ID', KEY_NAME), ('Enter the user ID', '')] + else: + os.system(' '.join(yadm_y('config', 'yadm.gpg-recipient', KEY_NAME))) + expect = [] + + if overwrite: + paths.archive.write('existing archive') + + if not key_exists: + remove_asymmetric_key() + + run = runner(yadm_y('encrypt'), expect=expect) + + if key_exists: + assert run.success + assert encrypted_data_valid(runner, paths.archive, encrypt_targets) + else: + assert run.failure + assert 'Unable to write' in run.out + + if ask: + assert 'Enter the user ID' in run.out + + +@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_y, paths, decrypt_targets, + dolist, key_exists): + """Test decryption""" + + # init empty yadm repo + os.system(' '.join(yadm_y('init', '-w', str(paths.work), '-f'))) + + decrypt_targets['asymmetric'].copy(paths.archive) + + # to test overwriting + paths.work.join('decrypt1').write('pre-existing file') + + if not key_exists: + remove_asymmetric_key() + + args = [] + + if dolist: + args.append('-l') + run = runner(yadm_y('decrypt') + args) + + if key_exists: + assert run.success + if dolist: + 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']: + assert paths.work.join(filename).read() == filename + else: + assert run.failure + assert 'Unable to extract encrypted files' in run.out + + +@pytest.mark.parametrize( + 'untracked', + [False, 'y', 'n'], + ids=['tracked', 'untracked_answer_y', 'untracked_answer_n']) +def test_offer_to_add(runner, yadm_y, paths, encrypt_targets, untracked): + """Test offer to add encrypted archive + + All the other encryption tests use an archive outside of the work tree. + However, the archive is often inside the work tree, and if it is, there + should be an offer to add it to the repo if it is not tracked. + """ + + worktree_archive = paths.work.join('worktree-archive.tar.gpg') + expect = [ + ('passphrase:', PASSPHRASE), + ('passphrase:', PASSPHRASE), + ] + + if untracked: + expect.append(('add it now', untracked)) + else: + worktree_archive.write('exists') + os.system(' '.join(yadm_y('add', str(worktree_archive)))) + + run = runner( + yadm_y('encrypt', '--yadm-archive', str(worktree_archive)), + expect=expect + ) + + assert run.success + assert run.err == '' + assert encrypted_data_valid(runner, worktree_archive, encrypt_targets) + + run = runner( + yadm_y('status', '--porcelain', '-uall', str(worktree_archive))) + assert run.success + assert run.err == '' + + if untracked == 'y': + # should be added to the index + 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 + else: + # should appear modified in the index + assert f'AM {worktree_archive.basename}' in run.out + + +def encrypted_data_valid(runner, encrypted, expected): + """Verify encrypted data matches expectations""" + run = runner([ + 'gpg', + '--passphrase', pipes.quote(PASSPHRASE), + '-d', pipes.quote(str(encrypted)), + '2>/dev/null', + '|', 'tar', 't'], shell=True, report=False) + file_count = 0 + for filename in run.out.splitlines(): + 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') + return True diff --git a/test/test_enter.py b/test/test_enter.py new file mode 100644 index 0000000..202df32 --- /dev/null +++ b/test/test_enter.py @@ -0,0 +1,85 @@ +"""Test enter""" + +import os +import warnings +import pytest + + +@pytest.mark.parametrize( + 'shell, success', [ + ('delete', True), + ('', 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_y, paths, shell, success): + """Enter tests""" + env = os.environ.copy() + if shell == 'delete': + # remove shell + if 'SHELL' in env: + del env['SHELL'] + elif shell == 'noexec': + # specify a non-executable path + noexec = paths.root.join('noexec') + noexec.write('') + noexec.chmod(0o664) + env['SHELL'] = str(noexec) + else: + env['SHELL'] = shell + run = runner(command=yadm_y('enter'), env=env) + assert run.success == success + assert run.err == '' + prompt = f'yadm shell ({paths.repo})' + if success: + assert run.out.startswith('Entering yadm repo') + assert run.out.rstrip().endswith('Leaving yadm repo') + if shell == 'delete': + # When SHELL is empty (unlikely), it is attempted to be run anyway. + # This is a but which must be fixed. + warnings.warn('Unhandled bug: SHELL executed when empty', Warning) + else: + assert f'PROMPT={prompt}' in run.out + assert f'PS1={prompt}' in run.out + assert f'GIT_DIR={paths.repo}' in run.out + if not success: + assert 'does not refer to an executable' in run.out + if 'env' in shell: + assert f'GIT_DIR={paths.repo}' in run.out + assert 'PROMPT=yadm shell' in run.out + assert 'PS1=yadm shell' in run.out + + +@pytest.mark.parametrize( + 'shell, opts, path', [ + ('bash', '--norc', '\\w'), + ('csh', '-f', '%~'), + ('zsh', '-f', '%~'), + ], ids=[ + 'bash', + 'csh', + 'zsh', + ]) +@pytest.mark.usefixtures('ds1_copy') +def test_enter_shell_ops(runner, yadm_y, paths, shell, opts, path): + """Enter tests for specific shell options""" + + # Create custom shell to detect options passed + custom_shell = paths.root.join(shell) + custom_shell.write('#!/bin/sh\necho OPTS=$*\necho PROMPT=$PROMPT') + custom_shell.chmod(0o775) + + env = os.environ.copy() + env['SHELL'] = custom_shell + + run = runner(command=yadm_y('enter'), env=env) + assert run.success + assert run.err == '' + assert f'OPTS={opts}' in run.out + assert f'PROMPT=yadm shell ({paths.repo}) {path} >' in run.out diff --git a/test/test_git.py b/test/test_git.py new file mode 100644 index 0000000..427c54a --- /dev/null +++ b/test/test_git.py @@ -0,0 +1,58 @@ +"""Test git""" + +import re +import pytest + + +@pytest.mark.usefixtures('ds1_copy') +def test_git(runner, yadm_y, paths): + """Test series of passthrough git commands + + Passthru unknown commands to Git + Git command 'add' - badfile + Git command 'add' + Git command 'status' + Git command 'commit' + Git command 'log' + """ + + # passthru unknown commands to Git + run = runner(command=yadm_y('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 == '' + + # git command 'add' - badfile + run = runner(command=yadm_y('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 == '' + + # git command 'add' + newfile = paths.work.join('test_git') + newfile.write('test_git') + run = runner(command=yadm_y('add', '-v', str(newfile))) + assert run.success + assert run.err == '' + assert "add 'test_git'" in run.out + + # git command 'status' + run = runner(command=yadm_y('status')) + assert run.success + assert run.err == '' + assert re.search(r'new file:\s+test_git', run.out) + + # git command 'commit' + run = runner(command=yadm_y('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) + + # git command 'log' + run = runner(command=yadm_y('log', '--oneline')) + assert run.success + assert run.err == '' + assert 'Add test_git' in run.out diff --git a/test/test_help.py b/test/test_help.py new file mode 100644 index 0000000..79a7652 --- /dev/null +++ b/test/test_help.py @@ -0,0 +1,17 @@ +"""Test help""" + + +def test_missing_command(runner, yadm_y): + """Run without any command""" + run = runner(command=yadm_y()) + assert run.failure + assert run.err == '' + assert run.out.startswith('Usage: yadm') + + +def test_help_command(runner, yadm_y): + """Run with help command""" + run = runner(command=yadm_y('help')) + assert run.failure + assert run.err == '' + assert run.out.startswith('Usage: yadm') diff --git a/test/test_hooks.py b/test/test_hooks.py new file mode 100644 index 0000000..f1df91e --- /dev/null +++ b/test/test_hooks.py @@ -0,0 +1,90 @@ +"""Test hooks""" + +import pytest + + +@pytest.mark.parametrize( + 'pre, pre_code, post, post_code', [ + (False, 0, False, 0), + (True, 0, False, 0), + (True, 5, False, 0), + (False, 0, True, 0), + (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', + ]) +def test_hooks( + runner, yadm_y, paths, + pre, pre_code, post, post_code): + """Test pre/post hook""" + + # generate hooks + if pre: + create_hook(paths, 'pre_version', pre_code) + if post: + create_hook(paths, 'post_version', post_code) + + # run yadm + run = runner(yadm_y('version')) + # when a pre hook fails, yadm should exit with the hook's code + assert run.code == pre_code + assert run.err == '' + + if pre: + 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 + else: + # when a pre hook fails, yadm should not run the command + 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 + + +# repo fixture is needed to test the population of YADM_HOOK_WORK +@pytest.mark.usefixtures('ds1_repo_copy') +def test_hook_env(runner, yadm_y, paths): + """Test hook environment""" + + # test will be done with a non existent "git" passthru command + # which should exit with a failing code + cmd = 'passthrucmd' + + # write the hook + hook = paths.hooks.join(f'post_{cmd}') + hook.write('#!/bin/sh\nenv\n') + hook.chmod(0o755) + + run = runner(yadm_y(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_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 + + +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.chmod(0o755) diff --git a/test/test_init.py b/test/test_init.py new file mode 100644 index 0000000..1519b38 --- /dev/null +++ b/test/test_init.py @@ -0,0 +1,78 @@ +"""Test init""" + +import pytest + + +@pytest.mark.parametrize( + '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_y, paths, repo_config, alt_work, repo_present, force): + """Test init + + Repos should have attribs: + - 0600 permissions + - not bare + - worktree = $HOME + - showUntrackedFiles = no + - yadm.managed = true + """ + + # these tests will assume this for $HOME + home = str(paths.root.mkdir('HOME')) + + # ds1_work_copy comes WITH an empty repo dir present. + 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') + assert old_repo.isfile() + else: + paths.repo.remove() + + # command args + args = ['init'] + if alt_work: + args.extend(['-w', paths.work]) + if force: + args.append('-f') + + # run init + run = runner(yadm_y(*args), env={'HOME': home}) + assert run.err == '' + + if repo_present and not force: + assert run.failure + assert 'repo already exists' in run.out + assert old_repo.isfile(), 'Missing original repo' + else: + assert run.success + assert 'Initialized empty shared Git repository' in run.out + + if repo_present: + assert not old_repo.isfile(), 'Original repo still exists' + + if alt_work: + assert repo_config('core.worktree') == paths.work + else: + 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' diff --git a/test/test_introspect.py b/test/test_introspect.py new file mode 100644 index 0000000..9026e98 --- /dev/null +++ b/test/test_introspect.py @@ -0,0 +1,46 @@ +"""Test introspect""" + +import pytest + + +@pytest.mark.parametrize( + 'name', [ + '', + 'invalid', + 'commands', + 'configs', + 'repo', + 'switches', + ]) +def test_introspect_category( + runner, yadm_y, paths, name, + supported_commands, supported_configs, supported_switches): + """Validate introspection category""" + if name: + run = runner(command=yadm_y('introspect', name)) + else: + run = runner(command=yadm_y('introspect')) + + assert run.success + assert run.err == '' + + expected = [] + if name == 'commands': + expected = supported_commands + elif name == 'config': + expected = supported_configs + elif name == 'switches': + expected = supported_switches + + # assert values + if name in ('', 'invalid'): + assert run.out == '' + if name == 'repo': + assert run.out.rstrip() == paths.repo + + # make sure every expected value is present + for value in expected: + assert value in run.out + # make sure nothing extra is present + if expected: + assert len(run.out.split()) == len(expected) diff --git a/test/test_jinja.py b/test/test_jinja.py new file mode 100644 index 0000000..54d248a --- /dev/null +++ b/test/test_jinja.py @@ -0,0 +1,200 @@ +"""Test jinja""" + +import os +import re +import pytest +import utils + + +@pytest.fixture(scope='module') +def envtpl_present(runner): + """Is envtpl present and working?""" + try: + run = runner(command=['envtpl', '-h']) + if run.success: + return True + except OSError: + pass + return False + + +@pytest.mark.usefixtures('ds1_copy') +def test_local_override(runner, yadm_y, paths, + tst_distro, envtpl_present): + """Test local overrides""" + if not envtpl_present: + pytest.skip('Unable to test without envtpl.') + + # define local overrides + utils.set_local(paths, 'class', 'or-class') + utils.set_local(paths, 'hostname', 'or-hostname') + utils.set_local(paths, 'os', 'or-os') + utils.set_local(paths, 'user', 'or-user') + + template = ( + 'j2-{{ YADM_CLASS }}-' + '{{ YADM_OS }}-{{ YADM_HOSTNAME }}-' + '{{ YADM_USER }}-{{ YADM_DISTRO }}' + '-{%- ' + f"include '{utils.INCLUDE_FILE}'" + ' -%}' + ) + expected = ( + f'j2-or-class-or-os-or-hostname-or-user-{tst_distro}' + f'-{utils.INCLUDE_CONTENT}' + ) + + utils.create_alt_files(paths, '##yadm.j2', content=template, + includefile=True) + + # os.system(f'find {paths.work}' + ' -name *j2 -ls -exec cat \'{}\' ";"') + # os.system(f'find {paths.work}') + # run alt to trigger linking + run = runner(yadm_y('alt')) + assert run.success + assert run.err == '' + created = created_list(run.out) + + # assert the proper creation has occurred + for file_path in (utils.ALT_FILE1, utils.ALT_FILE2): + source_file = file_path + '##yadm.j2' + assert paths.work.join(file_path).isfile() + lines = paths.work.join(file_path).readlines(cr=False) + assert lines[0] == source_file + assert lines[1] == expected + assert str(paths.work.join(source_file)) in created + + +@pytest.mark.parametrize('autoalt', [None, 'true', 'false']) +@pytest.mark.usefixtures('ds1_copy') +def test_auto_alt(runner, yadm_y, paths, autoalt, tst_sys, + envtpl_present): + """Test setting auto-alt""" + + if not envtpl_present: + pytest.skip('Unable to test without envtpl.') + + # set the value of auto-alt + if autoalt: + os.system(' '.join(yadm_y('config', 'yadm.auto-alt', autoalt))) + + # create file + jinja_suffix = '##yadm.j2' + utils.create_alt_files(paths, jinja_suffix, content='{{ YADM_OS }}') + + # run status to possibly trigger linking + run = runner(yadm_y('status')) + assert run.success + assert run.err == '' + created = created_list(run.out) + + # assert the proper creation has occurred + for file_path in (utils.ALT_FILE1, utils.ALT_FILE2): + source_file = file_path + jinja_suffix + if autoalt == 'false': + assert not paths.work.join(file_path).exists() + else: + assert paths.work.join(file_path).isfile() + lines = paths.work.join(file_path).readlines(cr=False) + assert lines[0] == source_file + assert lines[1] == tst_sys + # no created output when run via auto-alt + assert str(paths.work.join(source_file)) not in created + + +@pytest.mark.usefixtures('ds1_copy') +def test_jinja_envtpl_missing(runner, paths): + """Test operation when envtpl is missing""" + + script = f""" + YADM_TEST=1 source {paths.pgm} + process_global_args -Y "{paths.yadm}" + set_operating_system + configure_paths + ENVTPL_PROGRAM='envtpl_missing' main alt + """ + + utils.create_alt_files(paths, '##yadm.j2') + + run = runner(command=['bash'], inp=script) + assert run.success + assert run.err == '' + assert f'envtpl not available, not creating' in run.out + + +@pytest.mark.parametrize( + 'tracked, encrypt, exclude', [ + (False, False, False), + (True, False, False), + (False, True, False), + (False, True, True), + ], ids=[ + 'untracked', + 'tracked', + 'encrypted', + 'excluded', + ]) +@pytest.mark.usefixtures('ds1_copy') +def test_jinja(runner, yadm_y, paths, + tst_sys, tst_host, tst_user, tst_distro, + tracked, encrypt, exclude, + envtpl_present): + """Test jinja processing""" + + if not envtpl_present: + pytest.skip('Unable to test without envtpl.') + + jinja_suffix = '##yadm.j2' + + # set the class + tst_class = 'testclass' + utils.set_local(paths, 'class', tst_class) + + template = ( + 'j2-{{ YADM_CLASS }}-' + '{{ YADM_OS }}-{{ YADM_HOSTNAME }}-' + '{{ YADM_USER }}-{{ YADM_DISTRO }}' + '-{%- ' + f"include '{utils.INCLUDE_FILE}'" + ' -%}' + ) + expected = ( + f'j2-{tst_class}-' + f'{tst_sys}-{tst_host}-' + f'{tst_user}-{tst_distro}' + f'-{utils.INCLUDE_CONTENT}' + ) + + utils.create_alt_files(paths, jinja_suffix, content=template, + tracked=tracked, encrypt=encrypt, exclude=exclude, + includefile=True) + + # run alt to trigger linking + run = runner(yadm_y('alt')) + assert run.success + assert run.err == '' + created = created_list(run.out) + + # assert the proper creation has occurred + for file_path in (utils.ALT_FILE1, utils.ALT_FILE2): + source_file = file_path + jinja_suffix + if tracked or (encrypt and not exclude): + assert paths.work.join(file_path).isfile() + lines = paths.work.join(file_path).readlines(cr=False) + assert lines[0] == source_file + assert lines[1] == expected + assert str(paths.work.join(source_file)) in created + else: + assert not paths.work.join(file_path).exists() + assert str(paths.work.join(source_file)) not in created + + +def created_list(output): + """Parse output, and return list of created files""" + + created = dict() + for line in output.splitlines(): + match = re.match('Creating (.+) from template (.+)$', line) + if match: + created[match.group(1)] = match.group(2) + return created.values() diff --git a/test/test_list.py b/test/test_list.py new file mode 100644 index 0000000..44a5573 --- /dev/null +++ b/test/test_list.py @@ -0,0 +1,47 @@ +"""Test list""" + +import os +import pytest + + +@pytest.mark.parametrize( + 'location', [ + 'work', + 'outside', + 'subdir', + ]) +@pytest.mark.usefixtures('ds1_copy') +def test_list(runner, yadm_y, paths, ds1, location): + """List tests""" + if location == 'work': + run_dir = paths.work + 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_y('list', '-a')) + assert run.success + assert run.err == '' + returned_files = set(run.out.splitlines()) + expected_files = set([e.path for e in ds1 if e.tracked]) + assert returned_files == expected_files + # test without '-a' + # 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_y('list')) + assert run.success + assert run.err == '' + returned_files = set(run.out.splitlines()) + if location == 'subdir': + basepath = os.path.basename(os.getcwd()) + # only expect files within the subdir + # names should be relative to subdir + expected_files = set( + [e.path[len(basepath)+1:] 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 new file mode 100644 index 0000000..eb7ad8f --- /dev/null +++ b/test/test_perms.py @@ -0,0 +1,111 @@ +"""Test perms""" + +import os +import warnings +import pytest + + +@pytest.mark.parametrize('autoperms', ['notest', 'unset', 'true', 'false']) +@pytest.mark.usefixtures('ds1_copy') +def test_perms(runner, yadm_y, paths, ds1, autoperms): + """Test perms""" + # set the value of auto-perms + if autoperms != 'notest': + if autoperms != 'unset': + os.system(' '.join(yadm_y('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(private.path) for private in ds1.private] + + # create an archive file + os.system(f'touch "{str(paths.archive)}"') + 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') + 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') + + cmd = 'perms' + if autoperms != 'notest': + cmd = 'status' + run = runner(yadm_y(cmd)) + assert run.success + assert run.err == '' + if cmd == 'perms': + assert run.out == '' + + # these paths should be secured if processing perms + for private in privatepaths: + if '.p2' in private.basename or '.p4' in private.basename: + # Dot files within .ssh/.gnupg are not protected. + # This is a but which must be fixed + warnings.warn('Unhandled bug: private dot files', Warning) + continue + 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') + + # these paths should never be secured + for private in insecurepaths: + 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') +def test_perms_control(runner, yadm_y, paths, ds1, sshperms, gpgperms): + """Test fine control of perms""" + # set the value of ssh-perms + if sshperms: + os.system(' '.join(yadm_y('config', 'yadm.ssh-perms', sshperms))) + + # set the value of gpg-perms + if gpgperms: + os.system(' '.join(yadm_y('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(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') + + run = runner(yadm_y('perms')) + assert run.success + assert run.err == '' + assert run.out == '' + + # these paths should be secured if processing perms + for private in privatepaths: + if '.p2' in private.basename or '.p4' in private.basename: + # Dot files within .ssh/.gnupg are not protected. + # This is a but which must be fixed + warnings.warn('Unhandled bug: private dot files', Warning) + continue + 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') diff --git a/test/test_syntax.py b/test/test_syntax.py new file mode 100644 index 0000000..5e39b3a --- /dev/null +++ b/test/test_syntax.py @@ -0,0 +1,52 @@ +"""Syntax checks""" + +import os +import pytest + + +def test_yadm_syntax(runner, yadm): + """Is syntactically valid""" + run = runner(command=['bash', '-n', yadm]) + assert run.success + + +def test_shellcheck(runner, yadm, shellcheck_version): + """Passes shellcheck""" + 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(runner, pylint_version): + """Passes pylint""" + 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) + assert run.success + + +def test_flake8(runner, flake8_version): + """Passes flake8""" + run = runner(command=['flake8', '--version'], report=False) + if not run.out.startswith(flake8_version): + pytest.skip('Unsupported flake8 version') + run = runner(command=['flake8', 'test']) + assert run.success + + +def test_yamllint(runner, yamllint_version): + """Passes yamllint""" + 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) + assert run.success diff --git a/test/test_unit_bootstrap_available.py b/test/test_unit_bootstrap_available.py new file mode 100644 index 0000000..f37ac08 --- /dev/null +++ b/test/test_unit_bootstrap_available.py @@ -0,0 +1,33 @@ +"""Unit tests: bootstrap_available""" + + +def test_bootstrap_missing(runner, paths): + """Test result of bootstrap_available, when bootstrap missing""" + run_test(runner, paths, False) + + +def test_bootstrap_no_exec(runner, paths): + """Test result of bootstrap_available, when bootstrap not executable""" + 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.chmod(0o775) + run_test(runner, paths, True) + + +def run_test(runner, paths, success): + """Run bootstrap_available, and test result""" + script = f""" + YADM_TEST=1 source {paths.pgm} + YADM_BOOTSTRAP='{paths.bootstrap}' + bootstrap_available + """ + run = runner(command=['bash'], inp=script) + assert run.success == success + assert run.err == '' + assert run.out == '' diff --git a/test/test_unit_configure_paths.py b/test/test_unit_configure_paths.py new file mode 100644 index 0000000..094ff6b --- /dev/null +++ b/test/test_unit_configure_paths.py @@ -0,0 +1,80 @@ +"""Unit tests: configure_paths""" + +import pytest + +ARCHIVE = 'files.gpg' +BOOTSTRAP = 'bootstrap' +CONFIG = 'config' +ENCRYPT = 'encrypt' +HOME = '/testhome' +REPO = 'repo.git' +YDIR = '.yadm' + + +@pytest.mark.parametrize( + 'override, expect', [ + (None, {}), + ('-Y', {}), + ('--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 repo', + 'override config', + 'override encrypt', + 'override archive', + 'override bootstrap', + ]) +def test_config(runner, paths, override, expect): + """Test configure_paths""" + opath = 'override' + matches = match_map() + args = [] + if override == '-Y': + matches = match_map('/' + opath) + + if override: + args = [override, '/' + opath] + for ekey in expect.keys(): + matches[ekey] = f'{expect[ekey]}="/{opath}"' + run_test( + runner, paths, + [override, opath], + ['must specify a fully qualified'], 1) + + run_test(runner, paths, args, matches.values(), 0) + + +def match_map(yadm_dir=None): + """Create a dictionary of matches, relative to yadm_dir""" + if not yadm_dir: + yadm_dir = '/'.join([HOME, YDIR]) + return { + 'yadm': f'YADM_DIR="{yadm_dir}"', + 'repo': f'YADM_REPO="{yadm_dir}/{REPO}"', + 'config': f'YADM_CONFIG="{yadm_dir}/{CONFIG}"', + 'encrypt': f'YADM_ENCRYPT="{yadm_dir}/{ENCRYPT}"', + 'archive': f'YADM_ARCHIVE="{yadm_dir}/{ARCHIVE}"', + 'bootstrap': f'YADM_BOOTSTRAP="{yadm_dir}/{BOOTSTRAP}"', + 'git': f'GIT_DIR="{yadm_dir}/{REPO}"', + } + + +def run_test(runner, paths, args, expected_matches, expected_code=0): + """Run proces global args, and run configure_paths""" + argstring = ' '.join(['"'+a+'"' for a in args]) + script = f""" + YADM_TEST=1 HOME="{HOME}" source {paths.pgm} + process_global_args {argstring} + configure_paths + declare -p | grep -E '(YADM|GIT)_' + """ + run = runner(command=['bash'], inp=script) + assert run.code == expected_code + assert run.err == '' + for match in expected_matches: + assert match in run.out diff --git a/test/test_unit_parse_encrypt.py b/test/test_unit_parse_encrypt.py new file mode 100644 index 0000000..7ab1d30 --- /dev/null +++ b/test/test_unit_parse_encrypt.py @@ -0,0 +1,179 @@ +"""Unit tests: parse_encrypt""" + +import pytest + + +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' + + +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') + + +@pytest.mark.parametrize( + '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' + else: + 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 == '' + + # validate parsing result + assert 'EIF_COUNT:0' in run.out, 'EIF should be empty' + + +@pytest.mark.usefixtures('ds1_repo_copy') +def test_file_parse_encrypt(runner, paths): + """Test parse_encrypt + + Test an array of supported features of the encrypt configuration. + """ + + edata = '' + expected = set() + + # empty line + edata += '\n' + + # simple comments + edata += '# a simple comment\n' + edata += ' # a comment with leading space\n' + + # unreferenced directory + 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') + + # 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') + + # 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') + + # hidden files + 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') + + # 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 += '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') + + # write encrypt file + 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 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 + + 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): + """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' + if skip_parse: + parse_cmd = '' + if twice: + parse_cmd = 'parse_encrypt; parse_encrypt' + script = f""" + YADM_TEST=1 source {paths.pgm} + YADM_ENCRYPT={paths.encrypt} + export YADM_ENCRYPT + GIT_DIR={paths.repo} + export GIT_DIR + {parse_cmd} + export ENCRYPT_INCLUDE_FILES + export PARSE_ENCRYPT_SHORT + env + echo EIF_COUNT:${{#ENCRYPT_INCLUDE_FILES[@]}} + for value in "${{ENCRYPT_INCLUDE_FILES[@]}}"; do + echo "EIF:$value" + done + """ + run = runner(command=['bash'], inp=script) + return run diff --git a/test/test_unit_query_distro.py b/test/test_unit_query_distro.py new file mode 100644 index 0000000..3c53c54 --- /dev/null +++ b/test/test_unit_query_distro.py @@ -0,0 +1,26 @@ +"""Unit tests: query_distro""" + + +def test_lsb_release_present(runner, yadm, tst_distro): + """Match lsb_release -si when present""" + script = f""" + YADM_TEST=1 source {yadm} + query_distro + """ + run = runner(command=['bash'], inp=script) + assert run.success + assert run.err == '' + assert run.out.rstrip() == tst_distro + + +def test_lsb_release_missing(runner, yadm): + """Empty value when missing""" + script = f""" + YADM_TEST=1 source {yadm} + LSB_RELEASE_PROGRAM="missing_lsb_release" + query_distro + """ + run = runner(command=['bash'], inp=script) + assert run.success + assert run.err == '' + assert run.out.rstrip() == '' diff --git a/test/test_unit_set_os.py b/test/test_unit_set_os.py new file mode 100644 index 0000000..d2f2a2a --- /dev/null +++ b/test/test_unit_set_os.py @@ -0,0 +1,36 @@ +"""Unit tests: set_operating_system""" + +import pytest + + +@pytest.mark.parametrize( + 'proc_value, expected_os', [ + ('missing', 'uname'), + ('has Microsoft inside', 'WSL'), + ('another value', 'uname'), + ], ids=[ + '/proc/version missing', + '/proc/version includes MS', + '/proc/version excludes MS', + ]) +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.write(proc_value) + script = f""" + YADM_TEST=1 source {paths.pgm} + PROC_VERSION={proc_version} + set_operating_system + echo $OPERATING_SYSTEM + """ + run = runner(command=['bash'], inp=script) + assert run.success + assert run.err == '' + if expected_os == 'uname': + expected_os = tst_sys + assert run.out.rstrip() == expected_os diff --git a/test/test_unit_x_program.py b/test/test_unit_x_program.py new file mode 100644 index 0000000..3233a3d --- /dev/null +++ b/test/test_unit_x_program.py @@ -0,0 +1,46 @@ +"""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_y, paths, program, executable, success, value, match): + """Set yadm.X-program, and test result of require_X""" + + # set configuration + if executable: + os.system(' '.join(yadm_y( + 'config', f'yadm.{program}-program', executable))) + + # test require_[git,gpg] + script = f""" + YADM_TEST=1 source {paths.pgm} + YADM_CONFIG="{paths.config}" + require_{program} + echo ${program.upper()}_PROGRAM + """ + run = runner(command=['bash'], inp=script) + assert run.success == success + assert run.err == '' + + # [GIT,GPG]_PROGRAM set correctly + if value == 'program': + assert run.out.rstrip() == program + elif value: + assert run.out.rstrip() == value + + # error reported about bad config + if match: + assert match in run.out diff --git a/test/test_version.py b/test/test_version.py new file mode 100644 index 0000000..023eb82 --- /dev/null +++ b/test/test_version.py @@ -0,0 +1,35 @@ +"""Test version""" + +import re +import pytest + + +@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()) + if yadm_version: + return yadm_version[0] + 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') + + +def test_reported_version( + runner, yadm_y, expected_version): + """Report correct version""" + run = runner(command=yadm_y('version')) + assert run.success + assert run.err == '' + assert run.out == f'yadm {expected_version}\n' diff --git a/test/utils.py b/test/utils.py new file mode 100644 index 0000000..0b52b37 --- /dev/null +++ b/test/utils.py @@ -0,0 +1,92 @@ +"""Testing Utilities + +This module holds values/functions common to multiple tests. +""" + +import os + +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' + +# 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' + + +def set_local(paths, variable, value): + """Set local override""" + os.system( + f'GIT_DIR={str(paths.repo)} ' + f'git config --local "local.{variable}" "{value}"' + ) + + +def create_alt_files(paths, suffix, + preserve=False, tracked=True, + encrypt=False, exclude=False, + content=None, includefile=False): + """Create new files, and add to the repo + + This is used for testing alternate files. In each case, a suffix is + appended to two standard file paths. Particulars of the file creation and + repo handling are dependent upon the function arguments. + """ + + if not preserve: + for remove_path in (ALT_FILE1, ALT_FILE2, ALT_DIR): + if paths.work.join(remove_path).exists(): + paths.work.join(remove_path).remove(rec=1, ignore_errors=True) + assert not paths.work.join(remove_path).exists() + + new_file1 = paths.work.join(ALT_FILE1 + suffix) + new_file1.write(ALT_FILE1 + suffix, ensure=True) + new_file2 = paths.work.join(ALT_FILE2 + suffix) + new_file2.write(ALT_FILE2 + suffix, ensure=True) + new_dir = paths.work.join(ALT_DIR + suffix).join(CONTAINED) + new_dir.write(ALT_DIR + suffix, ensure=True) + + # Do not test directory support for jinja alternates + test_paths = [new_file1, new_file2] + test_names = [ALT_FILE1, ALT_FILE2] + if suffix != '##yadm.j2': + 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) + assert test_path.exists() + + _create_includefiles(includefile, paths, test_paths) + _create_tracked(tracked, test_paths, paths) + _create_encrypt(encrypt, test_names, suffix, paths, exclude) + + +def _create_includefiles(includefile, paths, test_paths): + if includefile: + for dpath in INCLUDE_DIRS: + incfile = paths.work.join(dpath + '/' + INCLUDE_FILE) + incfile.write(INCLUDE_CONTENT, ensure=True) + test_paths += [incfile] + + +def _create_tracked(tracked, test_paths, paths): + if tracked: + for track_path in test_paths: + os.system(f'GIT_DIR={str(paths.repo)} git add "{track_path}"') + os.system(f'GIT_DIR={str(paths.repo)} git commit -m "Add test files"') + + +def _create_encrypt(encrypt, test_names, suffix, paths, exclude): + if encrypt: + for encrypt_name in test_names: + paths.encrypt.write(f'{encrypt_name + suffix}\n', mode='a') + if exclude: + paths.encrypt.write(f'!{encrypt_name + suffix}\n', mode='a') diff --git a/yadm b/yadm index c018aa9..e9c0a03 100755 --- a/yadm +++ b/yadm @@ -1,20 +1,21 @@ #!/bin/sh # yadm - Yet Another Dotfiles Manager -# Copyright (C) 2015-2017 Tim Byrne +# Copyright (C) 2015-2019 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 -# the Free Software Foundation, version 3 of the License. - +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. - +# # You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# along with this program. If not, see . -#; execute script with bash (shebang line is /bin/sh for portability) +# execute script with bash (shebang line is /bin/sh for portability) if [ -z "$BASH_VERSION" ]; then [ "$YADM_TEST" != 1 ] && exec bash "$0" "$@" fi @@ -43,34 +44,34 @@ OPERATING_SYSTEM="Unknown" ENCRYPT_INCLUDE_FILES="unparsed" -#; flag causing path translations with cygpath +# flag causing path translations with cygpath USE_CYGPATH=0 -#; flag when something may have changes (which prompts auto actions to be performed) +# flag when something may have changes (which prompts auto actions to be performed) CHANGES_POSSIBLE=0 -#; flag when a bootstrap should be performed after cloning -#; 0: skip auto_bootstrap, 1: ask, 2: perform bootstrap, 3: prevent bootstrap +# flag when a bootstrap should be performed after cloning +# 0: skip auto_bootstrap, 1: ask, 2: perform bootstrap, 3: prevent bootstrap DO_BOOTSTRAP=0 function main() { require_git - #; capture full command, for passing to hooks + # capture full command, for passing to hooks FULL_COMMAND="$*" - #; create the YADM_DIR if it doesn't exist yet + # create the YADM_DIR if it doesn't exist yet [ -d "$YADM_DIR" ] || mkdir -p "$YADM_DIR" - #; parse command line arguments + # parse command line arguments local retval=0 internal_commands="^(alt|bootstrap|clean|clone|config|decrypt|encrypt|enter|help|init|introspect|list|perms|version)$" if [ -z "$*" ] ; then - #; no argumnts will result in help() + # no argumnts will result in help() help elif [[ "$1" =~ $internal_commands ]] ; then - #; for internal commands, process all of the arguments + # for internal commands, process all of the arguments YADM_COMMAND="$1" YADM_ARGS=() shift @@ -78,26 +79,26 @@ function main() { while [[ $# -gt 0 ]] ; do key="$1" case $key in - -a) #; used by list() + -a) # used by list() LIST_ALL="YES" ;; - -d) #; used by all commands + -d) # used by all commands DEBUG="YES" ;; - -f) #; used by init() and clone() + -f) # used by init() and clone() FORCE="YES" ;; - -l) #; used by decrypt() + -l) # used by decrypt() DO_LIST="YES" ;; - -w) #; used by init() and clone() + -w) # used by init() and clone() if [[ ! "$2" =~ ^/ ]] ; then error_out "You must specify a fully qualified work tree" fi YADM_WORK="$2" shift ;; - *) #; any unhandled arguments + *) # any unhandled arguments YADM_ARGS+=("$1") ;; esac @@ -108,14 +109,14 @@ function main() { invoke_hook "pre" $YADM_COMMAND "${YADM_ARGS[@]}" else - #; any other commands are simply passed through to git + # any other commands are simply passed through to git HOOK_COMMAND="$1" invoke_hook "pre" git_command "$@" retval="$?" fi - #; process automatic events + # process automatic events auto_alt auto_perms auto_bootstrap @@ -124,7 +125,7 @@ function main() { } -#; ****** yadm Commands ****** +# ****** yadm Commands ****** function alt() { @@ -148,7 +149,7 @@ function alt() { local_host="$(config local.hostname)" if [ -z "$local_host" ] ; then local_host=$(hostname) - local_host=${local_host%%.*} #; trim any domain from hostname + local_host=${local_host%%.*} # trim any domain from hostname fi match_host="(%|$local_host)" @@ -158,16 +159,16 @@ function alt() { fi match_user="(%|$local_user)" - #; regex for matching "##CLASS.SYSTEM.HOSTNAME.USER" + # regex for matching "##CLASS.SYSTEM.HOSTNAME.USER" match1="^(.+)##(()|$match_system|$match_system\.$match_host|$match_system\.$match_host\.$match_user)$" match2="^(.+)##($match_class|$match_class\.$match_system|$match_class\.$match_system\.$match_host|$match_class\.$match_system\.$match_host\.$match_user)$" cd_work "Alternates" || return - #; only be noisy if the "alt" command was run directly + # only be noisy if the "alt" command was run directly [ "$YADM_COMMAND" = "alt" ] && loud="YES" - #; decide if a copy should be done instead of a symbolic link + # decide if a copy should be done instead of a symbolic link local do_copy=0 if [[ $OPERATING_SYSTEM == CYGWIN* ]] ; then if [[ $(config --bool yadm.cygwin-copy) == "true" ]] ; then @@ -175,42 +176,75 @@ function alt() { fi fi - #; loop over all "tracked" files - #; for every file which matches the above regex, create a symlink + # process the files tracked by yadm once, this info is used multiple times + tracked_files=() + local IFS=$'\n' + for tracked_file in $("$GIT_PROGRAM" ls-files | LC_ALL=C sort); do + tracked_files+=("$tracked_file") + done + + # generate a list of possible alt files + possible_alts=() + local IFS=$'\n' + for possible_alt in "${tracked_files[@]}" "${ENCRYPT_INCLUDE_FILES[@]}"; do + if [[ $possible_alt =~ .\#\#. ]]; then + possible_alts+=("$YADM_WORK/${possible_alt%##*}") + fi + done + alt_linked=() + + # loop over all "tracked" files + # for every file which matches the above regex, create a symlink for match in $match1 $match2; do last_linked='' local IFS=$'\n' - for tracked_file in $("$GIT_PROGRAM" ls-files | sort) "${ENCRYPT_INCLUDE_FILES[@]}"; do - tracked_file="$YADM_WORK/$tracked_file" - #; process both the path, and it's parent directory - for alt_path in "$tracked_file" "${tracked_file%/*}"; do - if [ -e "$alt_path" ] ; then - if [[ $alt_path =~ $match ]] ; then - if [ "$alt_path" != "$last_linked" ] ; then - new_link="${BASH_REMATCH[1]}" - debug "Linking $alt_path to $new_link" - [ -n "$loud" ] && echo "Linking $alt_path to $new_link" - if [ "$do_copy" -eq 1 ]; then - if [ -L "$new_link" ]; then - rm -f "$new_link" - fi - cp -f "$alt_path" "$new_link" - else - ln -nfs "$alt_path" "$new_link" + # the alt_paths looped over here are a unique sorted list of both files and their immediate parent directory + for alt_path in $(for tracked in "${tracked_files[@]}"; do printf "%s\n" "$tracked" "${tracked%/*}"; done | LC_ALL=C sort -u) "${ENCRYPT_INCLUDE_FILES[@]}"; do + alt_path="$YADM_WORK/$alt_path" + if [ -e "$alt_path" ] ; then + if [[ $alt_path =~ $match ]] ; then + if [ "$alt_path" != "$last_linked" ] ; then + new_link="${BASH_REMATCH[1]}" + debug "Linking $alt_path to $new_link" + [ -n "$loud" ] && echo "Linking $alt_path to $new_link" + if [ "$do_copy" -eq 1 ]; then + if [ -L "$new_link" ]; then + rm -f "$new_link" fi - last_linked="$alt_path" + cp -f "$alt_path" "$new_link" + else + ln -nfs "$alt_path" "$new_link" + alt_linked+=("$alt_path") fi + last_linked="$alt_path" fi fi - done + fi done done - #; loop over all "tracked" files - #; for every file which is a *##yadm.j2 create a real file - local IFS=$'\n' + # review alternate candidates for stale links + # if a possible alt IS linked, but it's target is not part of alt_linked, + # remove it. + if readlink_available; then + for stale_candidate in "${possible_alts[@]}"; do + if [ -L "$stale_candidate" ]; then + link_target=$(readlink "$stale_candidate" 2>/dev/null) + if [ -n "$link_target" ]; then + removal=yes + for review_link in "${alt_linked[@]}"; do + [ "$link_target" = "$review_link" ] && removal=no + done + [ "$removal" = "yes" ] && rm -f "$stale_candidate" + fi + fi + done + fi + + # loop over all "tracked" files + # for every file which is a *##yadm.j2 create a real file local match="^(.+)##yadm\\.j2$" - for tracked_file in $("$GIT_PROGRAM" ls-files | sort) "${ENCRYPT_INCLUDE_FILES[@]}"; do + for tracked_file in "${tracked_files[@]}" "${ENCRYPT_INCLUDE_FILES[@]}"; do tracked_file="$YADM_WORK/$tracked_file" if [ -e "$tracked_file" ] ; then if [[ $tracked_file =~ $match ]] ; then @@ -223,7 +257,7 @@ function alt() { YADM_HOSTNAME="$local_host" \ YADM_USER="$local_user" \ YADM_DISTRO=$(query_distro) \ - "$ENVTPL_PROGRAM" < "$tracked_file" > "$real_file" + "$ENVTPL_PROGRAM" --keep-template "$tracked_file" -o "$real_file" else debug "envtpl not available, not creating $real_file from template $tracked_file" [ -n "$loud" ] && echo "envtpl not available, not creating $real_file from template $tracked_file" @@ -260,13 +294,13 @@ function clone() { while [[ $# -gt 0 ]] ; do key="$1" case $key in - --bootstrap) #; force bootstrap, without prompt + --bootstrap) # force bootstrap, without prompt DO_BOOTSTRAP=2 ;; - --no-bootstrap) #; prevent bootstrap, without prompt + --no-bootstrap) # prevent bootstrap, without prompt DO_BOOTSTRAP=3 ;; - *) #; main arguments are kept intact + *) # main arguments are kept intact clone_args+=("$1") ;; esac @@ -275,18 +309,18 @@ function clone() { [ -n "$DEBUG" ] && display_private_perms "initial" - #; clone will begin with a bare repo + # clone will begin with a bare repo local empty= init $empty - #; add the specified remote, and configure the repo to track origin/master + # add the specified remote, and configure the repo to track origin/master debug "Adding remote to new repo" "$GIT_PROGRAM" remote add origin "${clone_args[@]}" debug "Configuring new repo to track origin/master" "$GIT_PROGRAM" config branch.master.remote origin "$GIT_PROGRAM" config branch.master.merge refs/heads/master - #; fetch / merge (and possibly fallback to reset) + # fetch / merge (and possibly fallback to reset) debug "Doing an initial fetch of the origin" "$GIT_PROGRAM" fetch origin || { debug "Removing repo after failed clone" @@ -326,7 +360,7 @@ function clone() { in another way. EOF else - #; skip auto_bootstrap if conflicts could not be stashed + # skip auto_bootstrap if conflicts could not be stashed DO_BOOTSTRAP=0 cat </dev/null) archive_regex="^\?\?" if [[ $archive_status =~ $archive_regex ]] ; then @@ -484,13 +518,13 @@ function git_command() { require_repo - #; translate 'gitconfig' to 'config' -- 'config' is reserved for yadm + # translate 'gitconfig' to 'config' -- 'config' is reserved for yadm if [ "$1" = "gitconfig" ] ; then set -- "config" "${@:2}" fi - #; ensure private .ssh and .gnupg directories exist first - #; TODO: consider restricting this to only commands which modify the work-tree + # ensure private .ssh and .gnupg directories exist first + # TODO: consider restricting this to only commands which modify the work-tree auto_private_dirs=$(config --bool yadm.auto-private-dirs) if [ "$auto_private_dirs" != "false" ] ; then @@ -499,7 +533,7 @@ function git_command() { CHANGES_POSSIBLE=1 - #; pass commands through to git + # pass commands through to git debug "Running git command $GIT_PROGRAM $*" "$GIT_PROGRAM" "$@" return "$?" @@ -543,17 +577,17 @@ EOF function init() { - #; safety check, don't attempt to init when the repo is already present + # safety check, don't attempt to init when the repo is already present [ -d "$YADM_REPO" ] && [ -z "$FORCE" ] && \ error_out "Git repo already exists. [$YADM_REPO]\nUse '-f' if you want to force it to be overwritten." - #; remove existing if forcing the init to happen anyway + # remove existing if forcing the init to happen anyway [ -d "$YADM_REPO" ] && { debug "Removing existing repo prior to init" rm -rf "$YADM_REPO" } - #; init a new bare repo + # init a new bare repo debug "Init new repo" "$GIT_PROGRAM" init --shared=0600 --bare "$(mixed_path "$YADM_REPO")" "$@" configure_repo @@ -628,12 +662,12 @@ function list() { require_repo - #; process relative to YADM_WORK when --all is specified + # process relative to YADM_WORK when --all is specified if [ -n "$LIST_ALL" ] ; then cd_work "List" || return fi - #; list tracked files + # list tracked files "$GIT_PROGRAM" ls-files } @@ -642,33 +676,33 @@ function perms() { parse_encrypt - #; TODO: prevent repeats in the files changed + # TODO: prevent repeats in the files changed cd_work "Perms" || return GLOBS=() - #; include the archive created by "encrypt" + # include the archive created by "encrypt" [ -f "$YADM_ARCHIVE" ] && GLOBS+=("$YADM_ARCHIVE") - #; include all .ssh files (unless disabled) + # include all .ssh files (unless disabled) if [[ $(config --bool yadm.ssh-perms) != "false" ]] ; then GLOBS+=(".ssh" ".ssh/*") fi - #; include all gpg files (unless disabled) + # include all gpg files (unless disabled) if [[ $(config --bool yadm.gpg-perms) != "false" ]] ; then GLOBS+=(".gnupg" ".gnupg/*") fi - #; include any files we encrypt + # include any files we encrypt GLOBS+=("${ENCRYPT_INCLUDE_FILES[@]}") - #; remove group/other permissions from collected globs + # remove group/other permissions from collected globs #shellcheck disable=SC2068 #(SC2068 is disabled because in this case, we desire globbing) chmod -f go-rwx ${GLOBS[@]} >/dev/null 2>&1 - #; TODO: detect and report changing permissions in a portable way + # TODO: detect and report changing permissions in a portable way } @@ -679,7 +713,7 @@ function version() { } -#; ****** Utility Functions ****** +# ****** Utility Functions ****** function query_distro() { distro="" @@ -691,54 +725,54 @@ function query_distro() { function process_global_args() { - #; global arguments are removed before the main processing is done + # global arguments are removed before the main processing is done MAIN_ARGS=() while [[ $# -gt 0 ]] ; do key="$1" case $key in - -Y|--yadm-dir) #; override the standard YADM_DIR + -Y|--yadm-dir) # override the standard YADM_DIR if [[ ! "$2" =~ ^/ ]] ; then error_out "You must specify a fully qualified yadm directory" fi YADM_DIR="$2" shift ;; - --yadm-repo) #; override the standard YADM_REPO + --yadm-repo) # override the standard YADM_REPO if [[ ! "$2" =~ ^/ ]] ; then error_out "You must specify a fully qualified repo path" fi YADM_OVERRIDE_REPO="$2" shift ;; - --yadm-config) #; override the standard YADM_CONFIG + --yadm-config) # override the standard YADM_CONFIG if [[ ! "$2" =~ ^/ ]] ; then error_out "You must specify a fully qualified config path" fi YADM_OVERRIDE_CONFIG="$2" shift ;; - --yadm-encrypt) #; override the standard YADM_ENCRYPT + --yadm-encrypt) # override the standard YADM_ENCRYPT if [[ ! "$2" =~ ^/ ]] ; then error_out "You must specify a fully qualified encrypt path" fi YADM_OVERRIDE_ENCRYPT="$2" shift ;; - --yadm-archive) #; override the standard YADM_ARCHIVE + --yadm-archive) # override the standard YADM_ARCHIVE if [[ ! "$2" =~ ^/ ]] ; then error_out "You must specify a fully qualified archive path" fi YADM_OVERRIDE_ARCHIVE="$2" shift ;; - --yadm-bootstrap) #; override the standard YADM_BOOTSTRAP + --yadm-bootstrap) # override the standard YADM_BOOTSTRAP if [[ ! "$2" =~ ^/ ]] ; then error_out "You must specify a fully qualified bootstrap path" fi YADM_OVERRIDE_BOOTSTRAP="$2" shift ;; - *) #; main arguments are kept intact + *) # main arguments are kept intact MAIN_ARGS+=("$1") ;; esac @@ -749,14 +783,14 @@ function process_global_args() { function configure_paths() { - #; change all paths to be relative to YADM_DIR + # change all paths to be relative to YADM_DIR YADM_REPO="$YADM_DIR/$YADM_REPO" YADM_CONFIG="$YADM_DIR/$YADM_CONFIG" YADM_ENCRYPT="$YADM_DIR/$YADM_ENCRYPT" YADM_ARCHIVE="$YADM_DIR/$YADM_ARCHIVE" YADM_BOOTSTRAP="$YADM_DIR/$YADM_BOOTSTRAP" - #; independent overrides for paths + # independent overrides for paths if [ -n "$YADM_OVERRIDE_REPO" ]; then YADM_REPO="$YADM_OVERRIDE_REPO" fi @@ -773,7 +807,7 @@ function configure_paths() { YADM_BOOTSTRAP="$YADM_OVERRIDE_BOOTSTRAP" fi - #; use the yadm repo for all git operations + # use the yadm repo for all git operations GIT_DIR=$(mixed_path "$YADM_REPO") export GIT_DIR @@ -783,23 +817,23 @@ function configure_repo() { debug "Configuring new repo" - #; change bare to false (there is a working directory) + # change bare to false (there is a working directory) "$GIT_PROGRAM" config core.bare 'false' - #; set the worktree for the yadm repo + # set the worktree for the yadm repo "$GIT_PROGRAM" config core.worktree "$(mixed_path "$YADM_WORK")" - #; by default, do not show untracked files and directories + # by default, do not show untracked files and directories "$GIT_PROGRAM" config status.showUntrackedFiles no - #; possibly used later to ensure we're working on the yadm repo + # possibly used later to ensure we're working on the yadm repo "$GIT_PROGRAM" config yadm.managed 'true' } function set_operating_system() { - #; special detection of WSL (windows subsystem for linux) + # special detection of WSL (windows subsystem for linux) local proc_version proc_version=$(cat "$PROC_VERSION" 2>/dev/null) if [[ "$proc_version" =~ Microsoft ]]; then @@ -850,7 +884,7 @@ function invoke_hook() { if [ -x "$hook_command" ] ; then debug "Invoking hook: $hook_command" - #; expose some internal data to all hooks + # expose some internal data to all hooks work=$(unix_path "$("$GIT_PROGRAM" config core.worktree)") YADM_HOOK_COMMAND=$HOOK_COMMAND YADM_HOOK_EXIT=$exit_status @@ -866,7 +900,7 @@ function invoke_hook() { "$hook_command" hook_status=$? - #; failing "pre" hooks will prevent commands from being run + # failing "pre" hooks will prevent commands from being run if [ "$mode" = "pre" ] && [ "$hook_status" -ne 0 ]; then echo "Hook $hook_command was not successful" echo "$HOOK_COMMAND will not be run" @@ -924,7 +958,7 @@ function parse_encrypt() { shopt_dotglob="$(shopt -p dotglob)" shopt -s dotglob - #; parse both included/excluded + # parse both included/excluded while IFS='' read -r line || [ -n "$line" ]; do if [[ ! $line =~ ^# && ! $line =~ ^[[:space:]]*$ ]] ; then local IFS=$'\n' @@ -948,7 +982,7 @@ function parse_encrypt() { eval "$shopt_dotglob" - #; remove excludes from the includes + # remove excludes from the includes #(SC2068 is disabled because in this case, we desire globbing) FINAL_INCLUDE=() #shellcheck disable=SC2068 @@ -960,16 +994,20 @@ function parse_encrypt() { done [ -n "$skip" ] || FINAL_INCLUDE+=("$included") done - ENCRYPT_INCLUDE_FILES=("${FINAL_INCLUDE[@]}") + + # sort the encrypted files + #shellcheck disable=SC2207 + IFS=$'\n' ENCRYPT_INCLUDE_FILES=($(LC_ALL=C sort <<<"${FINAL_INCLUDE[*]}")) + unset IFS fi } -#; ****** Auto Functions ****** +# ****** Auto Functions ****** function auto_alt() { - #; process alternates if there are possible changes + # process alternates if there are possible changes if [ "$CHANGES_POSSIBLE" = "1" ] ; then auto_alt=$(config --bool yadm.auto-alt) if [ "$auto_alt" != "false" ] ; then @@ -981,7 +1019,7 @@ function auto_alt() { function auto_perms() { - #; process permissions if there are possible changes + # process permissions if there are possible changes if [ "$CHANGES_POSSIBLE" = "1" ] ; then auto_perms=$(config --bool yadm.auto-perms) if [ "$auto_perms" != "false" ] ; then @@ -1010,7 +1048,7 @@ function auto_bootstrap() { } -#; ****** Prerequisites Functions ****** +# ****** Prerequisites Functions ****** function require_archive() { [ -f "$YADM_ARCHIVE" ] || error_out "$YADM_ARCHIVE does not exist. did you forget to create it?" @@ -1060,11 +1098,15 @@ function envtpl_available() { command -v "$ENVTPL_PROGRAM" >/dev/null 2>&1 && return return 1 } +function readlink_available() { + command -v "readlink" >/dev/null 2>&1 && return + return 1 +} -#; ****** Directory tranlations ****** +# ****** Directory tranlations ****** function unix_path() { - #; for paths used by bash/yadm + # for paths used by bash/yadm if [ "$USE_CYGPATH" = "1" ] ; then cygpath -u "$1" else @@ -1072,7 +1114,7 @@ function unix_path() { fi } function mixed_path() { - #; for paths used by Git + # for paths used by Git if [ "$USE_CYGPATH" = "1" ] ; then cygpath -m "$1" else @@ -1080,7 +1122,7 @@ function mixed_path() { fi } -#; ****** echo replacements ****** +# ****** echo replacements ****** function echo() { IFS=' ' printf '%s\n' "$*" @@ -1094,7 +1136,7 @@ function echo_e() { printf '%b\n' "$*" } -#; ****** Main processing (when not unit testing) ****** +# ****** Main processing (when not unit testing) ****** if [ "$YADM_TEST" != 1 ] ; then process_global_args "$@"