Skip to content

Commit 8f6f0b9

Browse files
authored
Merge pull request #72 from openplans/test-modernization
Modernize Test Suite and Improve Tests
2 parents 9366050 + 5987193 commit 8f6f0b9

16 files changed

Lines changed: 489 additions & 37 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ build/
1414
coverage
1515
coverage.data
1616
.coverage
17+
htmlcov/
1718

1819
# junk
1920
.DS_Store

Containerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ RUN pip install --no-cache-dir -r /tmp/requirements.txt
2323

2424
# Copy the application code to the container
2525
COPY src /app
26+
COPY pytest.ini /app/pytest.ini
2627
WORKDIR /app
2728

2829
# Run collectstatic to gather static files

Makefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: test-env test test-clean build gcp-push gcp-restart gcp-deploy
1+
.PHONY: test-env test test-coverage test-coverage-html test-clean build gcp-push gcp-restart gcp-deploy
22

33
# Build the container image
44
build:
@@ -32,7 +32,15 @@ test-env:
3232

3333
# Run tests in a clean container environment
3434
test: test-env test-clean
35-
podman-compose run --rm test
35+
podman-compose run --rm test pytest
36+
37+
# Run tests with coverage report
38+
test-coverage: test-env test-clean
39+
podman-compose run --rm test pytest --cov=sa_api_v2 --cov-report=term-missing
40+
41+
# Run tests with HTML coverage report (outputs to htmlcov/)
42+
test-coverage-html: test-env test-clean
43+
podman-compose run --rm test pytest --cov=sa_api_v2 --cov-report=html
3644

3745
# Just clean up containers
3846
test-clean:

compose.yml

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ services:
1313
environment:
1414
- REDIS_URL=redis://redis:6379/0
1515
depends_on:
16-
db: {"condition": "service_healthy"}
17-
redis: {"condition": "service_healthy"}
16+
db: { "condition": "service_healthy" }
17+
redis: { "condition": "service_healthy" }
1818
restart: "no"
1919

2020
web:
@@ -27,9 +27,9 @@ services:
2727
ports:
2828
- "8000:8000"
2929
depends_on:
30-
db: {"condition": "service_healthy"}
31-
redis: {"condition": "service_healthy"}
32-
init: {"condition": "service_completed_successfully"}
30+
db: { "condition": "service_healthy" }
31+
redis: { "condition": "service_healthy" }
32+
init: { "condition": "service_completed_successfully" }
3333

3434
worker:
3535
build:
@@ -41,22 +41,24 @@ services:
4141
C_FORCE_ROOT: "true"
4242
REDIS_URL: "redis://redis:6379/0"
4343
depends_on:
44-
db: {"condition": "service_healthy"}
45-
redis: {"condition": "service_healthy"}
46-
init: {"condition": "service_completed_successfully"}
44+
db: { "condition": "service_healthy" }
45+
redis: { "condition": "service_healthy" }
46+
init: { "condition": "service_completed_successfully" }
4747

4848
test:
49-
build:
50-
context: .
51-
dockerfile: Containerfile
52-
command: python3 manage.py test .
49+
image: shareabouts-api
50+
command: pytest
5351
env_file: .env
5452
environment:
5553
- REDIS_URL=redis://redis:6379/0
54+
volumes:
55+
- ./htmlcov:/app/htmlcov
56+
- ./src:/app
57+
- ./pytest.ini:/app/pytest.ini
5658
depends_on:
57-
db: {"condition": "service_healthy"}
58-
redis: {"condition": "service_healthy"}
59-
init: {"condition": "service_completed_successfully"}
59+
db: { "condition": "service_healthy" }
60+
redis: { "condition": "service_healthy" }
61+
init: { "condition": "service_completed_successfully" }
6062

6163
db:
6264
image: postgis/postgis:15-3.3
@@ -67,7 +69,7 @@ services:
6769
ports:
6870
- "15432:5432"
6971
healthcheck:
70-
test: [ "CMD-SHELL","psql -U postgres -d shareabouts -c \"SELECT ST_Buffer( ST_SetSRID('POINT(0 0)'::GEOMETRY, 4326), 1) AS geom ;\""]
72+
test: [ "CMD-SHELL", "psql -U postgres -d shareabouts -c \"SELECT ST_Buffer( ST_SetSRID('POINT(0 0)'::GEOMETRY, 4326), 1) AS geom ;\"" ]
7173
interval: 10s
7274
timeout: 5s
7375
retries: 5
@@ -80,4 +82,4 @@ services:
8082
test: [ "CMD", "redis-cli", "ping" ]
8183
interval: 10s
8284
timeout: 5s
83-
retries: 5
85+
retries: 5

pytest.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[pytest]
2+
DJANGO_SETTINGS_MODULE = project.settings
3+
python_files = tests.py test_*.py *_tests.py
4+
addopts = --reuse-db --nomigrations

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ prompt-toolkit==3.0.43
4545
psycopg==3.1.17
4646
psycopg-binary==3.1.17
4747
pycparser==2.21
48+
pytest==7.4.4
49+
pytest-cov==4.1.0
50+
pytest-django==4.7.0
51+
pytest-xdist==3.5.0
4852
PyJWT==2.8.0
4953
python-dateutil==2.8.2
5054
python-social-auth==0.2.21

src/project/settings.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666

6767
LANGUAGE_CODE = 'en-us'
6868
USE_I18N = True
69-
USE_L10N = True
7069

7170
###############################################################################
7271
#
@@ -106,7 +105,8 @@
106105
},
107106
]
108107

109-
ATTACHMENT_STORAGE = 'django.core.files.storage.FileSystemStorage'
108+
# Use the new div-based form rendering to avoid RemovedInDjango50Warning
109+
FORM_RENDERER = 'django.forms.renderers.DjangoDivFormRenderer'
110110

111111
###############################################################################
112112
#
@@ -433,16 +433,27 @@ def custom_show_toolbar(request):
433433
# We support S3, GCS, and local filesystem.
434434
# Precedence: GCS > S3 > Local
435435

436-
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
437-
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
438-
ATTACHMENT_STORAGE = DEFAULT_FILE_STORAGE
436+
STORAGES = {
437+
'default': {
438+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
439+
},
440+
# Static files (CSS, JavaScript, Images)
441+
# https://docs.djangoproject.com/en/1.6/howto/static-files/
442+
'staticfiles': {
443+
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
444+
},
445+
'attachments': {
446+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
447+
},
448+
}
449+
ATTACHMENT_STORAGE = STORAGES['attachments']['BACKEND']
439450

440451
if 'GS_BUCKET_NAME' in environ:
441452
# Google Cloud Storage
442453
GS_BUCKET_NAME = environ['GS_BUCKET_NAME']
443454
GS_PROJECT_ID = environ.get('GS_PROJECT_ID')
444455

445-
DEFAULT_FILE_STORAGE = "storages.backends.gcloud.GoogleCloudStorage"
456+
STORAGES['default']['BACKEND'] = "storages.backends.gcloud.GoogleCloudStorage"
446457

447458
GS_DEFAULT_ACL = "publicRead"
448459

@@ -456,7 +467,7 @@ def custom_show_toolbar(request):
456467
MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/"
457468

458469
# Attachments
459-
ATTACHMENT_STORAGE = DEFAULT_FILE_STORAGE
470+
ATTACHMENT_STORAGE = STORAGES['attachments']['BACKEND'] = STORAGES['default']['BACKEND']
460471

461472
elif all([key in environ for key in ('SHAREABOUTS_AWS_KEY',
462473
'SHAREABOUTS_AWS_SECRET',
@@ -468,8 +479,8 @@ def custom_show_toolbar(request):
468479
AWS_QUERYSTRING_AUTH = False
469480
AWS_PRELOAD_METADATA = True
470481

471-
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
472-
ATTACHMENT_STORAGE = DEFAULT_FILE_STORAGE
482+
STORAGES['default']['BACKEND'] = 'storages.backends.s3boto3.S3Boto3Storage'
483+
ATTACHMENT_STORAGE = STORAGES['attachments']['BACKEND'] = STORAGES['default']['BACKEND']
473484

474485

475486
if 'SHAREABOUTS_TWITTER_KEY' in environ and 'SHAREABOUTS_TWITTER_SECRET' in environ:

src/pytest.ini

Whitespace-only changes.

src/sa_api_v2/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def render(self, name, value, attrs=None, renderer=None):
8282
return super(PrettyAceWidget, self).render(name, value, attrs=attrs, renderer=renderer)
8383

8484

85-
BaseGeoAdmin = admin.OSMGeoAdmin if settings.USE_GEODB else admin.ModelAdmin
85+
BaseGeoAdmin = admin.GISModelAdmin if settings.USE_GEODB else admin.ModelAdmin
8686

8787

8888
class SubmittedThingAdmin(BaseGeoAdmin):

src/sa_api_v2/models/core.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
else:
88
from django.db import models
99

10-
from django.core.files.storage import get_storage_class
10+
from django.core.files.storage import storages
1111
from django.utils.timezone import now
1212
from .. import cache
1313
from .. import utils
@@ -18,6 +18,10 @@
1818
from PIL import Image, UnidentifiedImageError
1919

2020

21+
# Create a simple AttachmentStorage factory
22+
AttachmentStorage = lambda *a, **kw : storages.create_storage(storages.backends['attachments'], *a, **kw)
23+
24+
2125
class TimeStampedModel (models.Model):
2226
created_datetime = models.DateTimeField(default=now, blank=True, db_index=True)
2327
updated_datetime = models.DateTimeField(auto_now=True, db_index=True)
@@ -294,9 +298,6 @@ def timestamp_filename(attachment, filename):
294298
return ''.join(['attachments/', utils.base62_time(), '-', filename])
295299

296300

297-
AttachmentStorage = get_storage_class(settings.ATTACHMENT_STORAGE)
298-
299-
300301
class Attachment (CacheClearingModel, TimeStampedModel):
301302
"""
302303
A file attached to a submitted thing.

0 commit comments

Comments
 (0)