Add tests
This uses the `docker compose run --rm` approach suggested by Dudo [here](https://github.com/orgs/gitops-ci-cd/discussions/1). I'm...still learning my around it. It has some learning curve - especially regarding running integration tests (the current setup _keeps_ the app running even after tests have terminated, which is probably not as-desired), but I suspect it'll become second-nature pretty quickly and will have outsized benefits when working in a team with heterogeneous workstation setups.
This commit is contained in:
parent
fb1b485849
commit
0a2da8eb51
31
Dockerfile
31
Dockerfile
@ -1,12 +1,37 @@
|
||||
FROM python:3.13
|
||||
# syntax=docker.io/docker/dockerfile:1.7-labs
|
||||
FROM python:3.13 AS deps
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY dev-requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r dev-requirements.txt
|
||||
RUN rm dev-requirements.txt
|
||||
|
||||
#=======================================================
|
||||
|
||||
FROM deps AS app
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
RUN apt-get update && apt-get install -y ffmpeg
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN rm requirements.txt
|
||||
|
||||
COPY --exclude=test_*.py src ./src
|
||||
|
||||
# Thanks to https://stackoverflow.com/questions/64776990/python-docker-no-module-found
|
||||
ENV PYTHONPATH /usr/src/app
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT ["python"]
|
||||
CMD ["src/main.py"]
|
||||
|
||||
#=======================================================
|
||||
|
||||
FROM deps AS test
|
||||
|
||||
COPY dev-requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r dev-requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD [ "python", "./main.py" ]
|
||||
# No `CMD`, because there are plenty of potential test commands, no single sensible default.
|
||||
|
46
compose.yaml
Normal file
46
compose.yaml
Normal file
@ -0,0 +1,46 @@
|
||||
services:
|
||||
base: &base
|
||||
tty: true
|
||||
stdin_open: true
|
||||
# Necessary because compose does not permit a service to lack a build
|
||||
build:
|
||||
context: .
|
||||
target: deps
|
||||
entrypoint: python
|
||||
volumes:
|
||||
- .:/usr/src/app:delegated
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
target: app
|
||||
environment:
|
||||
- DOWNLOAD_DIR=/download
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./download_dir:/download
|
||||
|
||||
test-base: &test-base
|
||||
<<: *base
|
||||
build:
|
||||
context: .
|
||||
target: test
|
||||
|
||||
test:
|
||||
<<: *test-base
|
||||
command: ["-m", "pytest", "--ignore=integ-tests", "."]
|
||||
|
||||
test-with-output:
|
||||
<<: *test-base
|
||||
command: ["-m", "pytest", "-rP", "--ignore=integ-tests", "."]
|
||||
|
||||
test-integ:
|
||||
<<: *test-base
|
||||
depends_on:
|
||||
# TODO - should really have a healthcheck here to avoid race-conditions. "Up" doesn't actually mean "Up and accepting requests"
|
||||
- app
|
||||
command: ["-m", "pytest", "integ-tests" ]
|
||||
volumes:
|
||||
- ./download_dir:/download
|
||||
- .:/usr/src/app:delegated # Have to re-specify this because the specification above overrides, rather than appending
|
2
dev-requirements.txt
Normal file
2
dev-requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
pytest==8.3.4
|
||||
requests==2.28.1
|
1
download_dir/README.md
Normal file
1
download_dir/README.md
Normal file
@ -0,0 +1 @@
|
||||
A location into which the application can download files when run locally. Particularly useful for sharing with the Integ Test service!
|
35
integ-tests/test_end-to-end.py
Normal file
35
integ-tests/test_end-to-end.py
Normal file
@ -0,0 +1,35 @@
|
||||
import pathlib
|
||||
import requests
|
||||
import time
|
||||
|
||||
def test_download():
|
||||
target_file_name = "A Beginner's Guide to the EICAR Test File [bTThnbwxN5g].m4a"
|
||||
download_path = pathlib.Path('/download')
|
||||
url = 'https://www.youtube.com/watch?v=bTThnbwxN5g'
|
||||
|
||||
try:
|
||||
# `app` is injected as a DNS name by the Docker Compose harness
|
||||
response = requests.post('http://app:8000/download', json={
|
||||
'url': url
|
||||
})
|
||||
assert response.status_code == 202, f"Non-202 response: {response.status_code}. Body: {response.json()}"
|
||||
time.sleep(2)
|
||||
for _ in range(5):
|
||||
if _does_target_file_exist(target_file_name, download_path):
|
||||
break
|
||||
else:
|
||||
time.sleep(3)
|
||||
else:
|
||||
assert False, "File not found after 15 seconds"
|
||||
finally:
|
||||
for f in download_path.iterdir():
|
||||
if f.name != 'README.md':
|
||||
f.unlink()
|
||||
|
||||
def _does_target_file_exist(file_name: str, dir: pathlib.Path) -> bool:
|
||||
for f in dir.iterdir():
|
||||
if f.name == file_name:
|
||||
return True
|
||||
else:
|
||||
print(f"Found file: {f.name} which does not match")
|
||||
return False
|
@ -1 +1 @@
|
||||
yt-dlp
|
||||
yt-dlp==2024.10.22
|
||||
|
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
@ -69,7 +69,7 @@ class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
|
||||
def do_GET(self):
|
||||
self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
content = 'ERROR: Only HEAD requests are permitted\n'.encode('utf-8')
|
||||
content = 'ERROR: Only POST requests are permitted\n'.encode('utf-8')
|
||||
self.send_header("Content-type", 'application/json')
|
||||
self.send_header('Content-Length', len(content))
|
||||
self.end_headers()
|
@ -4,7 +4,7 @@ import os
|
||||
import socketserver
|
||||
|
||||
from functools import partial
|
||||
from handler import Handler
|
||||
from src.handler import Handler
|
||||
from multiprocessing import Pool
|
||||
|
||||
PORT = int(os.environ.get('PORT', '8000'))
|
20
src/test_handler.py
Normal file
20
src/test_handler.py
Normal file
@ -0,0 +1,20 @@
|
||||
import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
from src.handler import download
|
||||
|
||||
def test_download():
|
||||
video_url = "https://www.youtube.com/watch?v=bTThnbwxN5g"
|
||||
expected_filename = "A Beginner's Guide to the EICAR Test File [bTThnbwxN5g].m4a"
|
||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||
os.environ['DOWNLOAD_DIR'] = tmpdirname
|
||||
download(video_url)
|
||||
download_dir = pathlib.Path(tmpdirname)
|
||||
passes = False
|
||||
for contents in download_dir.iterdir():
|
||||
print(f'DEBUG - {contents=}')
|
||||
if contents.name == expected_filename:
|
||||
passes = True
|
||||
break
|
||||
assert passes, "Could not find downloaded file"
|
Loading…
x
Reference in New Issue
Block a user