From e9aace626896f4ea4731116158a4af1aaa33ce57 Mon Sep 17 00:00:00 2001 From: scopesorting Date: Fri, 3 Nov 2023 19:45:55 +0100 Subject: [PATCH] creating stub objects for every single validation function. Unit tests are created for each function to check whether they return 'GREEN' whenever no violation is expected, or 'YELLOW'/'RED' when a rule violation is artificially forced. The test framework now successfully runs 116 unit tests. Adapted some validation functions, applied refactoring and solved potential obstacles along the way. At least from the perspective of unit tests, every function now works as expected. --- src/server/BreCal/__init__.py | 45 +- src/server/BreCal/database/sql_handler.py | 24 +- src/server/BreCal/stubs/__init__.py | 1 + src/server/BreCal/stubs/berth.py | 2 - src/server/BreCal/stubs/df_times.py | 78 ++ src/server/BreCal/stubs/times_agency.py | 8 + src/server/BreCal/stubs/times_bsmd.py | 8 + src/server/BreCal/stubs/times_full.py | 4 + src/server/BreCal/stubs/times_mooring.py | 8 + src/server/BreCal/stubs/times_pilot.py | 8 + .../BreCal/stubs/times_portauthority.py | 9 + src/server/BreCal/stubs/times_terminal.py | 8 + src/server/BreCal/stubs/times_tug.py | 8 + .../validators/validation_rule_functions.py | 38 +- src/server/tests/test_import_modules.py | 2 +- .../test_validation_rule_functions.py | 846 +++++++++++++++++- 16 files changed, 1033 insertions(+), 64 deletions(-) create mode 100644 src/server/BreCal/stubs/df_times.py create mode 100644 src/server/BreCal/stubs/times_bsmd.py create mode 100644 src/server/BreCal/stubs/times_tug.py diff --git a/src/server/BreCal/__init__.py b/src/server/BreCal/__init__.py index 1a6dc48..c4cf995 100644 --- a/src/server/BreCal/__init__.py +++ b/src/server/BreCal/__init__.py @@ -13,6 +13,25 @@ from .api import ships from .api import login from .api import user +from BreCal.brecal_utils.file_handling import get_project_root, ensure_path +from BreCal.brecal_utils.test_handling import execute_test_with_pytest, execute_coverage_test +from BreCal.brecal_utils.time_handling import difference_to_then + +from BreCal.validators.time_logic import TimeLogic +from BreCal.validators.validation_rules import ValidationRules +from BreCal.validators.schema_validation import validation_state_and_validation_name + +from BreCal.stubs.times_agency import get_times_agency +from BreCal.stubs.times_bsmd import get_times_bsmd +from BreCal.stubs.times_mooring import get_times_mooring +from BreCal.stubs.times_pilot import get_times_pilot +from BreCal.stubs.times_portauthority import get_times_port_authority +from BreCal.stubs.times_terminal import get_times_terminal +from BreCal.stubs.times_tug import get_times_tug +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.stubs.df_times import get_df_times + + def create_app(test_config=None): app = Flask(__name__, instance_relative_config=True) @@ -47,22 +66,24 @@ def create_app(test_config=None): return app -from BreCal.brecal_utils.file_handling import get_project_root, ensure_path -from BreCal.brecal_utils.test_handling import execute_test_with_pytest, execute_coverage_test -from BreCal.brecal_utils.time_handling import difference_to_then - -from BreCal.validators.time_logic import TimeLogic -from BreCal.validators.validation_rules import ValidationRules -from BreCal.validators.schema_validation import validation_state_and_validation_name - __all__ = [ - "get_project_root", - "ensure_path", - "execute_test_with_pytest", - "execute_coverage_test", + "get_project_root", + "ensure_path", + "execute_test_with_pytest", + "execute_coverage_test", "difference_to_then", "TimeLogic", "ValidationRules", "validation_state_and_validation_name", + + "get_times_agency", + "get_times_bsmd", + "get_times_mooring", + "get_times_pilot", + "get_times_port_authority", + "get_times_terminal", + "get_times_tug", + "get_times_full_simple", + "get_df_times", ] diff --git a/src/server/BreCal/database/sql_handler.py b/src/server/BreCal/database/sql_handler.py index 457b950..6c1acd2 100644 --- a/src/server/BreCal/database/sql_handler.py +++ b/src/server/BreCal/database/sql_handler.py @@ -221,6 +221,9 @@ class SQLHandler(): # filter out all NaN and NaT entries if non_null_column is not None: + # in the Pandas documentation, it says for .isnull(): + # "This function takes a scalar or array-like object and indicates whether values are missing + # (NaN in numeric arrays, None or NaN in object arrays, NaT in datetimelike)." df_times = df_times.loc[~df_times[non_null_column].isnull()] # NOT null filter # filter by the agency participant_type @@ -230,13 +233,20 @@ class SQLHandler(): def filter_df_by_key_value(self, df, key, value)->pd.DataFrame: return df.loc[df[key]==value] - def get_unique_ship_counts(self, all_df_times:pd.DataFrame, query:str, rounding:str="min", maximum_threshold=3): + def get_unique_ship_counts(self, all_df_times:pd.DataFrame, times_agency:pd.DataFrame, query:str, rounding:str="min", maximum_threshold=3): """given a dataframe of all agency times, get all unique ship counts, their values (datetime) and the string tags. returns a tuple (values,unique,counts)""" - # get values and optional: rounding - values = all_df_times.loc[:, query] + # optional: rounding if rounding is not None: - values = values.dt.round(rounding) # e.g., 'min' + all_df_times.loc[:, query] = all_df_times.loc[:, query].dt.round(rounding) # e.g., 'min' + query_time_agency = times_agency[query].iloc[0].round(rounding)# e.g., 'min' - unique, counts = np.unique(values, return_counts=True) - violation_state = np.any(np.greater(counts, maximum_threshold)) - return (values, unique, counts) + # after rounding, filter {all_df_times}, so only those, which match the current query are of interest + # takes 'times_agency' to sample, which value should match + all_df_times = all_df_times.loc[all_df_times[query]==query_time_agency] + + # finally, count all remaining entries + values = all_df_times.loc[:, query] + + # get unique entries and counts + counts = len(values) # unique, counts = np.unique(values, return_counts=True) + return counts # (values, unique, counts) diff --git a/src/server/BreCal/stubs/__init__.py b/src/server/BreCal/stubs/__init__.py index 83dd2ed..95578de 100644 --- a/src/server/BreCal/stubs/__init__.py +++ b/src/server/BreCal/stubs/__init__.py @@ -3,3 +3,4 @@ def generate_uuid1_int(): """# TODO: clarify, what kind of integer ID is used in mysql. Generates a proxy ID, which is used in the stubs""" from uuid import uuid1 return uuid1().int>>64 + diff --git a/src/server/BreCal/stubs/berth.py b/src/server/BreCal/stubs/berth.py index 240bcbe..2a782d7 100644 --- a/src/server/BreCal/stubs/berth.py +++ b/src/server/BreCal/stubs/berth.py @@ -7,7 +7,6 @@ def get_berth_simple(): # Note: #TODO: name, participant_id & lock state are arbitrary name = "Avangard Dalben" - participant_id = 1# e.g., Avangard lock = False owner_id = 1 # e.g., Avangard authority_id = 1 # e.g., Avangard @@ -19,7 +18,6 @@ def get_berth_simple(): berth = Berth( berth_id, name, - participant_id, lock, owner_id, authority_id, diff --git a/src/server/BreCal/stubs/df_times.py b/src/server/BreCal/stubs/df_times.py new file mode 100644 index 0000000..08d8fd7 --- /dev/null +++ b/src/server/BreCal/stubs/df_times.py @@ -0,0 +1,78 @@ +import pandas as pd +import random +import datetime + +from BreCal.stubs.times_agency import get_times_agency +from BreCal.stubs.times_bsmd import get_times_bsmd +from BreCal.stubs.times_mooring import get_times_mooring +from BreCal.stubs.times_pilot import get_times_pilot +from BreCal.stubs.times_portauthority import get_times_port_authority +from BreCal.stubs.times_terminal import get_times_terminal +from BreCal.stubs.times_tug import get_times_tug +from BreCal.database.enums import ParticipantType + +def get_df_times(shipcall=None): + """in case of providing a shipcall, one can read the id to set each times entry in the dataframe towards that shipcall id""" + df_times = pd.DataFrame([ + fct() + + for fct in [ + get_times_agency, + get_times_bsmd, + get_times_mooring, + get_times_pilot, + get_times_port_authority, + get_times_terminal, + get_times_tug + ] + ]) + if shipcall is not None: + df_times.loc[:,"shipcall_id"] = shipcall.id + return df_times + +def random_time_perturbation(df_times, query): + # random perturbations + population = [datetime.datetime.now(), None, pd.NaT] + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, query] = random.sample(population,k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.MOORING.value, query] = random.sample(population,k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.PORT_ADMINISTRATION.value, query] = random.sample(population,k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.PILOT.value, query] = random.sample(population,k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.TUG.value, query] = random.sample(population,k=1)[0] + return df_times + +def get_df_times_participants_disagree(query, shipcall=None, df_times = None): + if df_times is None: + df_times = get_df_times(shipcall) + df_times = random_time_perturbation(df_times=df_times, query=query) + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, query] = datetime.datetime.now()+datetime.timedelta(hours=2, minutes=14) + df_times.loc[df_times["participant_type"]==ParticipantType.PILOT.value, query] = datetime.datetime.now()+datetime.timedelta(hours=1, minutes=7) + + return df_times + +def build_stub_df_times(shipcall, query, reference_time): + """creates an artificial dataset, which simulates having too many shipcalls with too many identical times""" + df_times_a = get_df_times(shipcall) + df_times_a = df_times_a.loc[df_times_a["participant_type"]==ParticipantType.AGENCY.value] + df_times_a.loc[:,query] = reference_time + datetime.timedelta(seconds=17) + + df_times_b = get_df_times(shipcall) + df_times_b = df_times_b.loc[df_times_b["participant_type"]==ParticipantType.AGENCY.value] + df_times_b.loc[:,query] = reference_time + datetime.timedelta(seconds=21) + + df_times_c = get_df_times(shipcall) + df_times_c = df_times_c.loc[df_times_c["participant_type"]==ParticipantType.AGENCY.value] + df_times_c.loc[:,query] = reference_time + datetime.timedelta(seconds=26) + + df_times_d = get_df_times(shipcall) + df_times_d = df_times_d.loc[df_times_d["participant_type"]==ParticipantType.AGENCY.value] + df_times_d.loc[:,query] = reference_time + datetime.timedelta(seconds=28) + + df_times_e = get_df_times(shipcall) + df_times_e = df_times_e.loc[df_times_e["participant_type"]==ParticipantType.AGENCY.value] + df_times_e.loc[:,query] = reference_time + datetime.timedelta(seconds=29) + + return pd.concat([df_times_a, df_times_b, df_times_c, df_times_d, df_times_e],axis=0) + + diff --git a/src/server/BreCal/stubs/times_agency.py b/src/server/BreCal/stubs/times_agency.py index e69de29..31fc0e7 100644 --- a/src/server/BreCal/stubs/times_agency.py +++ b/src/server/BreCal/stubs/times_agency.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_agency(): + times_agency = get_times_full_simple() + times_agency.participant_type = ParticipantType.AGENCY.value + return times_agency + diff --git a/src/server/BreCal/stubs/times_bsmd.py b/src/server/BreCal/stubs/times_bsmd.py new file mode 100644 index 0000000..3f36e24 --- /dev/null +++ b/src/server/BreCal/stubs/times_bsmd.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_bsmd(): + times_bsmd = get_times_full_simple() + times_bsmd.participant_type = ParticipantType.BSMD.value + return times_bsmd + diff --git a/src/server/BreCal/stubs/times_full.py b/src/server/BreCal/stubs/times_full.py index 984f3db..f0176a1 100644 --- a/src/server/BreCal/stubs/times_full.py +++ b/src/server/BreCal/stubs/times_full.py @@ -3,9 +3,11 @@ this stub creates an example time object, where the times of every role are pres users will thereby be able to modify these values """ import datetime + from BreCal.stubs import generate_uuid1_int from BreCal.schemas.model import Times + def get_times_full_simple(): # only used for the stub base_time = datetime.datetime.now() @@ -65,3 +67,5 @@ def get_times_full_simple(): modified=modified, ) return times + + diff --git a/src/server/BreCal/stubs/times_mooring.py b/src/server/BreCal/stubs/times_mooring.py index e69de29..5e6570a 100644 --- a/src/server/BreCal/stubs/times_mooring.py +++ b/src/server/BreCal/stubs/times_mooring.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_mooring(): + times_mooring = get_times_full_simple() + times_mooring.participant_type = ParticipantType.MOORING.value + return times_mooring + diff --git a/src/server/BreCal/stubs/times_pilot.py b/src/server/BreCal/stubs/times_pilot.py index e69de29..1282133 100644 --- a/src/server/BreCal/stubs/times_pilot.py +++ b/src/server/BreCal/stubs/times_pilot.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_pilot(): + times_pilot = get_times_full_simple() + times_pilot.participant_type = ParticipantType.PILOT.value + return times_pilot + diff --git a/src/server/BreCal/stubs/times_portauthority.py b/src/server/BreCal/stubs/times_portauthority.py index e69de29..61ad4a5 100644 --- a/src/server/BreCal/stubs/times_portauthority.py +++ b/src/server/BreCal/stubs/times_portauthority.py @@ -0,0 +1,9 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_port_authority(): + times_port_authority = get_times_full_simple() + times_port_authority.participant_type = ParticipantType.PORT_ADMINISTRATION.value + return times_port_authority + + diff --git a/src/server/BreCal/stubs/times_terminal.py b/src/server/BreCal/stubs/times_terminal.py index e69de29..549ddb5 100644 --- a/src/server/BreCal/stubs/times_terminal.py +++ b/src/server/BreCal/stubs/times_terminal.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_terminal(): + times_terminal = get_times_full_simple() + times_terminal.participant_type = ParticipantType.TERMINAL.value + return times_terminal + diff --git a/src/server/BreCal/stubs/times_tug.py b/src/server/BreCal/stubs/times_tug.py new file mode 100644 index 0000000..0e8cdd1 --- /dev/null +++ b/src/server/BreCal/stubs/times_tug.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_tug(): + times_tug = get_times_full_simple() + times_tug.participant_type = ParticipantType.TUG.value + return times_tug + diff --git a/src/server/BreCal/validators/validation_rule_functions.py b/src/server/BreCal/validators/validation_rule_functions.py index 9e51eaf..fec7a7c 100644 --- a/src/server/BreCal/validators/validation_rule_functions.py +++ b/src/server/BreCal/validators/validation_rule_functions.py @@ -73,17 +73,22 @@ class ValidationRuleBaseFunctions(): when the query_time lays in the future, the delta is positive returns a violation state depending on whether the delta is - Violation, if: 0 >= delta > threshold + Violation, if: 0 >= delta <= threshold When the key time is defined (not None), there is no violation. Returns False options: query_time: will be used to measure the time difference of 'now' until the query time key_time: will be used to check, whether the respective key already has a value - threshold: threshold where a time difference becomes crucial. When the delta is below the threshold, a violation might occur + threshold: threshold where a time difference becomes crucial. When the delta is below the threshold, a violation might occur (minutes) """ # rule is not applicable -> return 'GREEN' - if self.check_is_not_a_time_or_is_none(key_time) or self.check_is_not_a_time_or_is_none(query_time): + # rule is only applicable, when 'key_time' is not defined (neither None, nor pd.NaT) + if (key_time is not None) and (key_time is not pd.NaT): + return False + + # when query_time is not valid, the rule cannot be applied + if self.check_is_not_a_time_or_is_none(query_time): return False # otherwise, this rule applies and the difference between 'now' and the query time is measured @@ -91,7 +96,7 @@ class ValidationRuleBaseFunctions(): # a violation occurs, when the delta (in minutes) exceeds the specified threshold of a participant # to prevent past-events from triggering violations, negative values are ignored - # Violation, if 0 >= delta >= threshold + # Violation, if 0 <= delta <= threshold violation_state = (delta >= 0) and (delta<=threshold) return violation_state @@ -141,7 +146,7 @@ class ValidationRuleBaseFunctions(): violation_state = n_unique_times!=1 return violation_state - def check_unique_shipcall_counts(self, query:str, rounding="min", maximum_threshold=3)->bool: + def check_unique_shipcall_counts(self, query:str, times_agency:pd.DataFrame, rounding="min", maximum_threshold=3, all_times_agency=None)->bool: """ # base function for all validation rules in the group {0005} A&B @@ -150,10 +155,11 @@ class ValidationRuleBaseFunctions(): """ # filter the df: keep only times_agents # filter out all NaN and NaT entries - times_agency = self.sql_handler.get_times_for_agency(non_null_column=query) + if all_times_agency is None: + all_times_agency = self.sql_handler.get_times_for_agency(non_null_column=query) - # get values and optionally round the values - (values, unique, counts) = self.sql_handler.get_unique_ship_counts(all_df_times=times_agency, query=query, rounding=rounding, maximum_threshold=maximum_threshold) + # get values and optionally round the values (internally) + counts = self.sql_handler.get_unique_ship_counts(all_df_times=all_times_agency, times_agency=times_agency, query=query, rounding=rounding, maximum_threshold=maximum_threshold) # when ANY of the unique values exceeds the threshold, a violation is observed violation_state = np.any(np.greater(counts, maximum_threshold)) @@ -651,7 +657,7 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions): return (StatusFlags.GREEN, None) # check, whether the start of operations is AFTER the estimated arrival time - violation_state = times_terminal.operations_start StatusFlags.GREEN.value, f"a violation must be identified" - assert description is not None, f"a violation description must be identified" + threshold = 60*5 + violation_state = vr.check_time_delta_violation_query_time_to_now(query_time=query_time, key_time=key_time, threshold=threshold) + assert not violation_state, f"the key time is filled in, so there should not be a violation" return +def test_check_time_delta_violation_query_time_to_now_no_key_time_but_event_in_distant_future(build_sql_proxy_connection): + vr = build_sql_proxy_connection["vr"] + + # ship arrives in three hours, while the threshold for an alert is (e.g.) 5 hours + # key time is given, so the function should always return False (no violation) + # query time (-> delta) & threshold have the same time -> no violation + query_time = datetime.datetime.now() + datetime.timedelta(hours=5, seconds=10) # when the delta & threshold are identical, microseconds between checking the time and defining it here, raise the violation + key_time = None + + threshold = 60*5 + violation_state = vr.check_time_delta_violation_query_time_to_now(query_time=query_time, key_time=key_time, threshold=threshold) + assert not violation_state, f"the event is still far enough away, so there should not be a violation." + return + +def test_check_time_delta_violation_query_time_to_now_key_time_is_none(build_sql_proxy_connection): + vr = build_sql_proxy_connection["vr"] + + # ship arrives in three hours, while the threshold for an alert is (e.g.) 5 hours + # key time is given, so the function should always return False (no violation) + query_time = datetime.datetime.now() + datetime.timedelta(hours=3) + key_time = None + + threshold = 60*5 # minutes + violation_state = vr.check_time_delta_violation_query_time_to_now(query_time=query_time, key_time=key_time, threshold=threshold) + assert violation_state, f"when the key time is not filled in and the query time is 'dangerously close', there should be a violation to indicate the traffic state" + return + +def test_check_time_delta_violation_query_time_to_now_key_time_is_pd_nat(build_sql_proxy_connection): + vr = build_sql_proxy_connection["vr"] + + # ship arrives in three hours, while the threshold for an alert is (e.g.) 5 hours + # key time is given, so the function should always return False (no violation) + query_time = datetime.datetime.now() + datetime.timedelta(hours=3) + key_time = pd.NaT + + threshold = 60*5 # minutes + violation_state = vr.check_time_delta_violation_query_time_to_now(query_time=query_time, key_time=key_time, threshold=threshold) + assert violation_state, f"when the key time is not filled in and the query time is 'dangerously close', there should be a violation to indicate the traffic state" + return + + + +def test_validation_rule_fct_missing_time_agency_berth_eta__missing_time_agency_no_violation(build_sql_proxy_connection): + """0001-A validation_rule_fct_missing_time_agency_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # artificially remove the agency, so the function is properly checked + df_times = df_times.loc[df_times["participant_type"]!=ParticipantType.AGENCY.value] + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_agency_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the agency's entry is not present" + assert msg is None, f"with a 'green' state, there should be no message returned" + return + +def test_validation_rule_fct_missing_time_agency_berth_eta__shipcall_eta_dangerously_close_no_times_agency(build_sql_proxy_connection): + """0001-A validation_rule_fct_missing_time_agency_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # the shipcall happens 'soon' + shipcall.eta = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.AGENCY-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_agency_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the agency's entry is not present and the shipcall takes place soon" + return + +def test_validation_rule_fct_missing_time_agency_berth_eta__shipcall_eta_distant_enough_no_times_agency(build_sql_proxy_connection): + """0001-A validation_rule_fct_missing_time_agency_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # the shipcall happens 'soon' + shipcall.eta = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.AGENCY+10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_agency_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'yellow', because the agency's entry is not present and the shipcall takes place soon" + return + +def test_validation_rule_fct_missing_time_agency_berth_eta__shipcall_eta_is_undefined_agency_eta_is_defined(build_sql_proxy_connection): + """0001-A validation_rule_fct_missing_time_agency_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # the shipcall is undefined + shipcall.eta = None + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_agency_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'yellow', because the agency's entry is not present and the shipcall takes place soon" + return + +def test_validation_rule_fct_missing_time_agency_berth_etd__shipcall_etd_is_undefined_agency_etd_is_defined(build_sql_proxy_connection): + """0001-B validation_rule_fct_missing_time_agency_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # the shipcall etd is 'soon' + shipcall.etd = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.AGENCY-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_agency_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the agency's entry is not present and the shipcall takes place soon" + return + +def test_validation_rule_fct_missing_time_mooring_berth_eta__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-C validation_rule_fct_missing_time_mooring_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.MOORING-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.MOORING.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_mooring_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_mooring_berth_etd__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-D validation_rule_fct_missing_time_mooring_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.MOORING-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.MOORING.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_mooring_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_portadministration_berth_eta__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-F validation_rule_fct_missing_time_portadministration_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.PORT_ADMINISTRATION-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.PORT_ADMINISTRATION.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_portadministration_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_portadministration_berth_etd__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-G validation_rule_fct_missing_time_portadministration_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.PORT_ADMINISTRATION-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.PORT_ADMINISTRATION.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_portadministration_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + +def test_validation_rule_fct_missing_time_pilot_berth_eta__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-H validation_rule_fct_missing_time_pilot_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.PILOT-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.PILOT.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_pilot_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + +def test_validation_rule_fct_missing_time_pilot_berth_etd__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-I validation_rule_fct_missing_time_pilot_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.PILOT-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.PILOT.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_pilot_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_tug_berth_eta__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-J validation_rule_fct_missing_time_tug_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.TUG-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.TUG.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_tug_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_tug_berth_etd__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-K validation_rule_fct_missing_time_tug_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.TUG-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.TUG.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_tug_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_terminal_berth_eta__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-L validation_rule_fct_missing_time_terminal_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.TERMINAL-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_terminal_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_terminal_berth_etd__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-M validation_rule_fct_missing_time_terminal_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.TERMINAL-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_terminal_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + +def test_validation_rule_fct_shipcall_incoming_participants_disagree_on_eta__participants_disagree_on_time_but_different_shipcall_type(build_sql_proxy_connection): + """0002-A validation_rule_fct_shipcall_incoming_participants_disagree_on_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to NOT match the function -> returns 'green' + query = "eta_berth" + shipcall.type = ShipcallType.SHIFTING.value + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall) + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_incoming_participants_disagree_on_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the shipcall type does not match the function" + return + + +def test_validation_rule_fct_shipcall_incoming_participants_disagree_on_eta__participants_disagree_on_time(build_sql_proxy_connection): + """0002-A validation_rule_fct_shipcall_incoming_participants_disagree_on_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to match the function + query = "eta_berth" + shipcall.type = ShipcallType.INCOMING.value + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall) # makes sure that there is disagreement among participants + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_incoming_participants_disagree_on_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.RED, f"function should return 'red', because agency and pilot disagree on the query" + return + +def test_validation_rule_fct_shipcall_incoming_participants_disagree_on_eta__participants_agree_on_time(build_sql_proxy_connection): + """0002-A validation_rule_fct_shipcall_incoming_participants_disagree_on_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to NOT match the function -> returns 'green' + query = "eta_berth" + shipcall.type = ShipcallType.INCOMING.value + df_times = get_df_times(shipcall) + df_times.loc[:,query] = datetime.datetime.now() + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_incoming_participants_disagree_on_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the participants fully agree on the time" + return + +def test_validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd__participants_disagree_on_time_but_different_shipcall_type(build_sql_proxy_connection): + """0002-B validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to NOT match the function -> returns 'green' + query = "etd_berth" + shipcall.type = ShipcallType.SHIFTING.value + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall) + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the shipcall type does not match the function" + return + + +def test_validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd__participants_disagree_on_time(build_sql_proxy_connection): + """0002-B validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to match the function + query = "etd_berth" + shipcall.type = ShipcallType.OUTGOING.value + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall) # makes sure that there is disagreement among participants + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.RED, f"function should return 'red', because agency and pilot disagree on the query" + return + +def test_validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd__participants_agree_on_time(build_sql_proxy_connection): + """0002-B validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to NOT match the function -> returns 'green' + query = "etd_berth" + shipcall.type = ShipcallType.OUTGOING.value + df_times = get_df_times(shipcall) + df_times.loc[:,query] = datetime.datetime.now() + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the participants fully agree on the time" + return + + + +def test_validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd__participants_disagree_on_time_but_different_shipcall_type(build_sql_proxy_connection): + """0002-C validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + shipcall.type = ShipcallType.INCOMING.value + df_times = get_df_times(shipcall) + + # set shipcall type to match the function + query = "eta_berth" + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall, df_times=df_times) # makes sure that there is disagreement among participants + + # set shipcall type to match the function + query = "etd_berth" + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall, df_times=df_times) # makes sure that there is disagreement among participants + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the shipcall type does not match the function" + return + +def test_validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd__participants_disagree_on_time(build_sql_proxy_connection): + """0002-C validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + shipcall.type = ShipcallType.SHIFTING.value + df_times = get_df_times(shipcall) + + # set shipcall type to match the function + query = "eta_berth" + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall, df_times=df_times) # makes sure that there is disagreement among participants + + # set shipcall type to match the function + query = "etd_berth" + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall, df_times=df_times) # makes sure that there is disagreement among participants + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.RED, f"function should return 'red', because agency and pilot disagree on the query" + return + +def test_validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd__participants_agree_on_time(build_sql_proxy_connection): + """0002-C validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + shipcall.type = ShipcallType.SHIFTING.value + df_times = get_df_times(shipcall) + + # set shipcall type to match the function + query = "eta_berth" + df_times.loc[:,query] = datetime.datetime.now() + + # set shipcall type to match the function + query = "etd_berth" + df_times.loc[:,query] = datetime.datetime.now() + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the participants fully agree on the time" + return + +def test_validation_rule_fct_eta_time_not_in_operation_window__times_dont_match(build_sql_proxy_connection): + """0003-A validation_rule_fct_eta_time_not_in_operation_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() # reference time for easier readability + + # the planned operations_start is before eta_berth (by one minute in this case) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = t0_time + datetime.timedelta(minutes=1) + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_start"] = t0_time + datetime.timedelta(minutes=0) + + (code, msg) = vr.validation_rule_fct_eta_time_not_in_operation_window(shipcall, df_times) + assert code==StatusFlags.RED, f"status flag should be 'red', because the planned operations start is BEFORE the estimated time of arrival for the shipcall" + return + +def test_validation_rule_fct_etd_time_not_in_operation_window__times_dont_match(build_sql_proxy_connection): + """0003-B validation_rule_fct_etd_time_not_in_operation_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() # reference time for easier readability + + # the planned operations_end is after etd_berth (by one minute in this case) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = t0_time + datetime.timedelta(hours=1) + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_end"] = t0_time+datetime.timedelta(hours=1, minutes=1) + + + (code, msg) = vr.validation_rule_fct_etd_time_not_in_operation_window(shipcall, df_times) + assert code==StatusFlags.RED, f"status flag should be 'red', because the planned operations end is AFTER the estimated time of departure for the shipcall" + return + +def test_validation_rule_fct_eta_time_not_in_operation_window_and_validation_rule_fct_etd_time_not_in_operation_window__always_okay(build_sql_proxy_connection): + """ + 0003-A validation_rule_fct_eta_time_not_in_operation_window + 0003-B validation_rule_fct_etd_time_not_in_operation_window + """ + vr = build_sql_proxy_connection['vr'] + import random + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() + + # 10 random permutations of None/pd.NaT/suitable values + # each of these combinations is okay and should return a 'green' state + for _i in range(10): + # eta_berth & operations start + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = random.sample([None, pd.NaT, t0_time + datetime.timedelta(minutes=0)],k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_start"] = random.sample([None, pd.NaT, t0_time + datetime.timedelta(minutes=0)], k=1)[0] + + # etd_berth & operations start + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = random.sample([None, pd.NaT, t0_time + datetime.timedelta(hours=1)],k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_end"] = random.sample([None, pd.NaT, t0_time+datetime.timedelta(hours=1)],k=1)[0] + + (code, msg) = vr.validation_rule_fct_eta_time_not_in_operation_window(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.GREEN, f"status flag should be 'green', as any of these perturbations sets operation & estimated time to be on par ot one the values missed" + (code, msg) = vr.validation_rule_fct_etd_time_not_in_operation_window(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.GREEN, f"status flag should be 'green', as any of these perturbations sets operation & estimated time to be on par ot one the values missed" + return + +def test_validation_rule_fct_eta_time_not_in_tidal_window__is_okay(build_sql_proxy_connection): + """0004-A validation_rule_fct_eta_time_not_in_tidal_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() + + # tidal window: [t0 +1min, t0 +1hr) + # eta berth: + shipcall.tidal_window_from = t0_time + datetime.timedelta(minutes=1) + shipcall.tidal_window_to = t0_time + datetime.timedelta(hours=1) + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = t0_time + datetime.timedelta(minutes=1) + (code, msg) = vr.validation_rule_fct_eta_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.GREEN, f"state should be 'green', because eta_berth matches precisely the tidal_window_from" + + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = t0_time + datetime.timedelta(hours=1) + (code, msg) = vr.validation_rule_fct_eta_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.GREEN, f"state should be 'green', because eta_berth matches precisely the tidal_window_to (in this case, the etd_berth would likely cause an issue)" + return + +def test_validation_rule_fct_eta_time_not_in_tidal_window__eta_outside_tidal_window(build_sql_proxy_connection): + """0004-A validation_rule_fct_eta_time_not_in_tidal_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() + + # tidal window: [t0 +1min, t0 +1hr) + # eta berth: t0+0min + shipcall.tidal_window_from = t0_time + datetime.timedelta(minutes=1) + shipcall.tidal_window_to = t0_time + datetime.timedelta(hours=1) + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = t0_time + datetime.timedelta(minutes=0) + (code, msg) = vr.validation_rule_fct_eta_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.RED, f"state should be 'red', eta_berth takes place before the tidal window" + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = t0_time + datetime.timedelta(hours=1, minutes=1) + (code, msg) = vr.validation_rule_fct_eta_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.RED, f"state should be 'red', eta_berth takes place after the tidal window" + return + + + +def test_validation_rule_fct_etd_time_not_in_tidal_window__is_okay(build_sql_proxy_connection): + """0004-B validation_rule_fct_etd_time_not_in_tidal_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() + + # tidal window: [t0 +1min, t0 +1hr) + # etd berth: + shipcall.tidal_window_from = t0_time + datetime.timedelta(minutes=1) + shipcall.tidal_window_to = t0_time + datetime.timedelta(hours=1) + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = t0_time + datetime.timedelta(minutes=1) + (code, msg) = vr.validation_rule_fct_etd_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.GREEN, f"state should be 'green', because etd_berth matches precisely the tidal_window_from" + + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = t0_time + datetime.timedelta(hours=1) + (code, msg) = vr.validation_rule_fct_etd_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.GREEN, f"state should be 'green', because etd_berth matches precisely the tidal_window_to (in this case, the etd_berth would likely cause an issue)" + return + +def test_validation_rule_fct_etd_time_not_in_tidal_window__etd_outside_tidal_window(build_sql_proxy_connection): + """0004-B validation_rule_fct_etd_time_not_in_tidal_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() + + # tidal window: [t0 +1min, t0 +1hr) + # etd berth: + shipcall.tidal_window_from = t0_time + datetime.timedelta(minutes=1) + shipcall.tidal_window_to = t0_time + datetime.timedelta(hours=1) + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = t0_time + datetime.timedelta(minutes=0) + (code, msg) = vr.validation_rule_fct_etd_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.RED, f"state should be 'red', etd_berth takes place before the tidal window" + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = t0_time + datetime.timedelta(hours=1, minutes=1) + (code, msg) = vr.validation_rule_fct_etd_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.RED, f"state should be 'red', etd_berth takes place after the tidal window" + return + +def test_validation_rule_fct_too_many_identical_eta_times__is_violated_by_too_many_identical_times(build_sql_proxy_connection): + """0005-A validation_rule_fct_too_many_identical_eta_times""" + vr = build_sql_proxy_connection['vr'] + query = "eta_berth" + + reference_time = pd.Timestamp(datetime.datetime.now()) + reference_time = reference_time.round("min") + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value] + df_times.loc[:,query] = reference_time + datetime.timedelta(seconds=12) + + all_times_df = build_stub_df_times(shipcall, query, reference_time) + (code, msg) = vr.validation_rule_fct_too_many_identical_eta_times(shipcall=shipcall, df_times=df_times, all_times_agency=all_times_df) + assert code == StatusFlags.YELLOW, f"status should be 'yellow', because the artificial 'all_times_df' contains five shipcalls with identical times, which exceeds the threshold" + return + +def test_validation_rule_fct_too_many_identical_etd_times__is_violated_by_too_many_identical_times(build_sql_proxy_connection): + """0005-B validation_rule_fct_too_many_identical_etd_times""" + vr = build_sql_proxy_connection['vr'] + query = "etd_berth" + + reference_time = pd.Timestamp(datetime.datetime.now()) + reference_time = reference_time.round("min") + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value] + df_times.loc[:,query] = reference_time + datetime.timedelta(seconds=12) + + all_times_df = build_stub_df_times(shipcall, query, reference_time) + (code, msg) = vr.validation_rule_fct_too_many_identical_etd_times(shipcall=shipcall, df_times=df_times, all_times_agency=all_times_df) + assert code == StatusFlags.YELLOW, f"status should be 'yellow', because the artificial 'all_times_df' contains five shipcalls with identical times, which exceeds the threshold" + return + +def test_validation_rule_fct_agency_and_terminal_berth_id_disagreement__agency_and_terminal_agree(build_sql_proxy_connection): + """0006-A validation_rule_fct_agency_and_terminal_berth_id_disagreement""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "berth_id"] = 143 + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "berth_id"] = 143 + + (code, msg) = vr.validation_rule_fct_agency_and_terminal_berth_id_disagreement(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.GREEN, f"status should be 'green', because agency and terminal agree on the selected berth id" + return + +def test_validation_rule_fct_agency_and_terminal_berth_id_disagreement__agency_and_terminal_disagree(build_sql_proxy_connection): + """0006-A validation_rule_fct_agency_and_terminal_berth_id_disagreement""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "berth_id"] = 143 + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "berth_id"] = 145 + + (code, msg) = vr.validation_rule_fct_agency_and_terminal_berth_id_disagreement(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.YELLOW, f"status should be 'yellow', because agency and terminal do not agree on the selected berth id" + return + +def test_validation_rule_fct_agency_and_terminal_pier_side_disagreement__agency_and_terminal_agree(build_sql_proxy_connection): + """0006-B validation_rule_fct_agency_and_terminal_pier_side_disagreement""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "pier_side"] = True + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "pier_side"] = True + + (code, msg) = vr.validation_rule_fct_agency_and_terminal_pier_side_disagreement(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.GREEN, f"status should be 'green', because agency and terminal agree on the selected pier side" + return + +def test_validation_rule_fct_agency_and_terminal_pier_side_disagreement__agency_and_terminal_disagree(build_sql_proxy_connection): + """0006-B validation_rule_fct_agency_and_terminal_pier_side_disagreement""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "pier_side"] = True + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "pier_side"] = False + + (code, msg) = vr.validation_rule_fct_agency_and_terminal_pier_side_disagreement(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.YELLOW, f"status should be 'yellow', because agency and terminal do not agree on the selected pier side" + return + + def test_validation_rule_fct_agency_and_terminal_pier_side_agreement(build_sql_proxy_connection): """#0006-A validation_rule_fct_agency_and_terminal_pier_side_disagreement""" @@ -89,8 +853,36 @@ def test_validation_rule_fct_agency_and_terminal_pier_side_agreement(build_sql_p assert description is None, f"no violation should be observed" return +def test_validation_rule_fct_agency_and_terminal_pier_side_disagreement(build_sql_proxy_connection): + """#0006-A validation_rule_fct_agency_and_terminal_pier_side_disagreement""" + import pandas as pd + from BreCal.stubs.times_full import get_times_full_simple + from BreCal.stubs.shipcall import get_shipcall_simple + from BreCal.database.enums import ParticipantType + from BreCal.database.enums import StatusFlags + vr = build_sql_proxy_connection["vr"] + shipcall = get_shipcall_simple() + t1 = get_times_full_simple() + t2 = get_times_full_simple() + + # roles: agency & terminal + t1.participant_type = ParticipantType.AGENCY.value + t2.participant_type = ParticipantType.TERMINAL.value + + # disagreement + t1.pier_side = True + t2.pier_side = False + + time_objects = [t1, t2] + df_times = pd.DataFrame.from_records([to_.__dict__ for to_ in time_objects]) + df_times.set_index('id',inplace=True) + + (state, description) = vr.validation_rule_fct_agency_and_terminal_pier_side_disagreement(shipcall, df_times) + assert state.value > StatusFlags.GREEN.value, f"a violation must be identified" + assert description is not None, f"a violation description must be identified" + return def test_validation_rule_fct_agency_and_terminal_berth_id_disagreement(build_sql_proxy_connection): """#0006-B validation_rule_fct_agency_and_terminal_pier_side_disagreement"""