diff --git a/.circleci/config.yml b/.circleci/config.yml index 90cb547..6ecc9ae 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,6 +13,12 @@ jobs: . venv/bin/activate pip install --requirement dev_requirements.txt ./dev-scripts/build + - run: + name: install coveralls and upload coverage information + command: | + . venv/bin/activate + pip install coveralls==3.0.1 + coveralls install: docker: - image: circleci/python:3.7.3 diff --git a/README.md b/README.md index 9b17ed6..28a6243 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pyrestic -[![CircleCI](https://circleci.com/gh/mtlynch/pyrestic.svg?style=svg)](https://circleci.com/gh/mtlynch/pyrestic) [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](LICENSE) +[![CircleCI](https://circleci.com/gh/mtlynch/pyrestic.svg?style=svg)](https://circleci.com/gh/mtlynch/pyrestic) [![Coverage Status](https://coveralls.io/repos/github/mtlynch/pyrestic/badge.svg?branch=master)](https://coveralls.io/github/mtlynch/pyrestic?branch=master) [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](LICENSE) ## Overview diff --git a/dev-scripts/build b/dev-scripts/build index b13f5a1..59569ed 100755 --- a/dev-scripts/build +++ b/dev-scripts/build @@ -22,6 +22,13 @@ find . \ -not -path "./${VIRTUALENV_DIR}/*" \ -delete +# Run unit tests and calculate code coverage. +coverage run \ + --source "$SOURCE_DIR" \ + --omit "*_test.py" \ + -m unittest discover --pattern "*_test.py" +coverage report + # Check that source has correct formatting. yapf \ --diff \ diff --git a/dev_requirements.txt b/dev_requirements.txt index 631f5ad..6051c6f 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,3 +1,4 @@ +coverage==5.5 isort[requirements_deprecated_finder]==5.7.0 pyflakes==2.2.0 yapf==0.31.0 diff --git a/restic/__init__.py b/restic/__init__.py new file mode 100644 index 0000000..bfb5871 --- /dev/null +++ b/restic/__init__.py @@ -0,0 +1,10 @@ +from restic.internal import self_update as internal_self_update +from restic.internal import version as internal_version + + +def self_update(): + return internal_self_update.run() + + +def version(): + return internal_version.run() diff --git a/restic/core.py b/restic/core.py deleted file mode 100644 index e6bc37d..0000000 --- a/restic/core.py +++ /dev/null @@ -1,86 +0,0 @@ -import re -import subprocess - -from restic.config import restic_bin - - -def run_restic(cmd): - out = '' - err = '' - try: - with subprocess.Popen(cmd, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - encoding='utf-8', - text=True) as proc: - out, err = proc.communicate() - if err is not None: - raise RuntimeError('Command runtime failure') - proc.wait() - if proc.returncode != 0: - raise RuntimeError(f'Return code {proc.returncode} is not zero') - except FileNotFoundError: - raise RuntimeError('Cannot find restic installed') - - return out - - -def version(): - cmd = [restic_bin, 'version'] - out = run_restic(cmd) - if out is None: - return None - matchObj = re.match( - r'restic ([0-9\.]+) compiled with go([0-9\.]+) on ([a-zA-Z0-9]+)/([a-zA-Z0-9]+)', - out) - return { - 'restic_version': matchObj.group(1), - 'go_version': matchObj.group(2), - 'platform_version': matchObj.group(3), - 'Architecture': matchObj.group(4) - } - - -def self_update(): - cmd = [restic_bin, 'self-update'] - run_restic(cmd) - - -def generate(bash_completion=None, man=None, zsh_completion=None): - cmd = [restic_bin, 'generate'] - if bash_completion is not None: - if type(bash_completion) == str: - cmd.extend(['--bash-completion', bash_completion]) - else: - raise ValueError('bash-completion shall be type of str or None') - - if man is not None: - if type(man) == str: - cmd.extend(['--man', man]) - else: - raise ValueError('man shall be type of str or None') - - if zsh_completion is not None: - if type(zsh_completion) == str: - cmd.extend(['--zsh-completion', zsh_completion]) - else: - raise ValueError('zsh-completion shall be type of str or None') - - run_restic(cmd) - - -''' -TODO: if there is a repository, it needs pasword. -''' - - -def is_initialized(path): - cmd = ['restic', '-r', path, 'snapshots'] - ret = run_restic(cmd) - lines = ret.splitlines() - for each_line in lines: - if each_line.strip( - ) == 'Is there a repository at the following location?': - return False - - return True diff --git a/restic/generate.py b/restic/generate.py new file mode 100644 index 0000000..2dbeda9 --- /dev/null +++ b/restic/generate.py @@ -0,0 +1,25 @@ +from restic.config import restic_bin +from restic.internal import command_executor + + +def generate(bash_completion=None, man=None, zsh_completion=None): + cmd = [restic_bin, 'generate'] + if bash_completion is not None: + if type(bash_completion) == str: + cmd.extend(['--bash-completion', bash_completion]) + else: + raise ValueError('bash-completion shall be type of str or None') + + if man is not None: + if type(man) == str: + cmd.extend(['--man', man]) + else: + raise ValueError('man shall be type of str or None') + + if zsh_completion is not None: + if type(zsh_completion) == str: + cmd.extend(['--zsh-completion', zsh_completion]) + else: + raise ValueError('zsh-completion shall be type of str or None') + + command_executor.execute(cmd) diff --git a/restic/internal/__init__.py b/restic/internal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/restic/internal/command_executor.py b/restic/internal/command_executor.py new file mode 100644 index 0000000..60b95a3 --- /dev/null +++ b/restic/internal/command_executor.py @@ -0,0 +1,22 @@ +import subprocess + + +def execute(cmd): + out = '' + err = '' + try: + with subprocess.Popen(cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + encoding='utf-8', + text=True) as proc: + out, err = proc.communicate() + if err is not None: + raise RuntimeError('Command runtime failure') + proc.wait() + if proc.returncode != 0: + raise RuntimeError(f'Return code {proc.returncode} is not zero') + except FileNotFoundError: + raise RuntimeError('Cannot find restic installed') + + return out diff --git a/restic/internal/self_update.py b/restic/internal/self_update.py new file mode 100644 index 0000000..6cfba36 --- /dev/null +++ b/restic/internal/self_update.py @@ -0,0 +1,7 @@ +from restic.config import restic_bin +from restic.internal import command_executor + + +def run(): + cmd = [restic_bin, 'self-update'] + command_executor.execute(cmd) diff --git a/restic/internal/self_update_test.py b/restic/internal/self_update_test.py new file mode 100644 index 0000000..e5f7ca3 --- /dev/null +++ b/restic/internal/self_update_test.py @@ -0,0 +1,13 @@ +import unittest +from unittest import mock + +import restic +from restic.internal import self_update + + +class SelfUpdateTest(unittest.TestCase): + + @mock.patch.object(self_update.command_executor, 'execute') + def test_self_update(self, mock_execute): + restic.self_update() + mock_execute.assert_called_with(['restic', 'self-update']) diff --git a/restic/internal/version.py b/restic/internal/version.py new file mode 100644 index 0000000..f95d580 --- /dev/null +++ b/restic/internal/version.py @@ -0,0 +1,18 @@ +import re + +from restic import config +from restic.internal import command_executor + + +def run(): + cmd = [config.restic_bin, 'version'] + out = command_executor.execute(cmd) + match = re.match( + r'restic ([0-9\.]+) compiled with go([0-9\.]+) on ([a-zA-Z0-9]+)/([a-zA-Z0-9]+)', + out) + return { + 'restic_version': match.group(1), + 'go_version': match.group(2), + 'platform_version': match.group(3), + 'architecture': match.group(4) + } diff --git a/restic/internal/version_test.py b/restic/internal/version_test.py new file mode 100644 index 0000000..6d578b5 --- /dev/null +++ b/restic/internal/version_test.py @@ -0,0 +1,23 @@ +import unittest +from unittest import mock + +import restic +from restic.internal import version + + +class VersionTest(unittest.TestCase): + + @mock.patch.object(version.command_executor, 'execute') + def test_version(self, mock_execute): + mock_execute.return_value = ( + 'restic 0.12.0 compiled with go1.15.8 on windows/amd64') + + self.assertEqual( + { + 'architecture': 'amd64', + 'go_version': '1.15.8', + 'platform_version': 'windows', + 'restic_version': '0.12.0' + }, restic.version()) + + mock_execute.assert_called_with(['restic', 'version'])