mirror of
https://github.com/jongracecox/anybadge.git
synced 2025-07-21 04:11:05 +02:00
Add package testing to CI (#68)
* Make `build_examples.py` callable from python * Allow tests to run from outside project directory * Add invoke tasks * Add server tests * Run travis tests against wheel package instead of local code * Update `badge.write_badge()` to support `pathlib.Path` * Update `CONTRIBUTING.md`
This commit is contained in:
@@ -18,7 +18,9 @@ script:
|
||||
pre-commit install &&
|
||||
pre-commit run --all;
|
||||
fi
|
||||
- pytest --doctest-modules --cov=anybadge --cov-report html:htmlcov anybadge tests
|
||||
- python setup.py bdist_wheel && pip install dist/anybadge*.whl
|
||||
- mkdir tmp && cd tmp
|
||||
- pytest --doctest-modules --cov=anybadge --cov-report html:htmlcov ../anybadge ../tests
|
||||
before_deploy:
|
||||
- sed -i "s/^version = .*/version = __version__ = \"$TRAVIS_TAG\"/" anybadge/__init__.py
|
||||
deploy:
|
||||
|
103
CONTRIBUTING.md
103
CONTRIBUTING.md
@@ -1,4 +1,5 @@
|
||||
# Contributing to anybadge
|
||||
|
||||
I love your input! I want to make contributing to this project as easy and transparent as possible, whether it's:
|
||||
|
||||
- Reporting a bug
|
||||
@@ -8,6 +9,7 @@ I love your input! I want to make contributing to this project as easy and trans
|
||||
- Becoming a maintainer
|
||||
|
||||
## I use [Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow), so all code changes happen through pull requests
|
||||
|
||||
Pull requests are the best way to propose changes to the codebase (I use
|
||||
[Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow)). I actively welcome your pull requests:
|
||||
|
||||
@@ -19,15 +21,18 @@ Pull requests are the best way to propose changes to the codebase (I use
|
||||
6. Issue that pull request!
|
||||
|
||||
## Any contributions you make will be under the MIT Software License
|
||||
|
||||
When you submit code changes, your submissions are understood to be under the same
|
||||
[MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers
|
||||
if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/jongracecox/anybadge/issues)
|
||||
|
||||
I use GitHub issues to track public bugs. Report a bug by
|
||||
[opening a new issue](https://github.com/jongracecox/anybadge/issues/new/choose).
|
||||
|
||||
## Write bug reports with detail, background, and sample code
|
||||
|
||||
**Great Bug Reports** tend to have:
|
||||
|
||||
- A quick summary and/or background
|
||||
@@ -41,14 +46,41 @@ I use GitHub issues to track public bugs. Report a bug by
|
||||
People *love* thorough bug reports.
|
||||
|
||||
## Use a Consistent Coding Style
|
||||
Please follow the existing coding style.
|
||||
|
||||
Please follow the existing coding style. Your code should be standardised using
|
||||
[Python Black](https://github.com/psf/black) using pre-commit when you make commits - please ensure you have
|
||||
pre-commit installed (see [here](#install-pre-commit)).
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its MIT License.
|
||||
|
||||
# Technical stuff
|
||||
# Development environment
|
||||
|
||||
Setup your development environment with the following steps:
|
||||
|
||||
- [Check out the project](#check-out-the-project)
|
||||
- [Install build requirements](#install-build-requirements)
|
||||
- [Install pre-commit](#install-pre-commit)
|
||||
|
||||
## Check out the project
|
||||
|
||||
Clone the project:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/jongracecox/anybadge.git
|
||||
```
|
||||
|
||||
## Install build requirements
|
||||
|
||||
Install build requirements with:
|
||||
|
||||
```bash
|
||||
pip install -r build-requirements.txt
|
||||
```
|
||||
|
||||
## Install pre-commit
|
||||
|
||||
## Pre-commit
|
||||
This projects makes use of [pre-commit](https://pre-commit.com) to add some safety checks and create consistency
|
||||
in the project code. When committing changes to this project, please first [install pre-commit](https://pre-commit.com/#install),
|
||||
then activate it for this project:
|
||||
@@ -84,17 +116,58 @@ Fixing examples/color_teal.svg
|
||||
|
||||
This shows that two files were updated by hooks, and need to be re-added (with `git add`) before trying to commit again.
|
||||
|
||||
## Documentation
|
||||
The `README.md` file contains a table showing example badges for the different built-in colors. If you modify the
|
||||
appearance of badges, or the available colors please update the table using the following code:
|
||||
# Development activities
|
||||
|
||||
```python
|
||||
import anybadge
|
||||
print("""| Color Name | Hex Code | Example |
|
||||
| ---------- | -------- | ------- |""")
|
||||
for color in sorted([c for c in anybadge.colors.Color], key=lambda x: x.name):
|
||||
file = 'examples/color_' + color.name.lower() + '.svg'
|
||||
url = 'https://cdn.rawgit.com/jongracecox/anybadge/master/' + file
|
||||
anybadge.Badge(label='Color', value=color, default_color=color.name.lower()).write_badge(file, overwrite=True)
|
||||
print("| {color} | {hex} |  |".format(color=color.name.lower(), hex=color.value.upper(), url=url))
|
||||
## Invoke
|
||||
|
||||
The project has some [Python invoke](https://www.pyinvoke.org/) tasks to help automate things. After installing
|
||||
build requirements you can run `inv --list` to see a list of available tasks.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
> inv --list
|
||||
Available tasks:
|
||||
|
||||
build Build the package.
|
||||
clean Clean up the project area.
|
||||
examples Generate examples markdown.
|
||||
server.docker-build
|
||||
server.docker-run
|
||||
server.run
|
||||
test.docker Run dockerised tests.
|
||||
test.local Run local tests.
|
||||
```
|
||||
|
||||
You can get help for a command using `inv --help <command>`.
|
||||
|
||||
Invoke tasks are defined in the `tasks/` directory in the project. Feel free to add new and useful tasks.
|
||||
|
||||
## Running tests
|
||||
|
||||
You can run tests locally using:
|
||||
|
||||
```bash
|
||||
inv test.local
|
||||
```
|
||||
|
||||
When running locally, you will be running tests against the code in the project. This has some disadvantages,
|
||||
specifically running locally may not detect files that are not included in the package build, e.g. sub-modules,
|
||||
templates, examples, etc. For this reason we have a containerised test. This can be run using:
|
||||
|
||||
```bash
|
||||
inv test.docker
|
||||
```
|
||||
|
||||
This will clean up the project `dist` directory, build the package locally, build the docker image,
|
||||
spin up a docker container, install the package and run the tests. The tests should run using the installed
|
||||
package and not the project source code, so this method should be used as a final test before pushing.
|
||||
|
||||
## Documentation
|
||||
|
||||
The `README.md` file contains a table showing example badges for the different built-in colors. If you modify the
|
||||
appearance of badges, or the available colors please update the table using the invoke task:
|
||||
|
||||
```bash
|
||||
inv examples
|
||||
```
|
||||
|
11
Dockerfile
11
Dockerfile
@@ -1,12 +1,15 @@
|
||||
FROM python:3-stretch
|
||||
FROM python:3-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update
|
||||
RUN apk update
|
||||
|
||||
COPY anybadge.py anybadge_server.py ./
|
||||
RUN pip install -U pip && pip install packaging
|
||||
|
||||
ENTRYPOINT ./anybadge_server.py
|
||||
COPY anybadge/ /app/anybadge/
|
||||
COPY anybadge_server.py /app/.
|
||||
|
||||
ENTRYPOINT ["./anybadge_server.py"]
|
||||
|
||||
# Example command to run Docker container
|
||||
# docker run -it --rm -p8000:8000 -e ANYBADGE_LISTEN_ADDRESS="" -e ANYBADGE_LOG_LEVEL=DEBUG labmonkey/anybadge:1.0
|
||||
|
@@ -746,21 +746,28 @@ class Badge:
|
||||
(color, ", ".join(list(Color.__members__.keys()))),
|
||||
)
|
||||
|
||||
def write_badge(self, file_path, overwrite=False) -> None:
|
||||
def write_badge(self, file_path: Union[str, Path], overwrite=False) -> None:
|
||||
"""Write badge to file."""
|
||||
|
||||
if isinstance(file_path, str):
|
||||
|
||||
if file_path.endswith("/"):
|
||||
raise ValueError("File location may not be a directory.")
|
||||
|
||||
file: Path = Path(file_path)
|
||||
else:
|
||||
file = file_path
|
||||
|
||||
# Validate path (part 1)
|
||||
if file_path.endswith("/"):
|
||||
if file.is_dir():
|
||||
raise ValueError("File location may not be a directory.")
|
||||
|
||||
# Get absolute filepath
|
||||
path = os.path.abspath(file_path)
|
||||
if not path.lower().endswith(".svg"):
|
||||
path += ".svg"
|
||||
# Ensure we're using a .svg extension
|
||||
file = file.with_suffix(".svg")
|
||||
|
||||
# Validate path (part 2)
|
||||
if not overwrite and os.path.exists(path):
|
||||
raise RuntimeError('File "{}" already exists.'.format(path))
|
||||
if not overwrite and file.exists():
|
||||
raise RuntimeError('File "{}" already exists.'.format(file))
|
||||
|
||||
with open(path, mode="w") as file_handle:
|
||||
with open(file, mode="w") as file_handle:
|
||||
file_handle.write(self.badge_svg_text)
|
||||
|
7
anybadge_server.py
Executable file
7
anybadge_server.py
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
if __name__ == "__main__":
|
||||
from anybadge.server.cli import main
|
||||
import sys
|
||||
|
||||
print(sys.argv)
|
||||
main()
|
@@ -1,3 +1,5 @@
|
||||
invoke
|
||||
pygments
|
||||
pytest
|
||||
pytest-cov
|
||||
requests
|
||||
|
@@ -1,14 +1,13 @@
|
||||
import anybadge
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
def main():
|
||||
print(
|
||||
"""
|
||||
| Color Name | Hex | Example |
|
||||
| ------------- | ------- | ------- |"""
|
||||
)
|
||||
for color in sorted(anybadge.colors.Color):
|
||||
|
||||
file = "examples/color_" + color.name.lower() + ".svg"
|
||||
|
||||
url = "https://cdn.rawgit.com/jongracecox/anybadge/master/" + file
|
||||
@@ -20,3 +19,7 @@ if __name__ == "__main__":
|
||||
print(
|
||||
f"| {color.name.lower():<13} | {color.value.upper():<7} | ':<84}|"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
8
docker/test/Dockerfile
Normal file
8
docker/test/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM python:3.10.0
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
RUN apt update && pip install -U pip
|
||||
COPY requirements.txt ./
|
||||
RUN pip install -r ./requirements.txt
|
||||
COPY run_docker_tests.sh ./
|
4
docker/test/requirements.txt
Normal file
4
docker/test/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
packaging
|
||||
pytest
|
||||
pytest-cov
|
||||
requests
|
7
docker/test/run_docker_tests.sh
Executable file
7
docker/test/run_docker_tests.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Running tests..."
|
||||
mkdir tmp && cd tmp
|
||||
mkdir tests
|
||||
pip install /app/dist/anybadge*.whl
|
||||
pytest --doctest-modules --cov=anybadge --cov-report html:htmlcov /app/anybadge /app/tests
|
47
tasks/__init__.py
Normal file
47
tasks/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Invoke tasks for the project."""
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from invoke import task, Collection
|
||||
from tasks import test, server
|
||||
|
||||
PROJECT_DIR = Path(__file__).parent.parent
|
||||
|
||||
os.chdir(PROJECT_DIR)
|
||||
|
||||
|
||||
@task
|
||||
def build(c):
|
||||
"""Build the package."""
|
||||
print("Building package...")
|
||||
subprocess.run(["python", "setup.py", "bdist_wheel"])
|
||||
|
||||
|
||||
@task
|
||||
def examples(c):
|
||||
"""Generate examples markdown."""
|
||||
print("Generating examples markdown...")
|
||||
from build_examples import main
|
||||
|
||||
main()
|
||||
|
||||
|
||||
def delete_files(files: str):
|
||||
for file in glob.glob(files):
|
||||
print(f" Deleting {file}")
|
||||
subprocess.run(["rm", "-rf", file])
|
||||
|
||||
|
||||
@task()
|
||||
def clean(c):
|
||||
"""Clean up the project area."""
|
||||
print("Cleaning the project directory...")
|
||||
delete_files("dist/*")
|
||||
delete_files("tests/test_*.svg")
|
||||
|
||||
|
||||
namespace = Collection(test, server)
|
||||
for fn in [build, examples, clean]:
|
||||
namespace.add_task(fn)
|
24
tasks/server.py
Normal file
24
tasks/server.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import subprocess
|
||||
|
||||
from invoke import task
|
||||
|
||||
|
||||
@task
|
||||
def docker_build(c):
|
||||
print("Building Docker image...")
|
||||
subprocess.run("docker build . -t anybadge:latest", shell=True)
|
||||
|
||||
|
||||
@task
|
||||
def docker_run(c, port=8000):
|
||||
print("Running server in Docker container...")
|
||||
subprocess.run(
|
||||
f"docker run -it --rm -p{port}:{port}/tcp anybadge:latest --port={port}",
|
||||
shell=True,
|
||||
)
|
||||
|
||||
|
||||
@task
|
||||
def run(c, port=8000):
|
||||
print("Running server locally...")
|
||||
subprocess.run(f"python3 anybadge_server.py --port={port}", shell=True)
|
32
tasks/test.py
Normal file
32
tasks/test.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from invoke import task
|
||||
|
||||
PROJECT_DIR = Path(__file__).parent.parent
|
||||
|
||||
|
||||
@task
|
||||
def local(c):
|
||||
"""Run local tests."""
|
||||
print("Running local tests...")
|
||||
subprocess.run(
|
||||
"pytest --doctest-modules --cov=anybadge --cov-report html:htmlcov anybadge tests",
|
||||
shell=True,
|
||||
)
|
||||
|
||||
|
||||
@task
|
||||
def docker(c):
|
||||
"""Run dockerised tests."""
|
||||
print("Running containerised tests...")
|
||||
|
||||
subprocess.run("invoke clean", shell=True)
|
||||
subprocess.run("invoke build", shell=True)
|
||||
subprocess.run(
|
||||
f"(cd docker/test && docker build . -t test-anybadge:latest)", shell=True
|
||||
)
|
||||
subprocess.run(
|
||||
f"docker run -v {PROJECT_DIR}:/app test-anybadge:latest /work/run_docker_tests.sh",
|
||||
shell=True,
|
||||
)
|
@@ -1,8 +1,12 @@
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from anybadge import Badge
|
||||
from anybadge.cli import main, parse_args
|
||||
|
||||
|
||||
TESTS_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
class TestAnybadge(TestCase):
|
||||
"""Test case class for anybadge package."""
|
||||
|
||||
@@ -234,7 +238,7 @@ class TestAnybadge(TestCase):
|
||||
self.assertTrue("font_size=10" in badge_repr)
|
||||
|
||||
def test_template_from_file(self):
|
||||
file = "tests/template.svg"
|
||||
file = Path(__file__).parent / Path("template.svg")
|
||||
badge = Badge("template from file", value=file, template=file)
|
||||
_ = badge.badge_svg_text
|
||||
|
||||
@@ -266,8 +270,14 @@ class TestAnybadge(TestCase):
|
||||
with self.assertRaisesRegex(
|
||||
RuntimeError, r'File ".*tests\/exists\.svg" already exists\.'
|
||||
):
|
||||
badge.write_badge("tests/exists")
|
||||
badge.write_badge("tests/exists")
|
||||
badge.write_badge(TESTS_DIR / Path("exists"))
|
||||
badge.write_badge(TESTS_DIR / Path("exists"))
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
RuntimeError, r'File ".*tests\/exists\.svg" already exists\.'
|
||||
):
|
||||
badge.write_badge(str(TESTS_DIR / Path("exists")))
|
||||
badge.write_badge(str(TESTS_DIR / Path("exists")))
|
||||
|
||||
def test_arg_parsing(self):
|
||||
args = parse_args(["-l", "label", "-v", "value"])
|
||||
|
45
tests/test_server.py
Normal file
45
tests/test_server.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import requests # type: ignore
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
class TestAnybadgeServer(TestCase):
|
||||
"""Test case class for anybadge server."""
|
||||
|
||||
def setUp(self):
|
||||
if not hasattr(self, "assertRaisesRegex"):
|
||||
self.assertRaisesRegex = self.assertRaisesRegexp
|
||||
self.proc = subprocess.Popen(
|
||||
["anybadge-server", "-p", "8000", "--listen-address", "127.0.0.1"],
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.proc.kill()
|
||||
|
||||
def test_server_is_running(self):
|
||||
"""Test that the server is running."""
|
||||
self.assertTrue(self.proc.pid > 0)
|
||||
|
||||
def test_server_root_request(self):
|
||||
"""Test that the server can be accessed."""
|
||||
url = "http://127.0.0.1:8000"
|
||||
response = requests.get(url)
|
||||
self.assertTrue(response.ok)
|
||||
self.assertTrue(
|
||||
response.content.startswith(b"<html><head><title>Anybadge Web Server.")
|
||||
)
|
||||
|
||||
def test_server_badge_request(self):
|
||||
"""Test that the server can be accessed."""
|
||||
url = "http://127.0.0.1:8000/?label=Project%20Awesomeness&value=110%"
|
||||
response = requests.get(url)
|
||||
self.assertTrue(response.ok)
|
||||
print(response.content)
|
||||
self.assertTrue(
|
||||
response.content.startswith(b'<?xml version="1.0" encoding="UTF-8"?>\n<svg')
|
||||
)
|
Reference in New Issue
Block a user