Skip to content

Commit

Permalink
Improve demo tests
Browse files Browse the repository at this point in the history
  • Loading branch information
matejak committed Nov 26, 2023
1 parent cbbfdec commit e997777
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 41 deletions.
46 changes: 46 additions & 0 deletions estimage/persistence/entrydef/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import contextlib
import typing

from ... import data


class Saver:
@classmethod
def bulk_save_metadata(cls, targets: typing.Iterable[data.BaseTarget]):
saver = cls()
for t in targets:
t.pass_data_to_saver(saver)

@classmethod
def forget_all(cls):
raise NotImplementedError()

@classmethod
@contextlib.contextmanager
def get_saver(cls):
yield cls()


class Loader:
@classmethod
def get_all_target_names(cls):
raise NotImplementedError()

@classmethod
def get_loaded_targets_by_id(cls, target_class=typing.Type[data.BaseTarget]):
raise NotImplementedError()

@classmethod
def denormalize(cls, t: data.BaseTarget):
for child in t.children:
child.parent = t
cls.denormalize(child)

@classmethod
def load_all_targets(cls, target_class=typing.Type[data.BaseTarget]):
raise NotImplementedError()

@classmethod
@contextlib.contextmanager
def get_loader(cls):
yield cls()
6 changes: 3 additions & 3 deletions estimage/persistence/entrydef/ini.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import typing
import datetime

from ... import data, inidata, PluginResolver, persistence
from ... import data, inidata, persistence


class IniTargetSaverBase(inidata.IniSaverBase):
class IniTargetSaverBase(inidata.IniSaverBase, persistence.entrydef.Saver):
def _store_our(self, t, attribute, value=None):
if value is None and hasattr(t, attribute):
value = getattr(t, attribute)
return self._write_items_attribute(t.name, attribute, value)


class IniTargetLoaderBase(inidata.IniLoaderBase):
class IniTargetLoaderBase(inidata.IniLoaderBase, persistence.entrydef.Loader):
def _get_our(self, t, attribute, fallback=None):
if fallback is None and hasattr(t, attribute):
fallback = getattr(t, attribute)
Expand Down
115 changes: 115 additions & 0 deletions estimage/persistence/entrydef/memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import collections
import typing

from ... import data, persistence


GLOBAL_STORAGE = collections.defaultdict(dict)


@persistence.saver_of(data.BaseTarget, "memory")
class MemoryTargetSaver(persistence.entrydef.Saver):
def _save(self, t, attribute):
GLOBAL_STORAGE[t.name][attribute] = getattr(t, attribute)

def save_title_and_desc(self, t):
self._save(t, "title")
self._save(t, "description")

def save_costs(self, t):
self._save(t, "point_cost")

def save_family_records(self, t):
self._save(t, "children")
self._save(t, "parent")

def save_assignee_and_collab(self, t):
self._save(t, "assignee")
self._save(t, "collaborators")

def save_priority_and_state(self, t):
self._save(t, "state")
self._save(t, "priority")

def save_tier(self, t):
self._save(t, "tier")

def save_tags(self, t):
self._save(t, "tags")

def save_work_span(self, t):
self._save(t, "work_span")

def save_uri_and_plugin(self, t):
self._save(t, "loading_plugin")
self._save(t, "uri")

@classmethod
def forget_all(cls):
GLOBAL_STORAGE.clear()


@persistence.loader_of(data.BaseTarget, "memory")
class MemoryTargetLoader(persistence.entrydef.Loader):
def _load(self, t, attribute):
setattr(t, attribute, GLOBAL_STORAGE[t.name][attribute])

def load_title_and_desc(self, t):
self._load(t, "title")
self._load(t, "description")

def load_costs(self, t):
self._load(t, "point_cost")

def load_family_records(self, t):
self._load(t, "children")
self._load(t, "parent")

def load_assignee_and_collab(self, t):
self._load(t, "assignee")
self._load(t, "collaborators")

def load_priority_and_state(self, t):
self._load(t, "priority")
self._load(t, "state")

def load_tier(self, t):
self._load(t, "tier")

def load_tags(self, t):
self._load(t, "tags")

def load_work_span(self, t):
self._load(t, "work_span")

def load_uri_and_plugin(self, t):
self._load(t, "loading_plugin")
self._load(t, "uri")

@classmethod
def get_all_target_names(cls):
return set(GLOBAL_STORAGE.keys())

@classmethod
def get_loaded_targets_by_id(cls, target_class=data.BaseTarget):
ret = dict()
loader = cls()
for name in GLOBAL_STORAGE:
target = target_class(name)
target.load_data_by_loader(loader)
ret[name] = target
return ret

@classmethod
def load_all_targets(cls, target_class=data.BaseTarget):
ret = []
loader = cls()
for name in GLOBAL_STORAGE:
target = target_class(name)
target.load_data_by_loader(loader)
ret.append(target)
return ret


class MemoryTargetIO(MemoryTargetSaver, MemoryTargetLoader):
pass
50 changes: 27 additions & 23 deletions estimage/plugins/demo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
import datetime
import json

import flask
import numpy as np

from ... import simpledata, data
from ... import simpledata, data, persistence


NULL_CHOICE = ("noop", "Do Nothing")
Expand All @@ -21,16 +20,21 @@ def load_data():
return ret



# TODO: Strategies
# - random / uniform effort distribution
# - flexible / lossy force usage
# - auto-assignment of tasks (according to selection, all at once, sequential, n at a time)
# - jump a day, jump to the end
class Demo:
def __init__(self, targets_by_id, loader):
self.targets_by_id = targets_by_id
def __init__(self, loader, start_date):
self.targets_by_id = loader.get_loaded_targets_by_id()
self.loader = loader
self.start_date = start_date

plugin_data = load_data()
self.day_index = plugin_data.get("day_index", 0)
if self.day_index == 0:
start(targets_by_id.values(), loader)
start(self.targets_by_id.values(), self.loader, start_date)

def get_sensible_choices(self):
targets = self.get_ordered_wip_targets()
Expand All @@ -46,8 +50,10 @@ def get_actual_choices(self):
targets = self.get_ordered_wip_targets()
plugin_data = load_data()
velocity_in_stash = plugin_data.get("velocity_in_stash", dict())
label = f"{t.title} {velocity_in_stash.get(t.name, 0):.2g}/{t.point_cost}"
actual_choices = [(t.name, label) for t in targets]
actual_choices = []
for t in targets:
label = f"{t.title} {velocity_in_stash.get(t.name, 0):.2g}/{t.point_cost}"
actual_choices.append((t.name, label))
if not actual_choices:
actual_choices = [NULL_CHOICE]
return actual_choices
Expand All @@ -57,10 +63,12 @@ def evaluate_progress(self, velocity_in_stash, names, model, plugin_data):
target = self.targets_by_id[name]

if velocity_in_stash[name] > model.remaining_point_estimate_of(name).expected:
flask.flash(f"Finished {name}")
conclude_target(target, self.loader, plugin_data["day_index"])
previously_finished = plugin_data.get("finished", [])
previously_finished.append(name)
plugin_data["finished"] = previously_finished
conclude_target(target, self.loader, self.start_date, plugin_data["day_index"])
else:
begin_target(target, self.loader, plugin_data["day_index"])
begin_target(target, self.loader, self.start_date, plugin_data["day_index"])

def apply_work(self, progress, names, model):
plugin_data = load_data()
Expand All @@ -72,7 +80,7 @@ def apply_work(self, progress, names, model):
velocity_in_stash = plugin_data.get("velocity_in_stash", dict())
apply_velocities(names, progress, velocity_in_stash)
plugin_data["velocity_in_stash"] = velocity_in_stash
evaluate_progress(velocity_in_stash, names, model, plugin_data)
self.evaluate_progress(velocity_in_stash, names, model, plugin_data)
save_data(plugin_data)

def get_not_finished_targets(self):
Expand Down Expand Up @@ -117,13 +125,11 @@ def DDAY_LABEL(self):
def get_date_of_dday(self):
data = load_data()
day_index = data.get("day_index", 0)
start = flask.current_app.config["RETROSPECTIVE_PERIOD"][0]
return start + datetime.timedelta(days=day_index)
return self.start + datetime.timedelta(days=day_index)


def start(targets, loader):
start = flask.current_app.config["RETROSPECTIVE_PERIOD"][0]
date = start - datetime.timedelta(days=20)
def start(targets, loader, start_date):
date = start_date - datetime.timedelta(days=20)
mgr = simpledata.EventManager()
for t in targets:
evt = data.Event(t.name, "state", date)
Expand All @@ -136,9 +142,8 @@ def start(targets, loader):
mgr.save()


def begin_target(target, loader, day_index):
start = flask.current_app.config["RETROSPECTIVE_PERIOD"][0]
date = start + datetime.timedelta(days=day_index)
def begin_target(target, loader, start_date, day_index):
date = start_date + datetime.timedelta(days=day_index)
mgr = simpledata.EventManager()
mgr.load()
if len(mgr.get_chronological_task_events_by_type(target.name)["state"]) < 2:
Expand All @@ -152,9 +157,8 @@ def begin_target(target, loader, day_index):
target.save_metadata(loader)


def conclude_target(target, loader, day_index):
start = flask.current_app.config["RETROSPECTIVE_PERIOD"][0]
date = start + datetime.timedelta(days=day_index)
def conclude_target(target, loader, start_date, day_index):
date = start_date + datetime.timedelta(days=day_index)
mgr = simpledata.EventManager()
mgr.load()
evt = data.Event(target.name, "state", date)
Expand Down
3 changes: 2 additions & 1 deletion estimage/plugins/demo/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def next_day():
cls, loader = web_utils.get_retro_loader()
targets_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("retro", user_id)

doer = demo.Demo(targets_by_id, loader)
start_date = flask.current_app.config["RETROSPECTIVE_PERIOD"][0]
doer = demo.Demo(loader, start_date)

form = forms.DemoForm()
form.issues.choices = doer.get_sensible_choices()
Expand Down
25 changes: 19 additions & 6 deletions estimage/plugins/demo/tests/test_demo.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import datetime

import pytest

from estimage import plugins, data, PluginResolver, persistence
Expand All @@ -9,18 +11,29 @@


@pytest.fixture
def some_targets():
def loader():
loader_and_saver = (
persistence.LOADERS[BaseTarget]["memory"],
persistence.SAVERS[BaseTarget]["memory"])
ret = type("loader", loader_and_saver, dict())
ret.forget_all()
yield ret
ret.forget_all()


@pytest.fixture
def some_targets(loader):
a = BaseTarget("a")
a.state = data.State.todo
b = BaseTarget("b")
b.state = data.State.in_progress
c = BaseTarget("c")
d = BaseTarget("d")
d.state = data.State.done
return [a, b, c, d]
loader.bulk_save_metadata([a, b, c, d])


def test_select_tasks_not_finished(some_targets):
doer = tm.Demo(some_targets, loader)
assert not tm.get_not_finished_targets([])
assert len(tm.get_not_finished_targets(some_targets)) == 2
def test_select_tasks_not_finished(some_targets, loader):
someday = datetime.datetime(2024, 2, 3)
doer = tm.Demo(loader, someday)
assert len(doer.get_not_finished_targets()) == 2
13 changes: 8 additions & 5 deletions estimage/visualize/burndown.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ class MPLPointPlot:
)
DDAY_LABEL = "today"

def __init__(self, a: history.Aggregation):
def __init__(self, a: history.Aggregation, * args, ** kwargs):
self.aggregation = a
self.start = a.start
self.end = a.end
self.width = 1.0
super().__init__(* args, ** kwargs)
self.status_arrays = np.zeros((len(self.STYLES), a.days))
dday_date = self.get_date_of_dday()
self.index_of_dday = history.days_between(self.aggregation.start, dday_date)
self.width = 1.0
self.index_of_dday = history.days_between(self.start, dday_date)

def get_date_of_dday(self):
return datetime.datetime.today()
Expand All @@ -44,7 +47,7 @@ def _show_plan(self, ax):
linewidth=self.width, label="burndown")

def _show_dday(self, ax):
if self.aggregation.start <= self.get_date_of_dday() <= self.aggregation.end:
if self.start <= self.get_date_of_dday() <= self.end:
ax.axvline(self.index_of_dday, label=self.DDAY_LABEL, color="grey", linewidth=self.width * 2)

def _plot_prepared_arrays(self, ax):
Expand Down Expand Up @@ -78,7 +81,7 @@ def get_figure(self):
self._show_dday(ax)
ax.legend(loc="upper right")

utils.x_axis_weeks_and_months(ax, self.aggregation.start, self.aggregation.end)
utils.x_axis_weeks_and_months(ax, self.start, self.end)
ax.set_ylabel("points")

return fig
Expand Down
1 change: 1 addition & 0 deletions estimage/visualize/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class MPLCompletionPlot:
def __init__(self, period_start, cdf):
self.dom = np.arange(len(cdf))
self.cdf = cdf * 100
self.start = period_start
self.ppf = sp.interpolate.interp1d(cdf, self.dom)

def _dom_to_days(self, dom_numbers):
Expand Down
Loading

0 comments on commit e997777

Please sign in to comment.