diff --git a/rota/shifts.py b/rota/shifts.py
index 07b4951..b9f5c13 100644
--- a/rota/shifts.py
+++ b/rota/shifts.py
@@ -1,6 +1,5 @@
from calendar import week
import datetime
-from distutils.log import debug
import itertools
from typing import Dict, Iterable, List, Sequence, Tuple, Set, Any, Type, Literal
import time
@@ -9,10 +8,12 @@ from pydantic import BaseModel, Extra, constr
from pathlib import Path
+import uuid
+
import datetime
from pyomo.environ import *
-from pyomo.opt import SolverFactory
+from pyomo.opt import SolverFactory, TerminationCondition, SolverStatus
import urllib.parse
from rota.workers import Worker
@@ -211,7 +212,7 @@ class RotaBuilder(object):
# self.night_blocks = ["weekday", "weekend", "none"]
- self.sites = None
+ self.sites: set = set()
self.balance_offset_modifier = balance_offset_modifier
self.ltft_balance_offset = ltft_balance_offset
@@ -246,6 +247,12 @@ class RotaBuilder(object):
"avoid_shifts_by_worker_names": [],
}
+ self.terminate_on_warning = [
+ "Worker/duplicate id",
+ "Worker/duplicate name",
+ "Worker/no valid shifts",
+ ]
+
self.results = None
self.warnings: list[tuple[str, str]] = []
@@ -294,13 +301,14 @@ class RotaBuilder(object):
self.unavailable_to_work_reason = {}
self.pref_not_to_work = {}
self.pref_not_to_work_reason = {}
- self.work_requests: set[Tuple[str, WeekInt, DayStr, ShiftName]] = set()
+ self.work_requests: set[
+ Tuple[str | uuid.UUID | int, WeekInt, DayStr, ShiftName]
+ ] = set()
self.work_requests_map = {}
def solve_model(
self,
- solver: str = "cbc",
- use_neos: bool = False,
+ solver: str = "appsi_highs",
options: dict = {},
debug_if_fail: bool = False,
):
@@ -314,41 +322,46 @@ class RotaBuilder(object):
if "threads" in options:
options.pop("threads")
self.opt = SolverFactory(solver, executable="scip")
+ elif solver == "appsi_highs":
+ self.opt = SolverFactory(solver)
+ if "seconds" in options:
+ options["time_limit"] = options.pop("seconds")
+ if "ratio" in options:
+ options["mip_rel_gap"] = options.pop("ratio")
else:
self.opt = SolverFactory(solver)
try:
console.print("Solving")
+ console.print(f"Options: {options}")
log_file = f"logs/{self.name}_{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}.log"
- if use_neos:
- solver_manager = SolverManagerFactory("neos") # Solve using neos server
- # results = solver_manager.solve(Rota.model, opt=opt, logfile="test.log")
- results = solver_manager.solve(
- self.model,
- keepfiles=True,
- tee=True,
- opt=self.opt,
- logfile=log_file,
- )
- else:
- results = self.opt.solve(
- self.model,
- tee=True,
- options=options,
- # options={
- # "threads": 10,
- # },
- logfile=log_file,
- keepfiles=True,
- )
+ results = self.opt.solve(
+ self.model,
+ tee=True,
+ options=options,
+ # options={
+ # "threads": 10,
+ # },
+ # logfile=log_file,
+ keepfiles=True,
+ load_solutions=False,
+ )
except KeyboardInterrupt:
return
pass
+ if (
+ not results.solver.termination_condition == TerminationCondition.infeasible
+
+ #and not results.solver.status == SolverStatus.aborted
+ ):
+ self.model.solutions.load_from(results)
+
self.results = results
console.print(f"Complete - outcome: {results.solver.status}")
+ console.print(f"Termination condition: {results.solver.termination_condition}")
if results.solver.status != "ok" and debug_if_fail:
console.print(f"Attempting each shift individually")
@@ -417,7 +430,7 @@ class RotaBuilder(object):
solve=True,
debug_if_fail: bool = False,
export=False,
- solver="cbc",
+ solver="appsi_highs",
):
self.run_start_time = datetime.datetime.now()
@@ -1305,7 +1318,15 @@ class RotaBuilder(object):
if self.use_shift_balance_extra:
if shift.name in worker.shift_balance_extra:
extra = worker.shift_balance_extra[shift.name]
- target_shifts = target_shifts + extra
+
+ # TODO look at how this affects allocation (how does it affect fte)
+ match extra:
+ case "double":
+ target_shifts = target_shifts * 2
+ case "half":
+ target_shifts = target_shifts / 2
+ case _:
+ target_shifts = target_shifts + extra
# print(worker.name, shift.name, target_shifts)
worker.shift_target_number[shift.name] = target_shifts
@@ -2461,6 +2482,15 @@ class RotaBuilder(object):
print(f"[bold red]WARNING:[/bold red] {warning_type} - {message}")
self.warnings.append((warning_type, message))
+ if warning_type in self.terminate_on_warning:
+ raise WarningTermination(warning_type)
+
+ def get_warnings(self, warning_type: None | str = None):
+ if warning_type is None:
+ return self.warnings
+ else:
+ return [warning for warning in self.warnings if warning[0] == warning_type]
+
def add_worker(self, worker: Worker) -> None:
"""Add a worker to the rota
@@ -2491,22 +2521,23 @@ class RotaBuilder(object):
self.workers_name_map: dict[str, Worker] = {}
for worker in track(self.workers, description="Building workers"):
+ print(worker.name)
worker.load_rota(self)
wid = worker.id
if wid in self.workers_id_map:
- raise ValueError(f"Worker with id '{wid}' has been added twice")
+ message = f"Worker with id '{wid}' has been added twice"
+ self.add_warning("Worker/duplicate id", message)
+
self.workers_id_map[wid] = worker
if worker.name in self.workers_name_map:
- raise ValueError(
- f"Worker with name '{worker.name}' has been added twice"
- )
+ message = f"Worker with name '{worker.name}' has been added twice"
+ self.add_warning("Worker/duplicate name", message)
self.workers_name_map[worker.name] = worker
if worker.site not in self.sites:
- raise ValueError(
- f"Worker with name '{worker.name}' ({worker.id}) has no valid shifts (site: {worker.site})"
- )
+ message = f"Worker with name '{worker.name}' ({worker.id}) has no valid shifts (site: {worker.site})"
+ self.add_warning("Worker/no valid shifts", message)
self.workers = sorted(self.workers)
@@ -3128,7 +3159,7 @@ class RotaBuilder(object):
# Loop through all the days possible shifts and see
# if the worker has been assigned
for shift in self.get_shift_names_by_week_day(week, day):
- if model.works[worker.id, week, day, shift].value > 0:
+ if model.works[worker.id, week, day, shift].value > 0.5:
shifts.append(shift)
a = shift[0]
shift_name = shift
@@ -3187,6 +3218,7 @@ class RotaBuilder(object):
shift_diff_dict[shift.name] = diff
worker_td = """
{name} ({grade}) [{fte}] | """.format(
site=worker.site,
nwds=nwds,
+ worker_id=worker.id,
name=worker.name,
fte=worker.fte,
fte_adj=worker.fte_adj,
@@ -3465,3 +3498,9 @@ class InvalidShift(Exception):
"""Raised when there are no active sites"""
pass
+
+
+class WarningTermination(Exception):
+ """Raised when a warning in the termination group is raised"""
+
+ pass
diff --git a/rota/workers.py b/rota/workers.py
index 3bda9ff..bd9b431 100644
--- a/rota/workers.py
+++ b/rota/workers.py
@@ -68,7 +68,7 @@ class Worker(BaseModel):
# rules is easier.
grade: int
# We can either have a user generated ID
- id: Optional[int| uuid.UUID] = None
+ id: Optional[int| uuid.UUID| str] = None
fte: int = 100
nwds: list[NonWorkingDays] = []
start_date: datetime.date | None = None
@@ -81,7 +81,7 @@ class Worker(BaseModel):
previous_shifts: dict = {}
shift_balance_extra: dict = {}
bank_holiday_extra: int = 0
- pair: str | None = None
+ pair: int | str | None = None
class Config:
extra = Extra.allow
@@ -161,6 +161,9 @@ class Worker(BaseModel):
(self.id, week, day)
] = f"END DATE: {self.calculated_end_date}"
+ if not self.not_available_to_work:
+ Rota.add_warning("Worker/No unavailability", f"{self.name} [{self.id}] has no unavailabilities (leave)")
+
for item in self.oop:
start_oop = item.start_date
end_oop = item.end_date
@@ -210,8 +213,8 @@ class Worker(BaseModel):
days_to_end = (self.calculated_end_date - Rota.start_date).days
# loop throught dates converting to week / day combination
- for item in self.pref_not_to_work:
- days_from_start = (item.date - Rota.start_date).days
+ for preference in self.pref_not_to_work:
+ days_from_start = (preference.date - Rota.start_date).days
# Ignore dates past the end of the rota (or end date)
if days_from_start < days_to_end:
week = days_from_start // 7 + 1
@@ -222,32 +225,32 @@ class Worker(BaseModel):
Rota.pref_not_to_work[(self.id, week, day)] = 1 / (
len(self.pref_not_to_work) + 1
)
- Rota.pref_not_to_work_reason[(self.id, week, day)] = item.reason
+ Rota.pref_not_to_work_reason[(self.id, week, day)] = preference.reason
# print(not_available_to_work)
# loop throught dates converting to week / day combination
unavailable_set = set()
- for item in self.not_available_to_work:
- days_from_start = (item.date - Rota.start_date).days
+ for unavalability in self.not_available_to_work:
+ days_from_start = (unavalability.date - Rota.start_date).days
# Ignore dates past the end of the rota (or end date)
if days_from_start < days_to_end:
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
Rota.unavailable_to_work.add((self.id, week, day))
- Rota.unavailable_to_work_reason[(self.id, week, day)] = item.reason
+ Rota.unavailable_to_work_reason[(self.id, week, day)] = unavalability.reason
unavailable_set.add((week, day))
- for item in self.work_requests:
- days_from_start = (item.date - Rota.start_date).days
+ for request in self.work_requests:
+ days_from_start = (request.date - Rota.start_date).days
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
- if item.shift == "*":
+ if request.shift == "*":
for shift in Rota.get_shifts_for_worker_site(self.site):
Rota.work_requests.add((self.id, week, day, shift.name))
else:
- Rota.work_requests.add((self.id, week, day, item.shift))
+ Rota.work_requests.add((self.id, week, day, request.shift))
# Calculate the proportion of the rota that is being worked
self.proportion_rota_to_work = days_to_work / Rota.rota_days_length
@@ -263,15 +266,15 @@ class Worker(BaseModel):
# TODO: this has already been validated, consider moving
self.non_working_day_list = []
- for item in self.nwds:
+ for non_working_day in self.nwds:
start_date = Rota.start_date
end_date = Rota.rota_end_date
- if item.start_date is not None:
- start_date = item.start_date
- if item.end_date is not None:
- end_date = item.end_date
+ if non_working_day.start_date is not None:
+ start_date = non_working_day.start_date
+ if non_working_day.end_date is not None:
+ end_date = non_working_day.end_date
- self.non_working_day_list.append((item.day, start_date, end_date))
+ self.non_working_day_list.append((non_working_day.day, start_date, end_date))
if days_to_work < 1:
self.fte_adj = 0
diff --git a/test/rota_constraints/test_avoid_grades_by_week.py b/test/rota_constraints/test_avoid_grades_by_week.py
index 7e65e3d..0cc9973 100644
--- a/test/rota_constraints/test_avoid_grades_by_week.py
+++ b/test/rota_constraints/test_avoid_grades_by_week.py
@@ -145,7 +145,7 @@ class TestWorkerRequests:
Rota.add_grade_constraint_by_week([3], [1,2,3], ["a"])
Rota.build_and_solve(options={"ratio": 0.000})
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
def test_avoid_grade_constraint_multiple_shifts(self):
Rota = generate_basic_rota()
@@ -192,4 +192,4 @@ class TestWorkerRequests:
Rota.add_grade_constraint_by_week([1], [7,8,9], ["a"])
Rota.build_and_solve(options={"ratio": 0.000})
- assert Rota.results.solver.status == "warning"
\ No newline at end of file
+ assert Rota.results.solver.status in ("warning", "error")
\ No newline at end of file
diff --git a/test/rota_constraints/test_avoid_workers_by_week.py b/test/rota_constraints/test_avoid_workers_by_week.py
index b857b2e..2f98dec 100644
--- a/test/rota_constraints/test_avoid_workers_by_week.py
+++ b/test/rota_constraints/test_avoid_workers_by_week.py
@@ -146,7 +146,7 @@ class TestWorkerRequests:
Rota.add_worker_name_constraint_by_week(["worker3"], [1,2,3], ["a"])
Rota.build_and_solve(options={"ratio": 0.000})
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
def test_avoid_worker_constraint_multiple_shifts(self):
Rota = generate_basic_rota()
@@ -193,4 +193,4 @@ class TestWorkerRequests:
Rota.add_worker_name_constraint_by_week(["worker1"], [7,8,9], ["a"])
Rota.build_and_solve(options={"ratio": 0.000})
- assert Rota.results.solver.status == "warning"
\ No newline at end of file
+ assert Rota.results.solver.status in ("warning", "error")
\ No newline at end of file
diff --git a/test/test_general_balancing.py b/test/test_general_balancing.py
index 47b515b..b9c53d9 100644
--- a/test/test_general_balancing.py
+++ b/test/test_general_balancing.py
@@ -75,7 +75,7 @@ class TestBalancing:
name="a",
length=12.5,
days=days,
- balance_weighting=5,
+ balance_weighting=10,
workers_required=1,
force_as_block=False,
constraint=[{"name": "pre","options": "2"}, {"name": "post","options": "2"}],
@@ -98,7 +98,7 @@ class TestBalancing:
),
)
- Rota.build_and_solve(options={"ratio": 0.001})
+ Rota.build_and_solve(options={"ratio": 0.0001})
Rota.export_rota_to_html("basic_balancing_weighted_shifts")
shift_summary = Rota.get_shift_summary_dict()
@@ -198,7 +198,7 @@ class TestBalancing:
worker = Rota.get_worker_by_name(worker_name)
# assert worker_shifts["a"] in (46,47,48)
- assert worker_shifts["c"] in (7, 8)
+ assert worker_shifts["c"] in (approx(7), approx(8))
#assert worker_shifts["b"] in (53, 54)
def test_weighted_shift_balancing4(self):
diff --git a/test/test_nights2.py b/test/test_nights2.py
index a30c020..94ff614 100644
--- a/test/test_nights2.py
+++ b/test/test_nights2.py
@@ -146,7 +146,7 @@ class TestNightShifts:
Rota.build_and_solve(options={"ratio": 0.000})
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
def test_nights2(self):
Rota = generate_basic_rota()
@@ -315,7 +315,7 @@ class TestNightShifts:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
def test_nights_max_frequency3(self):
Rota = generate_basic_rota()
@@ -352,7 +352,7 @@ class TestNightShifts:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("night")
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
def test_nights_max_frequency4(self):
Rota = generate_basic_rota()
diff --git a/test/test_nwds.py b/test/test_nwds.py
index fe343ba..e27b1e8 100644
--- a/test/test_nwds.py
+++ b/test/test_nwds.py
@@ -90,7 +90,7 @@ class TestDemoRota:
self.Rota.export_rota_to_html("nwd")
- assert self.Rota.results.solver.status == "warning"
+ assert self.Rota.results.solver.status in ("warning", "error")
def test_nwd_partial_rota(self):
# Set up rota
@@ -188,7 +188,7 @@ class TestDemoRota:
self.Rota.build_and_solve(options={"ratio": 0.00})
- assert self.Rota.results.solver.status == "warning"
+ assert self.Rota.results.solver.status in ("warning", "error")
def test_nwd_force_as_block(self):
# Set up rota
@@ -273,21 +273,21 @@ class TestDemoRota:
self.Rota.add_shifts(
SingleShift(
sites=("group1",), name="d", length= 12.5, days=days[:5],
- balance_offset=10,
+ #balance_offset=50,
workers_required=2,
force_as_block_unless_nwd=True,
assign_as_block=True,
),
SingleShift(
sites=("group1",), name="w", length= 12.5, days=days[5:],
- balance_offset=10,
+ #balance_offset=50,
workers_required=4,
force_as_block_unless_nwd=True,
assign_as_block=True,
),
)
- self.Rota.build_and_solve(options={"ratio": 0.00})
+ self.Rota.build_and_solve()
self.Rota.export_rota_to_html("nwd_block_force_split")
assert self.Rota.results.solver.status == "ok"
diff --git a/test/test_remote.py b/test/test_remote.py
index f8f3553..9fe240a 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -40,7 +40,7 @@ class TestRemoteRotas:
Rota.build_and_solve(options={"ratio": 0.000})
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
def test_remote_pass_week(self):
Rota = setup_basic_rota()
diff --git a/test/test_rota.py b/test/test_rota.py
index 23976dc..8ba921c 100644
--- a/test/test_rota.py
+++ b/test/test_rota.py
@@ -2,7 +2,8 @@ from copy import deepcopy
from re import S
from black import main
import pytest
-from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
+from pytest import approx
+from rota.shifts import NoWorkers, RotaBuilder, SingleShift, WarningTermination, days
import datetime
@@ -285,14 +286,14 @@ class TestDemoRotaNights:
worker = self.Rota.get_worker_by_name(worker_name)
if worker.fte == 40:
- assert worker_shifts["night_weekday"] in (0, 4)
- assert worker_shifts["night_weekend"] in (0, 3)
+ assert worker_shifts["night_weekday"] in (approx(0), approx(4))
+ assert worker_shifts["night_weekend"] in (approx(0), approx(3))
elif worker.fte == 60:
- assert worker_shifts["night_weekday"] in (0, 4, 8)
- assert worker_shifts["night_weekend"] in (0, 3, 6)
+ assert worker_shifts["night_weekday"] in (approx(0), approx(4), approx(8))
+ assert worker_shifts["night_weekend"] in (approx(0), approx(3), approx(6))
else:
- assert worker_shifts["night_weekday"] in (4, 8)
- assert worker_shifts["night_weekend"] in (3, 6)
+ assert worker_shifts["night_weekday"] in (approx(4), approx(8))
+ assert worker_shifts["night_weekend"] in (approx(3), approx(6))
class TestDemoRotaClear:
@@ -326,7 +327,7 @@ class TestDemoRotaClear:
name="a",
length=12.5,
days=days[:5],
- balance_offset=40,
+ #balance_offset=40,
workers_required=2,
constraint=[{"name": "pre", "options": 1}],
),
@@ -335,7 +336,7 @@ class TestDemoRotaClear:
name="b",
length=12.5,
days=days[:5],
- balance_offset=40,
+ #balance_offset=40,
workers_required=2,
),
SingleShift(
@@ -343,7 +344,7 @@ class TestDemoRotaClear:
name="c",
length=12.5,
days=days[3],
- balance_offset=40,
+ #balance_offset=40,
workers_required=1,
constraint=[{"name": "pre", "options": 1}, {"name": "post", "options": 1}],
),
@@ -481,7 +482,7 @@ class TestDemoRotaShiftConstraints:
def test_max_shifts_fail(self):
self.Rota.constraint_options["max_shifts_per_month"] = 13
self.Rota.build_and_solve()
- assert self.Rota.results.solver.status == "warning"
+ assert self.Rota.results.solver.status in ("warning", "error")
assert self.Rota.results.solver.termination_condition == "infeasible"
def test_max_shifts_extra_worker(self):
@@ -495,7 +496,7 @@ class TestDemoRotaShiftConstraints:
self.Rota.constraint_options["max_shifts_per_week"] = 3
self.Rota.build_and_solve()
- assert self.Rota.results.solver.status == "warning"
+ assert self.Rota.results.solver.status in ("warning", "error")
assert self.Rota.results.solver.termination_condition == "infeasible"
def test_max_shifts_per_week_pass(self):
@@ -550,7 +551,17 @@ class TestDemoRotaBalanceShiftSites:
self.Rota.constraint_options["balance_nights_across_sites"] = False
self.Rota.constraint_options["balance_nights"] = False
self.Rota.constraint_options["constrain_time_off_after_nights"] = False
+
+ # Worker 5 and 6 don't have any valid shifts so this should fail
+ #with pytest.raises(WarningTermination):
+ # self.Rota.build_and_solve(options={"ratio": 0.0})
+
+ # Unless we remove the warning
+ self.Rota.terminate_on_warning.remove("Worker/no valid shifts")
self.Rota.build_and_solve(options={"ratio": 0.0})
+
+ assert len(self.Rota.get_warnings("Worker/no valid shifts")) == 2
+
self.Rota.export_rota_to_html("test5")
print(self.Rota.get_workers_by_group())
@@ -586,6 +597,8 @@ class TestDemoRotaBalanceShiftSites:
self.Rota.constraint_options["balance_nights_across_sites"] = False
self.Rota.constraint_options["balance_nights"] = False
self.Rota.constraint_options["constrain_time_off_after_nights"] = False
+
+ self.Rota.terminate_on_warning.remove("Worker/no valid shifts")
self.Rota.build_and_solve(options={"ratio": 0.1})
group_workers = self.Rota.get_workers_by_group()
@@ -1151,7 +1164,7 @@ class TestNightUnavailable:
self.Rota.build_and_solve(options={"ratio": 0.00})
self.Rota.export_rota_to_html("testnight_unavail")
- assert self.Rota.results.solver.status == "warning"
+ assert self.Rota.results.solver.status in ("warning", "error")
assert self.Rota.results.solver.termination_condition == "infeasible"
def test_assign_non_night_prior_to_unavailablity(self):
@@ -1252,7 +1265,7 @@ class TestNightUnavailable:
#
# self.Rota.build_and_solve(options={"ratio": 0.00})
#
-# assert self.Rota.results.solver.status == "warning"
+# assert self.Rota.results.solver.status in ("warning", "error")
if __name__ == "__main__":
t = TestLimitConstraints()
diff --git a/test/test_shifts.py b/test/test_shifts.py
index 46fe6d2..3ff1ff8 100644
--- a/test/test_shifts.py
+++ b/test/test_shifts.py
@@ -142,7 +142,7 @@ class TestShiftConstraints:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("max_shifts_per_week_block")
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
def test_max_shifts_per_week_block_shift_number(self):
Rota = generate_basic_rota()
@@ -172,7 +172,7 @@ class TestShiftConstraints:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("max_shifts_per_week_block")
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
def test_pre_shift_constraint(self):
Rota = generate_basic_rota()
@@ -226,7 +226,7 @@ class TestShiftConstraints:
Rota.build_and_solve(options={"ratio": 0.000})
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
def test_pre_shift_constraint2(self):
Rota = generate_basic_rota()
@@ -269,7 +269,7 @@ class TestShiftConstraints:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("pre")
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
def test_post_shift_constraint(self):
Rota = generate_basic_rota()
@@ -311,4 +311,4 @@ class TestShiftConstraints:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("post")
- assert Rota.results.solver.status == "warning"
\ No newline at end of file
+ assert Rota.results.solver.status in ("warning", "error")
\ No newline at end of file
diff --git a/test/test_weekend_padding.py b/test/test_weekend_padding.py
index 9235c8b..3b5d816 100644
--- a/test/test_weekend_padding.py
+++ b/test/test_weekend_padding.py
@@ -210,7 +210,7 @@ class TestDemoRota:
self.Rota.build_and_solve(options={"ratio": 0.00})
- assert self.Rota.results.solver.status == "warning"
+ assert self.Rota.results.solver.status in ("warning", "error")
def test_post_mon(self):
# Set up rota
@@ -343,4 +343,4 @@ class TestDemoRota:
self.Rota.export_rota_to_html("weekend_padding")
- assert self.Rota.results.solver.status == "warning"
+ assert self.Rota.results.solver.status in ("warning", "error")
diff --git a/test/test_worker_requests.py b/test/test_worker_requests.py
index 3e54e73..d5ef441 100644
--- a/test/test_worker_requests.py
+++ b/test/test_worker_requests.py
@@ -1,5 +1,6 @@
import datetime
import pytest
+from pytest import approx
from rota.shifts import InvalidShift, NoWorkers, RotaBuilder, SingleShift, days
from rota.workers import Worker
@@ -179,7 +180,7 @@ class TestWorkerRequests:
worker = Rota.get_worker_by_name(worker_name)
- assert worker_shifts["a"] in (46, 47)
+ assert worker_shifts["a"] in (approx(46), approx(47))
# shift_string = Rota.get_worker_shift_list_string(worker)
@@ -216,7 +217,7 @@ class TestWorkerRequests:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("unavailable_to_work2")
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
assert Rota.results.solver.termination_condition == "infeasible"
def test_unavailable_to_work3(self):
@@ -304,7 +305,7 @@ class TestWorkerRequests:
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("unavailable_to_work4")
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
assert Rota.results.solver.termination_condition == "infeasible"
def test_work_requests(self):
@@ -523,7 +524,7 @@ class TestWorkerRequests:
worker = Rota.get_worker_by_name(worker_name)
assert worker_shifts["a"] in (26, 27)
- assert worker_shifts["b"] == 10
+ assert worker_shifts["b"] == approx(10)
shift_string = Rota.get_worker_shift_list_string(worker)
diff --git a/test/test_workers.py b/test/test_workers.py
index 07989c2..ca4be0d 100644
--- a/test/test_workers.py
+++ b/test/test_workers.py
@@ -1,5 +1,5 @@
import pytest
-from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
+from rota.shifts import NoWorkers, RotaBuilder, SingleShift, WarningTermination, days
import datetime
from rota.workers import Worker
@@ -64,7 +64,7 @@ class TestWorkers:
),
)
- with pytest.raises(ValueError):
+ with pytest.raises(WarningTermination):
Rota.build_and_solve(solve=False)
def test_worker_ftes(self):
@@ -143,6 +143,10 @@ class TestWorkers:
),
)
+ with pytest.raises(WarningTermination):
+ Rota.build_and_solve()
+
+ Rota.terminate_on_warning.remove("Worker/no valid shifts")
Rota.build_and_solve()
# All workers should have an adjusted fte of 50 %
@@ -298,6 +302,7 @@ class TestWorkers:
),
)
+ Rota.terminate_on_warning.remove("Worker/no valid shifts")
Rota.build_and_solve()
# All workers should have an adjusted fte of 50 %
@@ -353,7 +358,7 @@ class TestWorkers:
Rota.build_and_solve()
# With two paired workers we shouldn't be able to solve
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
worker3 = Worker(name="worker3", site="group1", grade=1, fte=100)
Rota.add_worker(worker3)
@@ -374,7 +379,7 @@ class TestWorkers:
Rota.build_and_solve()
- assert Rota.results.solver.status == "warning"
+ assert Rota.results.solver.status in ("warning", "error")
worker3.pair = 0