transport-accessibility/transport_accessibility/pt_map/tests_gtfs_compliance.py
Johannes Randerath 3853d25c1e Added LICENSE
- Code uses AGPL
- Docs use GNU FDL
2024-07-08 22:10:53 +02:00

180 lines
9.1 KiB
Python

"""
Test GTFS Compliance
====================
Test suite meant to make sure the models do their best to enforce compliance of the database with the gtfs standard.
In particular we are testing:
1. All of the txt files the reference suggests have a model representation.
2. All of the fields described in each of the files is representable inside the corresponding model.
3. The fields can be set with standard data for the correct data type.
4. None of the optional fields are enforced to be set. (TODO)
5. All of the required fields are enforced to be set with a legal value. (TODO)
6. If set, the optional fields hold only legal values (TODO)
7. All conditional requirements and restrictions are enforced. (TODO)
8. Foreign keys and many to many sets are actually set within the database and are validated. (TODO)
Functions
---------
_get_test_data(f: function):
Decorator, wrapping instance methods to provide an extendable method to get data from the test data set based on the TestCase's requirements.
_test_constructor(f: function):
Decorator. Basic test to validate if the models are instantiable with the given set of fields.
Classes
-------
AllFieldsPresentTestCase(TestCase):
Test case making sure all files are represented with a complete set of fields. (Conditions 1+2 from the above list)
ConstructorAllFieldsTestCase(TestCase):
Test case making sure the models can be instantiated given valid values for all their fields. (3)
ConstructorRequiredFieldsOnlyTestCase(TestCase):
Test case making sure the model creation is fine with just the required fields. (4)
"""
# This file is part of transport-accessibility.
# Copyright (C) 2024 Janek Kiljanski, Johannes Randerath
#
# transport-accessibility is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3
# of the License, or (at your option) any later version.
#
# transport-accessibility is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with transport-accessibility.
# If not, see <https://www.gnu.org/licenses/>.
from django.test import TestCase, TransactionTestCase
from pt_map.test_data import *
from pt_map.models import *
import unittest
from django.db import models, transaction
import random
import time
import inspect
import pt_map.models
import datetime
def _get_test_data(f):
"""Get test data for a field type."""
def wrapper(self, field, model, d, i):
f(self, field, model, d, i)
if field["name"] == "service_id":
self.add += 1
return f"{d['pk']}{self.add}"
match field["type"]:
case "fk":
if isinstance(field["references"], list):
field["references"] = random.choice(field["references"])
if field["references"].objects.all():
return field["references"].objects.all()[0]
if not field["references"] == model and field["references"]:
fk = field["references"].objects.create(**{**{f["name"]: self.get_test_data(f, field["references"], d, i) for f in self.gtfs_fields[field["references"]._meta.object_name] if not f["type"] == "mtm"}, "feed_info_id": self.feed_info})
for n, v in {f["name"]: self.get_test_data(f, model, d, i) for f in self.gtfs_fields[field["references"]._meta.object_name] if f["type"] == "mtm"}.items():
getattr(fk, n).set(v)
return fk
return None
case "unique":
return (d['pk'], "unique") #TODO
case "enum":
return random.choice(field["allowed_values"])
case "date":
return datetime.datetime.fromisoformat(d["date"])
case "pk":
return f"{d['pk']}{datetime.datetime.now().timestamp()}"
case "lat":
return d["float"] #TODO
case "lon":
return d["float"] #TODO
case "mtm":
choices = [j for j in range(len(data)-1) if not j == i]
indexes = set([])
while len(indexes) < 3:
indexes.add(random.choice(choices))
indexes = list(indexes)
mtm_test_data = [data[j] for j in indexes if field["references"]._meta.pk.name in data[j].keys() and not field["references"].objects.filter(pk=data[j]['pk'])]
qs = [field["references"].objects.create(**{**{f["name"]: self.get_test_data(f, field["references"], mtm_test_data[j], i) for f in self.gtfs_fields[field["references"]._meta.object_name] if not f["type"] == "mtm"}, "feed_info_id": self.feed_info}) for j in range(len(mtm_test_data))]
for m in qs:
for n, v in {f["name"]: self.get_test_data(f, model, d, i) for f in self.gtfs_fields[field["references"]._meta.object_name] if f["type"] == "mtm"}.items():
getattr(m, n).set(v)
return field["references"].objects.all()
case _:
return d[field["type"]]
return wrapper
def _test_constructor(index):
def decorate(f):
def wrapper(self, *args, **kwargs):
f(self, *args, **kwargs)
for name, model in [tpl for tpl in inspect.getmembers(pt_map.models, inspect.isclass) if tpl[1] not in [FeedInfo,]]:
with self.subTest(name=name):
d = data[index]
values = {**{field["name"]: self.get_test_data(field, model, d, index) for field in self.gtfs_fields[name] if not field["type"] == "mtm"}, "feed_info_id": self.feed_info}
obj = model.objects.create(**values)
for n, v in {field["name"]: self.get_test_data(field, model, d, index) for field in self.gtfs_fields[name] if field["type"] == "mtm"}.items():
getattr(obj, n).set(v)
self.assertIsNotNone(obj)
self.assertIsInstance(obj, models.Model)
return wrapper
return decorate
class AllFieldsPresentTestCase(TestCase):
"""Test for the presence of all the fields of all the models, the GTFS reference describes."""
def setUp(self):
self.model_fields = {name: [*[f.name for f in model._meta.fields], *[f.name for f in model._meta.many_to_many]] for name, model in inspect.getmembers(pt_map.models, inspect.isclass)}
self.gtfs_fields = {name: get_all_fields(name) for name,model in inspect.getmembers(pt_map.models, inspect.isclass)}
def test_all_fields_present(self):
"""Make sure the model has properties for all fields - regardless if required - provided by the GTFS standard."""
for name in self.model_fields.keys():
with self.subTest(name=name):
for f in self.gtfs_fields[name]:
with self.subTest(f=f):
self.assertIn(f["name"], self.model_fields[name])
class ConstructorAllFieldsTestCase(TransactionTestCase):
"""Test all the models can be initialized using appropriate data as specified by the GTFS."""
def setUp(self):
self.model_fields = {name: [f.name for f in model._meta.fields] for name, model in inspect.getmembers(pt_map.models, inspect.isclass) if not model == FeedInfo}
self.model_mtm = {name: [f.name for f in model._meta.many_to_many] for name, model in inspect.getmembers(pt_map.models, inspect.isclass) if not model == FeedInfo}
self.gtfs_fields = {name: get_all_fields(name) for name, model in inspect.getmembers(pt_map.models, inspect.isclass) if not model == FeedInfo}
self.feed_info = FeedInfo.objects.create(**{f["name"]: data[0][f["type"]] for f in get_all_fields("FeedInfo")})
self.add = 0
@_get_test_data
def get_test_data(self, field, model, d, i):
pass
@_test_constructor(0)
def test_constructor_all_fields_fixed(self):
"""Make sure all of the model's fields are initializable with appropriate values. Fixed test data."""
pass
@_test_constructor(random.randint(0, len(data)-1))
def test_constructor_all_fields_other(self):
"""Make sure all of the model's fields are initializable with appropriate values. A second set of test data."""
pass
class ConstructorRequiredFieldsOnlyTestCase(TransactionTestCase):
def setUp(self):
self.gtfs_fields = {name: get_required_fields(name) for name, model in inspect.getmembers(pt_map.models, inspect.isclass) if model not in [FeedInfo]}
self.feed_info = FeedInfo.objects.create(**{f["name"]: data[0][f["type"]] for f in get_required_fields("FeedInfo")})
self.add = 0
@_get_test_data
def get_test_data(self, field, model, d, i):
pass
@_test_constructor(0)
def test_constructor_required_fields_only_fixed(self):
pass
@_test_constructor(random.randint(0, len(data)-1))
def test_constructor_required_fields_only_other(self):
pass