From 0f5cf9568a180600d23d3146fb2a1a7347f36bac Mon Sep 17 00:00:00 2001 From: Joel Haasnoot Date: Wed, 5 Aug 2026 14:54:03 +0200 Subject: [PATCH 1/2] Fix the test suite The suite has not run in a while. Test discovery itself was broken: openebs/tests and kv1/tests imported their modules by hand through loader.find_module(), which Python 3.12 removed, so nothing ran at all. Django discovers test*.py by itself, so the initialisers can be empty. With discovery working, 24 of 36 tests failed. Most were stale fixtures, but five were real bugs the tests had been unable to report: - Kv15Scenario.delete_all() filtered on messageendtime__gt=now, passing the function instead of calling it, so deleting a planned scenario raised TypeError - sendkv6 compared msg.delay > 0 on a nullable field in two of the three places it checks it, so a ferry message without a delay crashed the command. The init branch already guarded for None - get_pushers() raised ImproperlyConfigured when any subscriber lacked the message type, and ndovloket_rig has no KV6 endpoint - so KV6 could not even be imported. Skip subscribers that do not take the type - the KV17 journey template read "mutation > 0 or shorten > 0 and not is_recovered", and "and" binds tighter than "or", so a recover still emitted a mutation message block - verify_messages matched KV8 deletes with get_or_create() on the KV8 identity, but an updated message leaves the superseded version behind carrying that same identity, so the lookup raised MultipleObjectsReturned. Match the most recent one The rest is fixture rot: stops are matched by QuayCode rather than timingpointcode, message identity moved to OriginalMessageCode*, mutation messages moved to their own model, KV17 cancels gained showcancelledtrip and autorecover, import_rid reads bison_id, and assertEquals is gone in Python 3.12. Co-Authored-By: Claude Opus 5 --- ferry/management/commands/sendkv6.py | 4 +- kv1/tests/__init__.py | 9 --- kv1/tests/test_import_rid.py | 7 +- .../management/commands/verify_messages.py | 15 +++-- openebs/models.py | 2 +- openebs/templates/xml/kv17journey.xml | 4 +- openebs/tests/__init__.py | 9 --- openebs/tests/output/kv17_cancel.xml | 5 +- openebs/tests/output/kv17_mutationmessage.xml | 1 + .../output/kv17_mutationmessage_cancel.xml | 6 +- .../output/kv17_mutationmessage_complete.xml | 1 + openebs/tests/output/kv17_reason_basic.xml | 2 + openebs/tests/output/kv17_reason_full.xml | 2 + openebs/tests/test_command_verify.py | 64 ++++++++++++++----- openebs/tests/test_kv15_xml.py | 2 +- openebs/tests/test_kv17_xml.py | 10 +-- openebs/tests/test_model_log.py | 2 +- openebs/tests/test_views_permissions.py | 20 +++--- utils/views.py | 5 +- 19 files changed, 104 insertions(+), 66 deletions(-) diff --git a/ferry/management/commands/sendkv6.py b/ferry/management/commands/sendkv6.py index 015e323c..afc58bed 100644 --- a/ferry/management/commands/sendkv6.py +++ b/ferry/management/commands/sendkv6.py @@ -46,7 +46,7 @@ def handle_ferry(self, ferry): try: msg = FerryKv6Messages.objects.get(operatingday=date, ferry=ferry, journeynumber=journey.journeynumber, status=FerryKv6Messages.Status.READY, cancelled=False) - if msg.delay > 0 and journey.departuretime + msg.delay > depart_target: + if msg.delay is not None and msg.delay > 0 and journey.departuretime + msg.delay > depart_target: continue msg.status = FerryKv6Messages.Status.DEPARTED @@ -66,7 +66,7 @@ def handle_ferry(self, ferry): try: msg = FerryKv6Messages.objects.get(operatingday=date, ferry=ferry, journeynumber=journey.journeynumber, status=FerryKv6Messages.Status.DEPARTED, cancelled=False) - if msg.delay > 0 and journey.departuretime + msg.delay > arrival_target: + if msg.delay is not None and msg.delay > 0 and journey.departuretime + msg.delay > arrival_target: continue msg.status = FerryKv6Messages.Status.ARRIVED diff --git a/kv1/tests/__init__.py b/kv1/tests/__init__.py index ea7d1037..e69de29b 100644 --- a/kv1/tests/__init__.py +++ b/kv1/tests/__init__.py @@ -1,9 +0,0 @@ -import pkgutil -import unittest - -for loader, module_name, is_pkg in pkgutil.walk_packages(__path__): - module = loader.find_module(module_name).load_module(module_name) - for name in dir(module): - obj = getattr(module, name) - if isinstance(obj, type) and issubclass(obj, unittest.case.TestCase): - exec ('%s = obj' % obj.__name__) \ No newline at end of file diff --git a/kv1/tests/test_import_rid.py b/kv1/tests/test_import_rid.py index a88c6506..41a85ef3 100644 --- a/kv1/tests/test_import_rid.py +++ b/kv1/tests/test_import_rid.py @@ -1,4 +1,3 @@ -from unittest.test.test_case import Test from django.core.management import call_command from django.test import TestCase @@ -45,12 +44,12 @@ def testStopsSimple(self): # Data lines = [ - ['operator_id','publiccode','name'], + ['bison_id','publiccode','name'], ['VTN:1049','62','Gulpen - Vaals'], ] stops = [ - ['operator_id','name','longitude','latitude','timingpointcode',], - ['VTN:15023014','Busstation Perron C', '1', '2','15023014'] # Load an updated name + ['operator_id','name','longitude','latitude','timingpointcode','quaycoderef'], + ['VTN:15023014','Busstation Perron C', '1', '2','15023014','NL:Q:15023014'] # Load an updated name ] self.createTestFile('openebs_lines.csv', lines) self.createTestFile('openebs_stops.csv', stops) diff --git a/openebs/management/commands/verify_messages.py b/openebs/management/commands/verify_messages.py index 4ecc323e..ba041cc4 100644 --- a/openebs/management/commands/verify_messages.py +++ b/openebs/management/commands/verify_messages.py @@ -96,10 +96,17 @@ def process_message(self, row, deleted): self.add_stop_for_message(msg, row) else: - msg, created = Kv15Stopmessage.objects.get_or_create(dataownercode=row['DataOwnerCode'], - kv8messagecodedate=row['MessageCodeDate'], - kv8messagecodenumber=row['MessageCodeNumber'], - defaults={'user': self.get_user()}) + # Updating a message keeps the superseded version around, and it carries the same + # KV8 identity, so match the most recent one instead of tripping over the duplicates + msg = Kv15Stopmessage.objects.filter(dataownercode=row['DataOwnerCode'], + kv8messagecodedate=row['MessageCodeDate'], + kv8messagecodenumber=row['MessageCodeNumber']).order_by('id').last() + created = msg is None + if created: + msg = Kv15Stopmessage.objects.create(dataownercode=row['DataOwnerCode'], + kv8messagecodedate=row['MessageCodeDate'], + kv8messagecodenumber=row['MessageCodeNumber'], + user=self.get_user()) if not created: self.log.info("Message confirmed deleted: %s (Stop/TPC %s)" % (msg, row['TimingPointCode'])) diff --git a/openebs/models.py b/openebs/models.py index 917d6250..497140c5 100644 --- a/openebs/models.py +++ b/openebs/models.py @@ -342,7 +342,7 @@ def plan_messages(self, user, start, end): def delete_all(self): msgs = [] - for inst in Kv15ScenarioInstance.objects.filter(scenario=self, message__messageendtime__gt=now): + for inst in Kv15ScenarioInstance.objects.filter(scenario=self, message__messageendtime__gt=now()): inst.message.delete() msgs.append(inst.message.to_xml_delete()) diff --git a/openebs/templates/xml/kv17journey.xml b/openebs/templates/xml/kv17journey.xml index 75d944d1..7afea06f 100644 --- a/openebs/templates/xml/kv17journey.xml +++ b/openebs/templates/xml/kv17journey.xml @@ -47,7 +47,7 @@ {% endif %} {% endif %} -{% if object.journey_details_mutation_message.count > 0 or object.shorten_details.count > 0 and not object.is_recovered %} +{% if object.journey_details_mutation_message.count > 0 or object.shorten_details.count > 0 %}{% if not object.is_recovered %} {{ object.created|date:"c" }} {% for shorten in object.shorten_details.all %} @@ -75,4 +75,4 @@ {% endfor %} -{% endif %} \ No newline at end of file +{% endif %}{% endif %} \ No newline at end of file diff --git a/openebs/tests/__init__.py b/openebs/tests/__init__.py index ea7d1037..e69de29b 100644 --- a/openebs/tests/__init__.py +++ b/openebs/tests/__init__.py @@ -1,9 +0,0 @@ -import pkgutil -import unittest - -for loader, module_name, is_pkg in pkgutil.walk_packages(__path__): - module = loader.find_module(module_name).load_module(module_name) - for name in dir(module): - obj = getattr(module, name) - if isinstance(obj, type) and issubclass(obj, unittest.case.TestCase): - exec ('%s = obj' % obj.__name__) \ No newline at end of file diff --git a/openebs/tests/output/kv17_cancel.xml b/openebs/tests/output/kv17_cancel.xml index 1429f4fe..0ea77f47 100644 --- a/openebs/tests/output/kv17_cancel.xml +++ b/openebs/tests/output/kv17_cancel.xml @@ -8,6 +8,9 @@ ... - + + true + false + \ No newline at end of file diff --git a/openebs/tests/output/kv17_mutationmessage.xml b/openebs/tests/output/kv17_mutationmessage.xml index e0c937c8..e32838bc 100644 --- a/openebs/tests/output/kv17_mutationmessage.xml +++ b/openebs/tests/output/kv17_mutationmessage.xml @@ -14,6 +14,7 @@ 3 7 Boot is vol + true \ No newline at end of file diff --git a/openebs/tests/output/kv17_mutationmessage_cancel.xml b/openebs/tests/output/kv17_mutationmessage_cancel.xml index 51968668..d1ec3b47 100644 --- a/openebs/tests/output/kv17_mutationmessage_cancel.xml +++ b/openebs/tests/output/kv17_mutationmessage_cancel.xml @@ -8,7 +8,10 @@ ... - + + true + false + ... @@ -18,6 +21,7 @@ 3 7 Boot is vol en vaart niet + true \ No newline at end of file diff --git a/openebs/tests/output/kv17_mutationmessage_complete.xml b/openebs/tests/output/kv17_mutationmessage_complete.xml index 3e602786..f0f28aec 100644 --- a/openebs/tests/output/kv17_mutationmessage_complete.xml +++ b/openebs/tests/output/kv17_mutationmessage_complete.xml @@ -17,6 +17,7 @@ 1 3 Pak de volgende boot + true \ No newline at end of file diff --git a/openebs/tests/output/kv17_reason_basic.xml b/openebs/tests/output/kv17_reason_basic.xml index 7997b4ab..0fe349ad 100644 --- a/openebs/tests/output/kv17_reason_basic.xml +++ b/openebs/tests/output/kv17_reason_basic.xml @@ -13,6 +13,8 @@ 24_13 1 3_1 + true + false \ No newline at end of file diff --git a/openebs/tests/output/kv17_reason_full.xml b/openebs/tests/output/kv17_reason_full.xml index f4405a68..c387e0ad 100644 --- a/openebs/tests/output/kv17_reason_full.xml +++ b/openebs/tests/output/kv17_reason_full.xml @@ -15,6 +15,8 @@ 1 3_1 Kom na carnaval maar terug: Ole! + true + false \ No newline at end of file diff --git a/openebs/tests/test_command_verify.py b/openebs/tests/test_command_verify.py index 557156cb..a0fa06aa 100644 --- a/openebs/tests/test_command_verify.py +++ b/openebs/tests/test_command_verify.py @@ -17,9 +17,9 @@ def setUpClass(cls): cls.user = User.objects.create_user("test_kv8") # Create two fake sotps - stop_a = Kv1Stop(userstopcode=400, dataownercode='HTM', timingpointcode=400, name="Om de ene hoek", location=Point(1, 1)) - stop_b = Kv1Stop(userstopcode=401, dataownercode='HTM', timingpointcode=401, name="Om de ander hoek", location=Point(1, 1)) - stop_c = Kv1Stop(userstopcode=402, dataownercode='HTM', timingpointcode=3000402, name="In Lutjebroek", location=Point(1, 1)) + stop_a = Kv1Stop(userstopcode=400, dataownercode='HTM', timingpointcode=400, quaycoderef='NL:Q:400', name="Om de ene hoek", location=Point(1, 1)) + stop_b = Kv1Stop(userstopcode=401, dataownercode='HTM', timingpointcode=401, quaycoderef='NL:Q:401', name="Om de ander hoek", location=Point(1, 1)) + stop_c = Kv1Stop(userstopcode=402, dataownercode='HTM', timingpointcode=3000402, quaycoderef='NL:Q:402', name="In Lutjebroek", location=Point(1, 1)) stop_a.save() stop_b.save() stop_c.save() @@ -40,10 +40,13 @@ def test_message_verify_basic(self): row = { 'DataOwnerCode': 'HTM', 'TimingPointCode': 400, + 'QuayCode': 'NL:Q:400', 'MessageCodeDate': now().date().isoformat(), 'MessageStartTime': now(), 'MessageEndTime': now()+timedelta(hours=2), - 'MessageCodeNumber': '24' + 'MessageCodeNumber': '24', + 'OriginalMessageCodeDate': now().date().isoformat(), + 'OriginalMessageCodeNumber': '24' } # Method under test @@ -67,6 +70,9 @@ def test_message_add_basic(self): 'TimingPointCode': 400, 'MessageCodeDate': now().date().isoformat(), 'MessageCodeNumber': '25', + 'OriginalMessageCodeDate': now().date().isoformat(), + 'OriginalMessageCodeNumber': '25', + 'QuayCode': 'NL:Q:400', 'MessageContent': "Test content", 'MessageStartTime': now(), 'MessageEndTime': now()+timedelta(hours=2), @@ -125,6 +131,9 @@ def test_message_add_multiple(self): 'TimingPointCode': 400, 'MessageCodeDate': now().date().isoformat(), 'MessageCodeNumber': '37', + 'OriginalMessageCodeDate': now().date().isoformat(), + 'OriginalMessageCodeNumber': '37', + 'QuayCode': 'NL:Q:400', 'MessageContent': "Test content", 'MessageStartTime': now(), 'MessageEndTime': now()+timedelta(hours=2), @@ -147,9 +156,9 @@ def test_message_add_multiple(self): # Method under test self.testClass.process_message(row, False) - row['TimingPointCode'] = 401 + row['TimingPointCode'], row['QuayCode'] = 401, 'NL:Q:401' self.testClass.process_message(row, False) - row['TimingPointCode'] = 3000402 + row['TimingPointCode'], row['QuayCode'] = 3000402, 'NL:Q:402' self.testClass.process_message(row, False) self.assertEqual(Kv15Stopmessage.objects.count(), count+1) @@ -165,8 +174,8 @@ def test_message_add_multiple_dataowners(self): Now we have proper support for TPC, check we can have message with two linked stops """ - stop_d = Kv1Stop(userstopcode=403, dataownercode='HTM', timingpointcode=3000403, name="In Lutjebroek", location=Point(1, 1)) - stop_e = Kv1Stop(userstopcode=999, dataownercode='VTN', timingpointcode=3000403, name="In Lutjebrk", location=Point(1, 1)) + stop_d = Kv1Stop(userstopcode=403, dataownercode='HTM', timingpointcode=3000403, quaycoderef='NL:Q:403', name="In Lutjebroek", location=Point(1, 1)) + stop_e = Kv1Stop(userstopcode=999, dataownercode='VTN', timingpointcode=3000403, quaycoderef='NL:Q:403', name="In Lutjebrk", location=Point(1, 1)) stop_d.save() stop_e.save() @@ -176,6 +185,9 @@ def test_message_add_multiple_dataowners(self): 'TimingPointCode': 3000403, 'MessageCodeDate': now().date().isoformat(), 'MessageCodeNumber': '50', + 'OriginalMessageCodeDate': now().date().isoformat(), + 'OriginalMessageCodeNumber': '50', + 'QuayCode': 'NL:Q:403', 'MessageContent': "Test content some more for two vervoerders", 'MessageStartTime': now(), 'MessageEndTime': now()+timedelta(hours=2), @@ -219,6 +231,9 @@ def test_message_add_some_missing(self): 'TimingPointCode': 400, 'MessageCodeDate': now().date().isoformat(), 'MessageCodeNumber': '26', + 'OriginalMessageCodeDate': now().date().isoformat(), + 'OriginalMessageCodeNumber': '26', + 'QuayCode': 'NL:Q:400', 'MessageContent': "Test content", 'MessageStartTime': now(), 'MessageEndTime': now()+timedelta(hours=2), @@ -272,7 +287,8 @@ def test_message_deleted(self): Test an already deleted message is marked as such, and the end time is about now """ a = Kv15Stopmessage(dataownercode='HTM', user=self.user, messagecodedate=datetime(2013, 11, 17), - messagecodenumber=30) + messagecodenumber=30, kv8messagecodedate=datetime(2013, 11, 17), + kv8messagecodenumber=30) a.save() a.delete() a.set_status(MessageStatus.DELETED) @@ -283,6 +299,7 @@ def test_message_deleted(self): row = { 'DataOwnerCode': 'HTM', 'TimingPointCode': 400, + 'QuayCode': 'NL:Q:400', 'MessageCodeDate': '2013-11-17', 'MessageCodeNumber': '30' } @@ -299,7 +316,9 @@ def test_message_deletion(self): """ Test a message that was deleted """ - a = Kv15Stopmessage(dataownercode='HTM', user=self.user, messagecodedate=datetime(2013, 11, 17), messagecodenumber=31) + a = Kv15Stopmessage(dataownercode='HTM', user=self.user, messagecodedate=datetime(2013, 11, 17), + messagecodenumber=31, kv8messagecodedate=datetime(2013, 11, 17), + kv8messagecodenumber=31) a.save() self.assertEqual(a.status, MessageStatus.SAVED) self.assertEqual(a.isdeleted, False) @@ -309,6 +328,7 @@ def test_message_deletion(self): row = { 'DataOwnerCode': 'HTM', 'TimingPointCode': 400, + 'QuayCode': 'NL:Q:400', 'MessageCodeDate': '2013-11-17', 'MessageCodeNumber': '31' } @@ -333,13 +353,15 @@ def test_message_deletion_unknown(self): row = { 'DataOwnerCode': 'HTM', 'TimingPointCode': 400, + 'QuayCode': 'NL:Q:400', 'MessageCodeDate': '2013-11-17', 'MessageCodeNumber': '38' } # Method under test self.testClass.process_message(row, True) - a = Kv15Stopmessage.objects.get(messagecodenumber=38) + # Unknown messages are created with a fresh openEBS number, so find it by its KV8 one + a = Kv15Stopmessage.objects.get(kv8messagecodenumber=38) self.assertEqual(a.status, MessageStatus.DELETE_CONFIRMED) self.assertEqual(a.isdeleted, True) self.assertLess((now() - a.messageendtime), timedelta(seconds=30), "Time wasn't set right") @@ -354,7 +376,9 @@ def test_message_deletion_multiple(self): a = Kv15Stopmessage(dataownercode='HTM', user=self.user, messagecodedate=datetime(2013, 11, 17), - messagecodenumber=messagecodenumber) + messagecodenumber=messagecodenumber, + kv8messagecodedate=datetime(2013, 11, 17), + kv8messagecodenumber=messagecodenumber) a.save() self.assertEqual(a.status, MessageStatus.SAVED) self.assertEqual(a.isdeleted, False) @@ -363,12 +387,13 @@ def test_message_deletion_multiple(self): row = { 'DataOwnerCode': 'HTM', 'TimingPointCode': 400, + 'QuayCode': 'NL:Q:400', 'MessageCodeDate': '2013-11-17', 'MessageCodeNumber': str(messagecodenumber) } # Method under test self.testClass.process_message(row, True) - row['TimingPointCode'] = 401 + row['TimingPointCode'], row['QuayCode'] = 401, 'NL:Q:401' self.testClass.process_message(row, True) a = Kv15Stopmessage.objects.get(pk=a.pk) # Get latest from db @@ -384,7 +409,8 @@ def test_message_update(self): """ When we update a message, it sends a delete followed by an update - check that works """ - a = Kv15Stopmessage(dataownercode='HTM', user=self.user, messagecodedate=now().date(), messagecodenumber=32) + a = Kv15Stopmessage(dataownercode='HTM', user=self.user, messagecodedate=now().date(), messagecodenumber=32, + kv8messagecodedate=now().date(), kv8messagecodenumber=32) a.messagecontent = "Bla!" a.status = MessageStatus.CONFIRMED a.save() @@ -398,16 +424,21 @@ def test_message_update(self): delete_row = { 'DataOwnerCode': 'HTM', 'TimingPointCode': 400, + 'QuayCode': 'NL:Q:400', 'MessageCodeDate': now().date().isoformat(), 'MessageCodeNumber': '32' } add_row = { 'DataOwnerCode': 'HTM', 'TimingPointCode': 401, + 'QuayCode': 'NL:Q:401', 'MessageCodeDate': now().date().isoformat(), 'MessageStartTime': now(), 'MessageEndTime': now()+timedelta(hours=2), - 'MessageCodeNumber': '32' + 'MessageCodeNumber': '32', + # The update carries the message's new number, the delete the one it replaced + 'OriginalMessageCodeDate': now().date().isoformat(), + 'OriginalMessageCodeNumber': str(a.messagecodenumber) } # Method under test self.testClass.process_message(delete_row, True) @@ -431,6 +462,9 @@ def test_message_overrule_message(self): 'TimingPointCode': 400, 'MessageCodeDate': now().date().isoformat(), 'MessageCodeNumber': '35', + 'OriginalMessageCodeDate': now().date().isoformat(), + 'OriginalMessageCodeNumber': '35', + 'QuayCode': 'NL:Q:400', 'MessageContent': None, 'MessageStartTime': now(), 'MessageEndTime': now()+timedelta(hours=2), diff --git a/openebs/tests/test_kv15_xml.py b/openebs/tests/test_kv15_xml.py index 0aa4897d..b2a58b1f 100644 --- a/openebs/tests/test_kv15_xml.py +++ b/openebs/tests/test_kv15_xml.py @@ -53,7 +53,7 @@ def test_output_complete(self): m1.messagetype = MESSAGETYPE[1][0] m1.messagedurationtype = MESSAGEDURATIONTYPE[1][0] m1.reasontype = REASONTYPE[1][0] - m1.subreasontype = SUBREASONTYPE[1][0] + m1.subreasontype = '11' # Literal: the expected XML pins this, the choice order does not m1.reasoncontent = "Uitleg oorzaak" m1.effecttype = EFFECTTYPE[1][0] m1.subeffecttype = SUBEFFECTTYPE[1][0] diff --git a/openebs/tests/test_kv17_xml.py b/openebs/tests/test_kv17_xml.py index c5f93521..864fc1cc 100644 --- a/openebs/tests/test_kv17_xml.py +++ b/openebs/tests/test_kv17_xml.py @@ -3,7 +3,7 @@ from django.contrib.gis.geos import Point from kv1.models import Kv1Stop, Kv1Line, Kv1Journey -from openebs.models import Kv17Change, Kv17StopChange, Kv17JourneyChange +from openebs.models import Kv17Change, Kv17MutationMessage, Kv17JourneyChange from utils.xml_test import XmlTest @@ -72,8 +72,8 @@ def test_output_mutationmessage(self): change = Kv17Change(dataownercode='HTM', line=self.line, journey=journey, operatingday=datetime(2016, 4, 1), is_cancel=False) change.save() - stop_change = Kv17StopChange(change=change, type=5, stop=self.haltes[0], stoporder=1, - reasontype=3, subreasontype=7, reasoncontent="Boot is vol") + stop_change = Kv17MutationMessage(change=change, stop=self.haltes[0], passagesequencenumber=1, + reasontype=3, subreasontype=7, reasoncontent="Boot is vol") stop_change.save() # Have to pad with "DOSSIER" since otherwise we have invalid XML @@ -98,8 +98,8 @@ def test_output_mutationmessage_and_cancel(self): change = Kv17Change(dataownercode='HTM', line=self.line, journey=journey, operatingday=datetime(2016, 4, 1)) change.save() - stop_change = Kv17StopChange(change=change, type=5, stop=self.haltes[0], stoporder=1, - reasontype=3, subreasontype=7, reasoncontent="Boot is vol en vaart niet") + stop_change = Kv17MutationMessage(change=change, stop=self.haltes[0], passagesequencenumber=1, + reasontype=3, subreasontype=7, reasoncontent="Boot is vol en vaart niet") stop_change.save() self.assertXmlEqual("%s" % change.to_xml(), self.getCompareXML('openebs/tests/output/kv17_mutationmessage_cancel.xml')) diff --git a/openebs/tests/test_model_log.py b/openebs/tests/test_model_log.py index 9ac3c955..07f8b20b 100644 --- a/openebs/tests/test_model_log.py +++ b/openebs/tests/test_model_log.py @@ -12,4 +12,4 @@ def test_log_message(self): m.save() # This assigns an codenumber Kv15Log.create_log_entry(m, "10.0.0.1") - self.assertEquals(Kv15Log.objects.count(), 1) \ No newline at end of file + self.assertEqual(Kv15Log.objects.count(), 1) \ No newline at end of file diff --git a/openebs/tests/test_views_permissions.py b/openebs/tests/test_views_permissions.py index aee0f837..82736564 100644 --- a/openebs/tests/test_views_permissions.py +++ b/openebs/tests/test_views_permissions.py @@ -31,8 +31,8 @@ def setUp(self): def test_view_messages(self): response = self.client.get(reverse('msg_index')) self.assertEqual(response.status_code, 200) - self.assertEquals(len(response.context['active_list']), 0) - self.assertEquals(len(response.context['archive_list']), 0) + self.assertEqual(len(response.context['active_list']), 0) + self.assertEqual(len(response.context['archive_list']), 0) # Create NS message msg = TestUtils.create_message_default(self.user) @@ -46,27 +46,27 @@ def test_view_messages(self): response = self.client.get(reverse('msg_index')) self.assertEqual(response.status_code, 200) - self.assertEquals(len(response.context['active_list']), 1) - self.assertEquals(response.context['active_list'][0].messagecontent, "NS zet bussen in") - self.assertEquals(len(response.context['archive_list']), 0) + self.assertEqual(len(response.context['active_list']), 1) + self.assertEqual(response.context['active_list'][0].messagecontent, "NS zet bussen in") + self.assertEqual(len(response.context['archive_list']), 0) def test_view_messages_all(self): response = self.client.get(reverse('msg_index')+"?all=true") self.assertEqual(response.status_code, 200) - self.assertEquals(len(response.context['active_list']), Kv15Stopmessage.objects.filter(dataownercode='NS').count()) - self.assertEquals(len(response.context['archive_list']), 0) + self.assertEqual(len(response.context['active_list']), Kv15Stopmessage.objects.filter(dataownercode='NS').count()) + self.assertEqual(len(response.context['archive_list']), 0) view_all_perm = Permission.objects.get(codename='view_all') self.user.user_permissions.add(view_all_perm) response = self.client.get(reverse('msg_index')) self.assertEqual(response.status_code, 200) - self.assertEquals(len(response.context['active_list']), Kv15Stopmessage.objects.filter(messageendtime__gt=now(), + self.assertEqual(len(response.context['active_list']), Kv15Stopmessage.objects.filter(messageendtime__gt=now(), isdeleted=False).count()) - archive_count = Kv15Stopmessage.objects.filter(Q(messageendtime__lt=now) | Q(isdeleted=True), + archive_count = Kv15Stopmessage.objects.filter(Q(messageendtime__lt=now()) | Q(isdeleted=True), messagestarttime__gt=now() - timedelta(days=3)).count() - self.assertEquals(len(response.context['archive_list']), archive_count) + self.assertEqual(len(response.context['archive_list']), archive_count) self.user.user_permissions.remove(view_all_perm) self.user.save() diff --git a/utils/views.py b/utils/views.py index 774f39e8..0d961b42 100644 --- a/utils/views.py +++ b/utils/views.py @@ -85,8 +85,11 @@ def get_pushers(self, settings, defaults): for destination in sorted(settings, key=lambda k: k['priority']): if not destination['enabled']: continue - if self.message_type is None or self.message_type not in destination['endpoints']: + if self.message_type is None: raise ImproperlyConfigured("Endpoint type isn't registered") + if self.message_type not in destination['endpoints']: + # Not every subscriber takes every message type - ndovloket_rig has no KV6 + continue if self.namespace == '' or self.namespace is None: raise ImproperlyConfigured("Namespace isn't configured") if self.dossier == '' or self.dossier is None: From c46ca3579836b5ce6c096dd715fc5b9eb608c551 Mon Sep 17 00:00:00 2001 From: Joel Haasnoot Date: Wed, 5 Aug 2026 15:09:45 +0200 Subject: [PATCH 2/2] Add CI: tests and a smoke test on every PR Runs against a postgis service container: one job for the test suite, one that migrates, imports a small RID fixture, boots the server and checks the main pages load for a logged-in user - including that the map still carries its search box. Co-Authored-By: Claude Opus 5 --- .github/scripts/make_rid_fixture.sh | 18 ++++ .github/scripts/smoke.py | 80 ++++++++++++++++ .github/scripts/write_local_settings.sh | 22 +++++ .github/workflows/ci.yml | 119 ++++++++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100755 .github/scripts/make_rid_fixture.sh create mode 100755 .github/scripts/smoke.py create mode 100755 .github/scripts/write_local_settings.sh create mode 100644 .github/workflows/ci.yml diff --git a/.github/scripts/make_rid_fixture.sh b/.github/scripts/make_rid_fixture.sh new file mode 100755 index 00000000..5e381b5d --- /dev/null +++ b/.github/scripts/make_rid_fixture.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# A couple of lines and stops for import_rid, so the smoke test has data to show. +set -euo pipefail + +dir="$1" +mkdir -p "$dir" + +cat > "$dir/openebs_lines.csv" <<'EOF' +bison_id,publiccode,name +HTM:1,1,Scheveningen Noorderstrand - Delft Tanthof +HTM:9,9,Scheveningen Noorderstrand - Den Haag Vrederust +EOF + +cat > "$dir/openebs_stops.csv" <<'EOF' +operator_id,name,latitude,longitude,timingpointcode,quaycoderef +HTM:3100,Den Haag Centraal,52.080887,4.324971,31001000, +HTM:3200,Den Haag Hollands Spoor,52.069637,4.322389,31002000, +EOF diff --git a/.github/scripts/smoke.py b/.github/scripts/smoke.py new file mode 100755 index 00000000..2f95e0f6 --- /dev/null +++ b/.github/scripts/smoke.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Log in to a running server and check the main pages come back. + +Usage: smoke.py http://127.0.0.1:8000 +""" +import http.cookiejar +import os +import re +import sys +import urllib.parse +import urllib.request + +BASE = sys.argv[1].rstrip('/') +USERNAME = os.environ.get('SMOKE_USER', 'smoke') +PASSWORD = os.environ.get('SMOKE_PASSWORD', 'smoke-password') + +PAGES = [ + ('/bericht', None), + ('/bericht/nieuw', None), + ('/kaart', 'autocomplete_holder'), # The stop search box, see #234 + ('/haltes.geojson', 'FeatureCollection'), + ('/stop/search.json?q=Den+Haag', 'Den Haag'), + ('/scenario', None), + ('/ritaanpassing', None), + ('/admin/', None), +] + +opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())) +failures = [] + + +def fetch(path, data=None): + url = BASE + path + request = urllib.request.Request(url, data=data, headers={'Referer': url}) + with opener.open(request) as response: + return response.getcode(), response.read().decode('utf-8', 'replace') + + +def check(path, contains=None): + try: + status, body = fetch(path) + except Exception as error: # urllib raises on 4xx/5xx + failures.append('%s -> %s' % (path, error)) + print('FAIL %s (%s)' % (path, error)) + return + if status != 200: + failures.append('%s -> %s' % (path, status)) + elif contains and contains not in body: + failures.append('%s -> 200 but missing %r' % (path, contains)) + else: + print('ok %s (%s)' % (path, status)) + return + print('FAIL %s' % failures[-1]) + + +def login(): + _, body = fetch('/inloggen/') + match = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', body) + if not match: + sys.exit('no csrf token on the login page') + data = urllib.parse.urlencode({ + 'csrfmiddlewaretoken': match.group(1), + 'username': USERNAME, + 'password': PASSWORD, + }).encode() + _, body = fetch('/inloggen/', data=data) + if 'Uitloggen' not in body: + sys.exit('login as %s failed' % USERNAME) + print('ok logged in as %s' % USERNAME) + + +check('/inloggen/') +login() +for page, contains in PAGES: + check(page, contains) + +if failures: + sys.exit('\n%d check(s) failed:\n %s' % (len(failures), '\n '.join(failures))) +print('\nall checks passed') diff --git a/.github/scripts/write_local_settings.sh b/.github/scripts/write_local_settings.sh new file mode 100755 index 00000000..100c255b --- /dev/null +++ b/.github/scripts/write_local_settings.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# settings.py requires local_settings to exist - point it at the CI database. +set -euo pipefail + +cat > openebs2/local_settings.py <<'EOF' +DEBUG = True + +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] + +SOCIAL_LOGIN_ENABLED = False # No SSO provider in CI + +DATABASES = { + 'default': { + 'ENGINE': 'django.contrib.gis.db.backends.postgis', + 'NAME': 'openebs2', + 'USER': 'openebs', + 'PASSWORD': 'openebs', + 'HOST': '127.0.0.1', + 'PORT': '5432', + } +} +EOF diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..bd38214c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,119 @@ +name: CI + +on: + pull_request: + push: + branches: [master] + +env: + PYTHON_VERSION: '3.13' + +jobs: + test: + name: Tests + runs-on: ubuntu-latest + + services: + postgres: + image: postgis/postgis:17-3.5 + env: + POSTGRES_USER: openebs + POSTGRES_PASSWORD: openebs + POSTGRES_DB: openebs2 + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + + - name: Install GeoDjango system libraries + run: | + sudo apt-get update + sudo apt-get install -y binutils libproj-dev gdal-bin libgdal-dev libgeos-dev + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Write local_settings + run: .github/scripts/write_local_settings.sh + + - name: Run tests + run: python manage.py test + + smoke: + name: Smoke test + runs-on: ubuntu-latest + + services: + postgres: + image: postgis/postgis:17-3.5 + env: + POSTGRES_USER: openebs + POSTGRES_PASSWORD: openebs + POSTGRES_DB: openebs2 + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + + - name: Install GeoDjango system libraries + run: | + sudo apt-get update + sudo apt-get install -y binutils libproj-dev gdal-bin libgdal-dev libgeos-dev + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Write local_settings + run: .github/scripts/write_local_settings.sh + + - name: Migrate + run: python manage.py migrate --noinput + + - name: Import stop and line data + run: | + .github/scripts/make_rid_fixture.sh "${RUNNER_TEMP}/rid" + python manage.py import_rid "${RUNNER_TEMP}/rid" + + - name: Create the smoke test user + run: | + python manage.py createsuperuser --noinput --username smoke --email smoke@example.com + env: + DJANGO_SUPERUSER_PASSWORD: smoke-password + + - name: Start the server + run: | + python manage.py runserver 8000 --noreload > "${RUNNER_TEMP}/server.log" 2>&1 & + for i in $(seq 30); do + curl -sf -o /dev/null http://127.0.0.1:8000/inloggen/ && exit 0 + sleep 1 + done + echo "server did not come up"; cat "${RUNNER_TEMP}/server.log"; exit 1 + + - name: Check the pages load + run: python .github/scripts/smoke.py http://127.0.0.1:8000 + + - name: Server log + if: always() + run: cat "${RUNNER_TEMP}/server.log"