Skip to content

Commit

Permalink
Add CI test to ensure recommender runs directly and with serverless o…
Browse files Browse the repository at this point in the history
…ffline (#31)

Adds PyTest tests to CI, and uses pytest to implement a version of the
smoketest with both direct Python API calls and using `serverless
offline start` to make sure that we don't break the local dev workflow
in addition to the Docker image workflow.

It also uses the `actions/cache` action to cache the model data so we
don't hit our S3 bucket so often.
  • Loading branch information
mdekstrand authored Jun 17, 2024
1 parent 2091506 commit ee9ad3a
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 5 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ jobs:
run: |
pipx install "dvc[s3]==3.*"
- name: Cache model data
uses: actions/cache@v4
with:
path: .dvc/cache
key: docker-dvc-cache-${{ hashFiles('src/models/**.dvc') }}

- name: Fetch model data
run: |
dvc pull -R src/models
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Tests

on:
push:
branches:
- main
pull_request:

# override default shell for mamba activation
defaults:
run:
shell: bash -el {0}

jobs:
run-tests:
name: Run the PyTest tests
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install environment
uses: mamba-org/setup-micromamba@v1
with:
environment-file: conda-lock.yml
environment-name: poprox
create-args: --category main --category dev --category test

- name: Install Node dependencies
run: |
npm ci
- name: Install recommender package
run: |
pip install --no-deps -e .
- name: Cache model data
uses: actions/cache@v4
with:
path: .dvc/cache
key: test-dvc-cache-${{ hashFiles('src/models/**.dvc') }}

- name: Fetch model data
run: |
dvc pull -R src/models
env:
AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}}
AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}}

- name: Run tests
run: |
python -m pytest -v
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ conda activate poprox-recsys
If you use `micromamba` instead of a full Conda installation, it can directly use the lockfile:

```console
micromamba create -n poprox-recs -f conda-lock.yml --category dev
micromamba create -n poprox-recs -f conda-lock.yml --category main --category dev
```

Set up `pre-commit` to make sure that code formatting rules are applied as you make changes:
Expand Down
81 changes: 78 additions & 3 deletions conda-lock.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
version: 1
metadata:
content_hash:
win-64: 3fff14af7a2b604d3f5af87c368a50ac073bba5244ee1c8d0465059732f81340
linux-64: 56ca3ab160eaac85dedc8711256b4c6a362dfe72a7e507f7478cad02d2c5f27a
osx-arm64: 8cd1a138f0524cc67c6d7bd1f206766f51e275fec14c179459cec48104691f57
win-64: ba6c7ce3da6dc2f431e12f9d793b070097df37fc3750830389f00cb53ca70d03
linux-64: 0ea25337871378e8b2e55111b2770e0a6e6ddfbbddb3ea9d45915bf059a4516a
osx-arm64: 3359e9440e37a2121c57091d0226d1eaf5b95eaf7886aee24a060cb3108bf265
channels:
- url: pytorch
used_env_vars: []
Expand Down Expand Up @@ -10527,6 +10527,45 @@ package:
sha256: 9a82c7d49c4771342b398661862975efb9c30e7af600b5d2e08a0bf416fda492
category: main
optional: false
- name: pexpect
version: 4.9.0
manager: conda
platform: linux-64
dependencies:
ptyprocess: '>=0.5'
python: '>=3.7'
url: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_0.conda
hash:
md5: 629f3203c99b32e0988910c93e77f3b6
sha256: 90a09d134a4a43911b716d4d6eb9d169238aff2349056f7323d9db613812667e
category: dev
optional: true
- name: pexpect
version: 4.9.0
manager: conda
platform: osx-arm64
dependencies:
python: '>=3.7'
ptyprocess: '>=0.5'
url: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_0.conda
hash:
md5: 629f3203c99b32e0988910c93e77f3b6
sha256: 90a09d134a4a43911b716d4d6eb9d169238aff2349056f7323d9db613812667e
category: dev
optional: true
- name: pexpect
version: 4.9.0
manager: conda
platform: win-64
dependencies:
python: '>=3.7'
ptyprocess: '>=0.5'
url: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_0.conda
hash:
md5: 629f3203c99b32e0988910c93e77f3b6
sha256: 90a09d134a4a43911b716d4d6eb9d169238aff2349056f7323d9db613812667e
category: dev
optional: true
- name: pillow
version: 10.3.0
manager: conda
Expand Down Expand Up @@ -11005,6 +11044,42 @@ package:
sha256: 576a228630a72f25d255a5e345e5f10878e153221a96560f2498040cd6f54005
category: main
optional: false
- name: ptyprocess
version: 0.7.0
manager: conda
platform: linux-64
dependencies:
python: ''
url: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd3deb0d_0.tar.bz2
hash:
md5: 359eeb6536da0e687af562ed265ec263
sha256: fb31e006a25eb2e18f3440eb8d17be44c8ccfae559499199f73584566d0a444a
category: dev
optional: true
- name: ptyprocess
version: 0.7.0
manager: conda
platform: osx-arm64
dependencies:
python: ''
url: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd3deb0d_0.tar.bz2
hash:
md5: 359eeb6536da0e687af562ed265ec263
sha256: fb31e006a25eb2e18f3440eb8d17be44c8ccfae559499199f73584566d0a444a
category: dev
optional: true
- name: ptyprocess
version: 0.7.0
manager: conda
platform: win-64
dependencies:
python: ''
url: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd3deb0d_0.tar.bz2
hash:
md5: 359eeb6536da0e687af562ed265ec263
sha256: fb31e006a25eb2e18f3440eb8d17be44c8ccfae559499199f73584566d0a444a
category: dev
optional: true
- name: pyarrow
version: 16.1.0
manager: conda
Expand Down
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies = [
[project.optional-dependencies]
# this is sub-optimal for various reasons, but it's the best way with conda-lock
dev = ["dvc[s3]==3.*", "docopt>=0.6", "requests~=2.31", "pre-commit>=3.7,<4"]
test = ["pexpect~=4.9"]

[project.urls]
Documentation = "https://github.com/CCRI-POPROX/poprox-recommender#readme"
Expand All @@ -42,7 +43,12 @@ allow-direct-references = true
path = "src/poprox_recommender/__about__.py"

[tool.hatch.envs.default]
dependencies = ["coverage[toml]>=6.5", "pytest>=8"]
dependencies = [
"coverage[toml]>=6.5",
"pytest>=8",
"pexpect~=4.9",
"requests~=2.13",
]
[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
test-cov = "coverage run -m pytest {args:tests}"
Expand Down
7 changes: 7 additions & 0 deletions tests/test_pfar.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
from poprox_recommender.default import select_articles, user_topic_preference
from poprox_recommender.topics import extract_general_topic, general_topics, match_news_topics_to_general

try:
import pytest

pytestmark = pytest.mark.skip("not a test module")
except ImportError:
pass


def load_test_articles():
event_path = "/home/sun00587/research/POPROX/poprox-recommender/tests/request_body.json" # update when the model path func is ready # noqa: E501
Expand Down
64 changes: 64 additions & 0 deletions tests/test_serverless_offline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
Test the POPROX endpoint running under Serverless Offline.
"""

import logging
import sys
from pathlib import Path
from threading import Condition, Lock, Thread

import requests
from pexpect import EOF, spawn
from pytest import fail, fixture, mark

logger = logging.getLogger(__name__)


@fixture(scope="module")
def sl_listener():
"""
Fixture that starts and stops serverless offline to test endpoint responses.
"""

thread = ServerlessBackground()
thread.start()
try:
with thread.lock:
if thread.ready.wait(10):
logger.info("ready for tests")
yield
else:
fail("serverless timed out")
finally:
thread.proc.sendintr()


class ServerlessBackground(Thread):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.lock = Lock()
self.ready = Condition(self.lock)

def run(self):
logger.info("starting serverless")
self.proc = spawn("npx serverless offline start", logfile=sys.stdout.buffer)
self.proc.expect(r"Server ready:")
logger.info("server ready")
with self.lock:
self.ready.notify_all()
self.proc.expect(EOF)


@mark.serverless
def test_basic_request(sl_listener):
test_dir = Path(__file__)
req_f = test_dir.parent / "basic-request.json"
req_body = req_f.read_text()

logger.info("sending request")
res = requests.post("http://localhost:3000", req_body)
assert res.status_code == 200
logger.info("response: %s", res.text)
body = res.json()
assert "recommendations" in body
assert len(body["recommendations"]) > 0
27 changes: 27 additions & 0 deletions tests/test_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""
Test the POPROX API through direct call.
"""

import logging
from pathlib import Path

from poprox_concepts.api.recommendations import RecommendationRequest
from poprox_recommender.default import select_articles

logger = logging.getLogger(__name__)


def test_direct_basic_request():
test_dir = Path(__file__)
req_f = test_dir.parent / "basic-request.json"
req = RecommendationRequest.model_validate_json(req_f.read_text())

logger.info("generating recommendations")
recs = select_articles(
req.todays_articles,
req.past_articles,
req.click_histories,
req.num_recs,
)
# do we get recommendations?
assert len(recs) > 0

0 comments on commit ee9ad3a

Please sign in to comment.