Beispiel #1
0
        def _execute(self, sp_controller, date_converter, as_key):
            env = sp_controller.env
            end = min(self.sprint.end, now(tz=utc))

            from agilo.scrum.burndown.model import BurndownDataAggregator
            aggregator = BurndownDataAggregator(env, self.remaining_field)
            return aggregator.burndown_data_for_sprint(self.sprint,
                extend_until=end, filter_by_component=self.filter_by_component)
 def test_stores_historic_data_change(self):
     self.teh.create_task(sprint=self.sprint.name, remaining_time=8)
     aggregator = BurndownDataAggregator(self.env)
     changes = aggregator.changes_for_sprint(self.sprint)
     self.assert_length(2, changes)
     self._confirm_commitment()
     changes = aggregator.changes_for_sprint(self.sprint)
     self.assert_length(1, changes)
 def test_stores_historic_data_change(self):
     self.teh.create_task(sprint=self.sprint.name, remaining_time=8)
     aggregator = BurndownDataAggregator(self.env)
     changes = aggregator.changes_for_sprint(self.sprint)
     self.assert_length(2, changes)
     self._confirm_commitment()
     changes = aggregator.changes_for_sprint(self.sprint)
     self.assert_length(1, changes)
Beispiel #4
0
        def _execute(self, sp_controller, date_converter, as_key):
            env = sp_controller.env
            end = min(self.sprint.end, now(tz=utc))

            from agilo.scrum.burndown.model import BurndownDataAggregator
            aggregator = BurndownDataAggregator(env, self.remaining_field)
            return aggregator.burndown_data_for_sprint(
                self.sprint,
                extend_until=end,
                filter_by_component=self.filter_by_component)
 def test_second_commit_updates_the_first(self):
     self.assert_none(self._commitment(self.sprint, self.team))
     self._confirm_commitment()
     self.assert_equals(13, self._commitment(self.sprint, self.team))
     
     self.teh.create_task(sprint=self.sprint.name, remaining_time=8)
     self._invalidate_backlog_cache()
     self._confirm_commitment()
     self.assert_equals(21, self._commitment(self.sprint, self.team))
     
     aggregator = BurndownDataAggregator(self.env)
     changes = aggregator.changes_for_sprint(self.sprint)
     self.assert_length(1, changes)
    def test_second_commit_updates_the_first(self):
        self.assert_none(self._commitment(self.sprint, self.team))
        self._confirm_commitment()
        self.assert_equals(13, self._commitment(self.sprint, self.team))

        self.teh.create_task(sprint=self.sprint.name, remaining_time=8)
        self._invalidate_backlog_cache()
        self._confirm_commitment()
        self.assert_equals(21, self._commitment(self.sprint, self.team))

        aggregator = BurndownDataAggregator(self.env)
        changes = aggregator.changes_for_sprint(self.sprint)
        self.assert_length(1, changes)
 def setUp(self):
     self.super()
     self.sprint = self.teh.create_sprint(self.sprint_name())
     self.aggregator = BurndownDataAggregator(self.env)
     self.confirmer = BurndownDataConfirmCommitment(self.env)
class ConfirmCommitmentTest(AgiloTestCase):
    
    def setUp(self):
        self.super()
        self.sprint = self.teh.create_sprint(self.sprint_name())
        self.aggregator = BurndownDataAggregator(self.env)
        self.confirmer = BurndownDataConfirmCommitment(self.env)
    
    def changes(self, sprint=None):
        return self.aggregator.changes_for_sprint(sprint and ValueObject(name=sprint) or self.sprint)
    
    def create_change(self, delta=23, marker_key=None, marker_value=None, **kwargs):
        change = BurndownDataChange(self.env)
        change.type = BurndownDataConstants.REMAINING_TIME
        change.scope = self.sprint.name
        change.when = now()
        change.set_delta(delta)
        for key, value in kwargs.items():
            setattr(change, key, value)
        if marker_key is not None:
            change.update_marker(marker_key, marker_value)
        return change
    
    def insert_changes(self):
        self.create_change().save()
        self.create_change().save()
        self.create_change().save()
    
    
    
    def test_can_remove_all_entries_that_are_already_set_for_a_sprint(self):
        self.insert_changes()
        self.confirmer.remove_old_changes_for_sprint(self.sprint)
        self.assert_length(0, self.changes())
    
    # TODO: might need to care about the key so story burndown values are not / too removed?
    def test_will_not_touch_entries_for_other_sprints_when_removing(self):
        self.insert_changes()
        self.create_change(scope='fnord').save()
        self.confirmer.remove_old_changes_for_sprint(self.sprint)
        self.assert_length(0, self.changes())
        self.assert_length(1, self.changes(sprint='fnord'))
    
    def test_sum_remaining_time_in_sprint(self):
        self.create_change(delta=3).save()
        self.assert_equals(3, self.confirmer.sum_remaining_time_for_sprint(self.sprint))
        self.create_change(delta=5).save()
        self.assert_equals(8, self.confirmer.sum_remaining_time_for_sprint(self.sprint))
        self.create_change(delta=10).save()
        self.assert_equals(18, self.confirmer.sum_remaining_time_for_sprint(self.sprint))
        self.create_change(delta=-12).save()
        self.assert_equals(6, self.confirmer.sum_remaining_time_for_sprint(self.sprint))
    
    def test_adding_initial_remaining_time(self):
        self.confirmer.add_initial_change_for_sprint_with_remaining_time(self.sprint, 10)
        self.assert_length(1, self.changes())
        self.assert_equals(10, self.changes()[0].delta())
    
    def test_created_initial_burndown_element_has_correct_time(self):
        self.confirmer.add_initial_change_for_sprint_with_remaining_time(self.sprint, 23)
        self.assert_almost_equals(now(), self.changes()[0].when, max_delta=timedelta(seconds=2))
    
    def test_enters_sum_of_removed_burndown_as_first_entry_after_removing_all_others(self):
        self.create_change(delta=10).save()
        self.create_change(delta=7).save()
        self.create_change(delta=6).save()
        self.confirmer.confirm_commitment_for_sprint(self.sprint)
        self.assert_length(1, self.changes())
        self.assert_equals(23, self.changes()[0].delta())
    
    # TODO: Consider to only remove ticket-change-entries until the target time!
    def test_can_set_time_of_commitment(self):
        self.create_change(delta=30).save()
        some_time_ago = now() - timedelta(days=3)
        self.confirmer.confirm_commitment_for_sprint(self.sprint, when=some_time_ago)
        self.assert_length(1, self.changes())
        self.assert_almost_equals(some_time_ago, self.changes()[0].when, max_delta=timedelta(seconds=2))
    
    def test_can_add_metadata_with_commitment_for_each_component_if_filtered_burndown_is_activated(self):
        self.teh.enable_burndown_filter()
        self.create_change(delta=3, when=self.sprint.start, marker_key=Key.COMPONENT, marker_value='foo').save()
        self.create_change(delta=5, when=self.sprint.start, marker_key=Key.COMPONENT, marker_value='bar').save()
        self.create_change(delta=7, when=self.sprint.start).save()
        self.confirmer.confirm_commitment_for_sprint(self.sprint, when=self.sprint.start)
        self.assert_length(1, self.changes())
        change = self.changes()[0]
        self.assert_true(change.has_marker(BurndownDataConstants.DELTAS_BY_COMPONENT))
        deltas = change.marker_value(BurndownDataConstants.DELTAS_BY_COMPONENT)
        self.assert_equals(dict(foo=3, bar=5), deltas)
    
    def test_stores_correct_component_metadata_if_confirm_commitment_is_pressed_twice(self):
        self.teh.enable_burndown_filter()
        self.create_change(delta=3, when=self.sprint.start, marker_key=Key.COMPONENT, marker_value='foo').save()
        self.confirmer.confirm_commitment_for_sprint(self.sprint, when=self.sprint.start)
        self.confirmer.confirm_commitment_for_sprint(self.sprint, when=self.sprint.start)
        
        self.assert_length(1, self.changes())
        change = self.changes()[0]
        deltas = change.marker_value(BurndownDataConstants.DELTAS_BY_COMPONENT)
        self.assert_equals(dict(foo=3), deltas)
Beispiel #9
0
 def setUp(self):
     self.super()
     self.sprint = self.teh.create_sprint(self.sprint_name())
     self.aggregator = BurndownDataAggregator(self.env)
Beispiel #10
0
class TimeAggregationTest(AgiloTestCase):
    def setUp(self):
        self.super()
        self.sprint = self.teh.create_sprint(self.sprint_name())
        self.aggregator = BurndownDataAggregator(self.env)

    def change(self,
               time_delta,
               how_much,
               type=None,
               scope=None,
               marker_key=None,
               marker_value=True):
        instance = BurndownDataChange(self.env).update_values(
            type=type or BurndownDataConstants.REMAINING_TIME,
            scope=getattr(scope, 'name', self.sprint.name),
            when=now() + time_delta,
            delta=how_much,
        )
        if marker_key is not None:
            instance.update_marker(marker_key, marker_value)
        return instance

    def fixture(self):
        return [
            self.change(timedelta(hours=-3), -5),
            self.change(timedelta(hours=-1), -3),
            self.change(timedelta(hours=-5), 10),
        ]

    def aggregate(self,
                  changes,
                  interval=timedelta(hours=2),
                  aggregate_until=None,
                  discard_after=None,
                  extend_until=None,
                  filter_by_component=None):
        return self.aggregator.aggregate_changes_with_interval(
            changes,
            duration=interval,
            aggregate_until=aggregate_until,
            discard_after=discard_after,
            extend_until=extend_until,
            filter_by_component=filter_by_component)

    def test_can_aggregate_zero_changes(self):
        self.assert_equals([], self.aggregate([]))

    def test_can_aggregate_one_change(self):
        first = self.change(timedelta(hours=0), 10)
        self.assert_equals([burndown_entry(first.when, first.delta())],
                           self.aggregate([first]))

    def test_just_transforms_to_changes_if_no_aggregation_is_neccessary(self):
        first = self.change(timedelta(hours=0), 0)
        second = self.change(timedelta(hours=10), 6)
        aggregated = self.aggregate([first, second],
                                    interval=timedelta(hours=1))
        self.assert_length(2, aggregated)
        self.assert_equals(burndown_entry(first.when, first.delta()),
                           aggregated[0])
        self.assert_equals(
            burndown_entry(second.when,
                           first.delta() + second.delta()), aggregated[1])

    def test_can_add_up_durations(self):
        first = self.change(timedelta(hours=0), 10)
        second = self.change(timedelta(hours=10), 10)
        aggregated = self.aggregate([first, second],
                                    interval=timedelta(hours=1))
        self.assert_length(2, aggregated)
        self.assert_equals(burndown_entry(first.when, 10), aggregated[0])
        self.assert_equals(burndown_entry(second.when, 20), aggregated[1])

    def test_does_not_discard_start_or_end_value_even_if_interval_is_bigger_than_their_distance(
            self):
        first = self.change(timedelta(hours=0), 10)
        second = self.change(timedelta(hours=1), 10)
        aggregated = self.aggregate([first, second])
        self.assert_length(2, aggregated)
        self.assert_equals(burndown_entry(first.when, 10), aggregated[0])
        self.assert_equals(burndown_entry(second.when, 20), aggregated[1])

    def test_can_aggregate_multiple_changes_in_interval(self):
        first = self.change(timedelta(hours=0), 10)
        second = self.change(timedelta(hours=1), 10)
        third = self.change(timedelta(hours=2), 10)
        aggregated = self.aggregate([first, second, third])
        self.assert_length(2, aggregated)
        self.assert_equals(burndown_entry(first.when, 10), aggregated[0])
        self.assert_equals(burndown_entry(third.when, 30), aggregated[1])

    def test_can_extend_series_till_specified_time(self):
        first = self.change(timedelta(hours=-10), 10)
        end = now()
        aggregated = self.aggregate([first], extend_until=end)
        self.assert_equals([
            burndown_entry(first.when, first.delta()),
            burndown_entry(end, first.delta())
        ], aggregated)

    def test_first_entry_is_always_at_start_of_sprint(self):
        change = self.change(timedelta(days=-100), 10)
        actual = self.aggregate([change], aggregate_until=self.sprint.start)
        self.assert_time_equals(self.sprint.start, actual[0].when)

    def test_will_aggregate_changes_before_sprint_start_to_sprint_start(self):
        first = self.change(timedelta(days=-100), 15)
        second = self.change(timedelta(days=-100), 23)
        actual = self.aggregate([first, second],
                                aggregate_until=self.sprint.start)
        self.assert_equals(15 + 23, actual[0].remaining_time)

    def test_will_disregard_all_burndown_changes_after_sprint_end(self):
        change = self.change(timedelta(days=100), 200)
        self.assert_length(
            0, self.aggregate([change], discard_after=self.sprint.end))

    def test_changes_after_sprint_end_have_no_influence_on_burndown_chart_in_sprint(
            self):
        changes = [
            self.change(timedelta(), 10),
            self.change(timedelta(days=1), 10),
            self.change(timedelta(days=2), 10),
            self.change(timedelta(days=100), 1000)
        ]
        aggregated = self.aggregate(changes,
                                    discard_after=self.sprint.end,
                                    extend_until=self.sprint.end)
        self.assert_time_equals(changes[0].when, aggregated[0].when)
        self.assert_time_equals(self.sprint.end, aggregated[-1].when)
        self.assert_equals(30, aggregated[-1].remaining_time)

    def test_one_change_and_time_extension(self):
        self.change(timedelta(days=-20), 12).save()
        until = now() + timedelta(days=10)
        aggregated = self.aggregator.burndown_data_for_sprint(
            self.sprint, extend_until=until)
        self.assert_length(2, aggregated)

    def test_does_not_count_the_last_entry_twice(self):
        changes = [
            self.change(timedelta(), 0),
            self.change(timedelta(), 0),
            self.change(timedelta(), 0),
            self.change(timedelta(), 10),
        ]

        aggregated = self.aggregate(changes)
        self.assert_equals(0, aggregated[-2].remaining_time)
        self.assert_equals(10, aggregated[-1].remaining_time)

    # --------------------------------------------------------------------------------
    # Not aggregating over markers

    def test_can_have_normal_intersection_of_entries(self):
        changes = [
            self.change(timedelta(minutes=-1), 10),
            BurndownDataChange.create_aggregation_skip_marker(
                self.env, self.sprint_name()),
            self.change(timedelta(), 10),
            BurndownDataChange.create_aggregation_skip_marker(
                self.env, self.sprint_name()),
            self.change(timedelta(minutes=1), 10),
        ]
        aggregated = self.aggregate(changes)
        self.assert_length(4, aggregated)
        self.assert_equals(10, aggregated[0].remaining_time)
        self.assert_equals(10, aggregated[1].remaining_time)
        self.assert_equals(20,
                           aggregated[2].remaining_time)  # this is the jump
        self.assert_equals(30, aggregated[3].remaining_time)

    def test_only_changes_surrounded_with_markers_(self):
        changes = [
            BurndownDataChange.create_aggregation_skip_marker(
                self.env, self.sprint_name()),
            self.change(timedelta(), 10),
            self.change(timedelta(), 10),
            self.change(timedelta(), 10),
            BurndownDataChange.create_aggregation_skip_marker(
                self.env, self.sprint_name()),
        ]
        aggregated = self.aggregate(changes, interval=timedelta(hours=1))
        self.assert_length(2, aggregated)
        self.assert_equals(0, aggregated[0].remaining_time)
        self.assert_equals(30, aggregated[1].remaining_time)

    def test_can_add_additional_points_when_agregation_is_disabled_with_marker(
            self):
        changes = [
            self.change(timedelta(minutes=-30), 0),
            self.change(timedelta(minutes=-1), 10),
            BurndownDataChange.create_aggregation_skip_marker(
                self.env, self.sprint_name()),
            self.change(timedelta(minutes=1), 10),
            self.change(timedelta(minutes=30), 10),
        ]
        aggregated = self.aggregate(changes)
        self.assert_length(3, aggregated)
        self.assert_equals(0, aggregated[0].remaining_time)
        self.assert_equals(10, aggregated[1].remaining_time)
        self.assert_equals(30, aggregated[2].remaining_time)

    def test_can_have_skip_marker_as_first_entry(self):
        changes = [
            BurndownDataChange.create_aggregation_skip_marker(
                self.env, self.sprint_name()),
            self.change(timedelta(minutes=1), 10),
            self.change(timedelta(minutes=30), 10),
        ]
        aggregated = self.aggregate(changes)
        self.assert_length(2, aggregated)
        self.assert_equals(0, aggregated[0].remaining_time)
        self.assert_equals(20, aggregated[1].remaining_time)

    def test_can_have_two_markers(self):
        changes = [
            BurndownDataChange.create_aggregation_skip_marker(
                self.env, self.sprint_name()),
            BurndownDataChange.create_aggregation_skip_marker(
                self.env, self.sprint_name()),
        ]
        aggregated = self.aggregate(changes, interval=timedelta(hours=1))
        self.assert_length(2, aggregated)
        self.assert_equals(0, aggregated[0].remaining_time)
        self.assert_equals(0, aggregated[1].remaining_time)

    # --------------------------------------------------------------------------
    # filter by component

    def _generate_changes_with_components(self):
        self.teh.enable_burndown_filter()
        changes = [
            self.change(timedelta(hours=-10), 10),
            self.change(timedelta(hours=-5), 10),
            self.change(timedelta(),
                        5,
                        marker_key=Key.COMPONENT,
                        marker_value='fnord'),
            self.change(timedelta(),
                        8,
                        marker_key=Key.COMPONENT,
                        marker_value='foobardibub'),
        ]
        return changes

    def test_can_filter_by_component(self):
        changes = self._generate_changes_with_components()
        aggregated = self.aggregate(changes, filter_by_component='fnord')
        self.assert_length(1, aggregated)
        self.assert_equals(5, aggregated[0].remaining_time)

    def test_can_see_all_changes_if_filtering_is_enabled_but_not_selected(
            self):
        changes = self._generate_changes_with_components()
        aggregated = self.aggregate(changes)
        self.assert_length(4, aggregated)

    def test_can_see_all_changes_if_filtering_and_filter_is_empty(self):
        changes = self._generate_changes_with_components()
        aggregated = self.aggregate(changes, filter_by_component='')
        self.assert_length(4, aggregated)

    def test_throws_if_filtering_is_enabled_but_preferences_are_disabled(self):
        self.assert_raises(
            ValueError,
            lambda: self.aggregate([], filter_by_component='fnord'))

    # --------------------------------------------------------------------------
    # selecting db objects

    def test_can_select_changes(self):
        map(lambda change: change.save(), self.fixture())
        changes = self.aggregator.changes_for_sprint(self.sprint)
        self.assert_length(3, changes)

    def test_will_only_return_changes_for_sprint(self):
        other_sprint = self.teh.create_sprint('Fnord')
        map(lambda change: change.save(), self.fixture())
        self.change(timedelta(hours=2), 3000, scope=other_sprint).save()
        changes = self.aggregator.changes_for_sprint(self.sprint)
        self.assert_length(3, changes)

    def test_will_only_return_changes_for_specific_key(self):
        map(lambda change: change.save(), self.fixture())
        self.change(timedelta(hours=2), 3000, type='foo').save()
        changes = self.aggregator.changes_for_sprint(self.sprint)
        self.assert_length(3, changes)

    def test_return_changes_ordered_by_time(self):
        map(lambda change: change.save(), self.fixture())
        changes = self.aggregator.changes_for_sprint(self.sprint)
        self.assert_larger_than(changes[1].when, changes[0].when)
        self.assert_larger_than(changes[2].when, changes[1].when)
 def setUp(self):
     self.super()
     self.sprint = self.teh.create_sprint(self.sprint_name())
     self.aggregator = BurndownDataAggregator(self.env)
class TimeAggregationTest(AgiloTestCase):
    
    def setUp(self):
        self.super()
        self.sprint = self.teh.create_sprint(self.sprint_name())
        self.aggregator = BurndownDataAggregator(self.env)
    
    def change(self, time_delta, how_much, type=None, scope=None, marker_key=None, marker_value=True):
        instance = BurndownDataChange(self.env).update_values(
            type= type or BurndownDataConstants.REMAINING_TIME,
            scope=getattr(scope, 'name', self.sprint.name),
            when= now() + time_delta,
            delta=how_much,
        )
        if marker_key is not None:
            instance.update_marker(marker_key, marker_value)
        return instance
    
    def fixture(self):
        return [
            self.change(timedelta(hours=-3), -5),
            self.change(timedelta(hours=-1), -3),
            self.change(timedelta(hours=-5), 10),
        ]
    
    def aggregate(self, changes, interval=timedelta(hours=2), aggregate_until=None, discard_after=None, extend_until=None, filter_by_component=None):
        return self.aggregator.aggregate_changes_with_interval(changes, duration=interval, 
            aggregate_until=aggregate_until, discard_after=discard_after, extend_until=extend_until,
            filter_by_component=filter_by_component)
    
    
    
    def test_can_aggregate_zero_changes(self):
        self.assert_equals([], self.aggregate([]))
    
    def test_can_aggregate_one_change(self):
        first = self.change(timedelta(hours=0), 10)
        self.assert_equals([burndown_entry(first.when, first.delta())], self.aggregate([first]))
    
    def test_just_transforms_to_changes_if_no_aggregation_is_neccessary(self):
        first = self.change(timedelta(hours=0), 0)
        second = self.change(timedelta(hours=10), 6)
        aggregated = self.aggregate([first, second], interval=timedelta(hours=1))
        self.assert_length(2, aggregated)
        self.assert_equals(burndown_entry(first.when, first.delta()), aggregated[0])
        self.assert_equals(burndown_entry(second.when, first.delta() + second.delta()), aggregated[1])
    
    def test_can_add_up_durations(self):
        first = self.change(timedelta(hours=0), 10)
        second = self.change(timedelta(hours=10), 10)
        aggregated = self.aggregate([first, second], interval=timedelta(hours=1))
        self.assert_length(2, aggregated)
        self.assert_equals(burndown_entry(first.when, 10), aggregated[0])
        self.assert_equals(burndown_entry(second.when, 20), aggregated[1])
    
    def test_does_not_discard_start_or_end_value_even_if_interval_is_bigger_than_their_distance(self):
        first = self.change(timedelta(hours=0), 10)
        second = self.change(timedelta(hours=1), 10)
        aggregated = self.aggregate([first, second])
        self.assert_length(2, aggregated)
        self.assert_equals(burndown_entry(first.when, 10), aggregated[0])
        self.assert_equals(burndown_entry(second.when, 20), aggregated[1])
    
    def test_can_aggregate_multiple_changes_in_interval(self):
        first = self.change(timedelta(hours=0), 10)
        second = self.change(timedelta(hours=1), 10)
        third = self.change(timedelta(hours=2), 10)
        aggregated = self.aggregate([first, second, third])
        self.assert_length(2, aggregated)
        self.assert_equals(burndown_entry(first.when, 10), aggregated[0])
        self.assert_equals(burndown_entry(third.when, 30), aggregated[1])
    
    def test_can_extend_series_till_specified_time(self):
        first = self.change(timedelta(hours=-10), 10)
        end = now()
        aggregated = self.aggregate([first], extend_until=end)
        self.assert_equals([burndown_entry(first.when, first.delta()), burndown_entry(end, first.delta())], aggregated)
    
    def test_first_entry_is_always_at_start_of_sprint(self):
        change = self.change(timedelta(days=-100), 10)
        actual = self.aggregate([change], aggregate_until=self.sprint.start)
        self.assert_time_equals(self.sprint.start, actual[0].when)
    
    def test_will_aggregate_changes_before_sprint_start_to_sprint_start(self):
        first = self.change(timedelta(days=-100), 15)
        second = self.change(timedelta(days=-100), 23)
        actual = self.aggregate([first, second], aggregate_until=self.sprint.start)
        self.assert_equals(15+23, actual[0].remaining_time)
    
    def test_will_disregard_all_burndown_changes_after_sprint_end(self):
        change = self.change(timedelta(days=100), 200)
        self.assert_length(0, self.aggregate([change], discard_after=self.sprint.end))
    
    def test_changes_after_sprint_end_have_no_influence_on_burndown_chart_in_sprint(self):
        changes = [
            self.change(timedelta(), 10),
            self.change(timedelta(days=1), 10),
            self.change(timedelta(days=2), 10),
            self.change(timedelta(days=100), 1000)
        ]
        aggregated = self.aggregate(changes, discard_after=self.sprint.end, extend_until=self.sprint.end)
        self.assert_time_equals(changes[0].when, aggregated[0].when)
        self.assert_time_equals(self.sprint.end, aggregated[-1].when)
        self.assert_equals(30, aggregated[-1].remaining_time)
    
    def test_one_change_and_time_extension(self):
        self.change(timedelta(days=-20), 12).save()
        until = now() + timedelta(days=10)
        aggregated = self.aggregator.burndown_data_for_sprint(self.sprint, extend_until=until)
        self.assert_length(2, aggregated)
    
    def test_does_not_count_the_last_entry_twice(self):
        changes = [
            self.change(timedelta(), 0),
            self.change(timedelta(), 0),
            self.change(timedelta(), 0),
            self.change(timedelta(), 10),
        ]
        
        aggregated = self.aggregate(changes)
        self.assert_equals(0, aggregated[-2].remaining_time)
        self.assert_equals(10, aggregated[-1].remaining_time)
    
    
    # --------------------------------------------------------------------------------
    # Not aggregating over markers
    
    def test_can_have_normal_intersection_of_entries(self):
        changes = [
            self.change(timedelta(minutes=-1), 10),
            BurndownDataChange.create_aggregation_skip_marker(self.env, self.sprint_name()),
            self.change(timedelta(), 10),
            BurndownDataChange.create_aggregation_skip_marker(self.env, self.sprint_name()),
            self.change(timedelta(minutes=1), 10),
        ]
        aggregated = self.aggregate(changes)
        self.assert_length(4, aggregated)
        self.assert_equals(10, aggregated[0].remaining_time)
        self.assert_equals(10, aggregated[1].remaining_time)
        self.assert_equals(20, aggregated[2].remaining_time) # this is the jump
        self.assert_equals(30, aggregated[3].remaining_time)
    
    def test_only_changes_surrounded_with_markers_(self):
        changes = [
            BurndownDataChange.create_aggregation_skip_marker(self.env, self.sprint_name()),
            self.change(timedelta(), 10),
            self.change(timedelta(), 10),
            self.change(timedelta(), 10),
            BurndownDataChange.create_aggregation_skip_marker(self.env, self.sprint_name()),
        ]
        aggregated = self.aggregate(changes, interval=timedelta(hours=1))
        self.assert_length(2, aggregated)
        self.assert_equals(0, aggregated[0].remaining_time)
        self.assert_equals(30, aggregated[1].remaining_time)
    
    def test_can_add_additional_points_when_agregation_is_disabled_with_marker(self):
        changes = [
            self.change(timedelta(minutes=-30), 0),
            self.change(timedelta(minutes=-1), 10),
            BurndownDataChange.create_aggregation_skip_marker(self.env, self.sprint_name()),
            self.change(timedelta(minutes=1), 10),
            self.change(timedelta(minutes=30), 10),
        ]
        aggregated = self.aggregate(changes)
        self.assert_length(3, aggregated)
        self.assert_equals(0, aggregated[0].remaining_time)
        self.assert_equals(10, aggregated[1].remaining_time)
        self.assert_equals(30, aggregated[2].remaining_time)
    
    def test_can_have_skip_marker_as_first_entry(self):
        changes = [
            BurndownDataChange.create_aggregation_skip_marker(self.env, self.sprint_name()),
            self.change(timedelta(minutes=1), 10),
            self.change(timedelta(minutes=30), 10),
        ]
        aggregated = self.aggregate(changes)
        self.assert_length(2, aggregated)
        self.assert_equals(0, aggregated[0].remaining_time)
        self.assert_equals(20, aggregated[1].remaining_time)
    
    def test_can_have_two_markers(self):
        changes = [
            BurndownDataChange.create_aggregation_skip_marker(self.env, self.sprint_name()),
            BurndownDataChange.create_aggregation_skip_marker(self.env, self.sprint_name()),
        ]
        aggregated = self.aggregate(changes, interval=timedelta(hours=1))
        self.assert_length(2, aggregated)
        self.assert_equals(0, aggregated[0].remaining_time)
        self.assert_equals(0, aggregated[1].remaining_time)
    
    # --------------------------------------------------------------------------
    # filter by component
    
    def _generate_changes_with_components(self):
        self.teh.enable_burndown_filter()
        changes = [
            self.change(timedelta(hours=-10), 10),
            self.change(timedelta(hours=-5), 10),
            self.change(timedelta(), 5, marker_key=Key.COMPONENT, marker_value='fnord'),
            self.change(timedelta(), 8, marker_key=Key.COMPONENT, marker_value='foobardibub'),
        ]
        return changes
    
    def test_can_filter_by_component(self):
        changes = self._generate_changes_with_components()
        aggregated = self.aggregate(changes, filter_by_component='fnord')
        self.assert_length(1, aggregated)
        self.assert_equals(5, aggregated[0].remaining_time)
    
    def test_can_see_all_changes_if_filtering_is_enabled_but_not_selected(self):
        changes = self._generate_changes_with_components()
        aggregated = self.aggregate(changes)
        self.assert_length(4, aggregated)
    
    def test_can_see_all_changes_if_filtering_and_filter_is_empty(self):
        changes = self._generate_changes_with_components()
        aggregated = self.aggregate(changes, filter_by_component='')
        self.assert_length(4, aggregated)
    
    def test_throws_if_filtering_is_enabled_but_preferences_are_disabled(self):
        self.assert_raises(ValueError, lambda: self.aggregate([], filter_by_component='fnord'))
    
    
    # --------------------------------------------------------------------------
    # selecting db objects
    
    def test_can_select_changes(self):
        map(lambda change: change.save(), self.fixture())
        changes = self.aggregator.changes_for_sprint(self.sprint)
        self.assert_length(3, changes)
    
    def test_will_only_return_changes_for_sprint(self):
        other_sprint = self.teh.create_sprint('Fnord')
        map(lambda change: change.save(), self.fixture())
        self.change(timedelta(hours=2), 3000, scope=other_sprint).save()
        changes = self.aggregator.changes_for_sprint(self.sprint)
        self.assert_length(3, changes)
    
    def test_will_only_return_changes_for_specific_key(self):
        map(lambda change: change.save(), self.fixture())
        self.change(timedelta(hours=2), 3000, type='foo').save()
        changes = self.aggregator.changes_for_sprint(self.sprint)
        self.assert_length(3, changes)
    
    def test_return_changes_ordered_by_time(self):
        map(lambda change: change.save(), self.fixture())
        changes = self.aggregator.changes_for_sprint(self.sprint)
        self.assert_larger_than(changes[1].when, changes[0].when)
        self.assert_larger_than(changes[2].when, changes[1].when)
 def setUp(self):
     self.super()
     self.sprint = self.teh.create_sprint(self.sprint_name())
     self.aggregator = BurndownDataAggregator(self.env)
     self.confirmer = BurndownDataConfirmCommitment(self.env)
class ConfirmCommitmentTest(AgiloTestCase):
    def setUp(self):
        self.super()
        self.sprint = self.teh.create_sprint(self.sprint_name())
        self.aggregator = BurndownDataAggregator(self.env)
        self.confirmer = BurndownDataConfirmCommitment(self.env)

    def changes(self, sprint=None):
        return self.aggregator.changes_for_sprint(
            sprint and ValueObject(name=sprint) or self.sprint)

    def create_change(self,
                      delta=23,
                      marker_key=None,
                      marker_value=None,
                      **kwargs):
        change = BurndownDataChange(self.env)
        change.type = BurndownDataConstants.REMAINING_TIME
        change.scope = self.sprint.name
        change.when = now()
        change.set_delta(delta)
        for key, value in kwargs.items():
            setattr(change, key, value)
        if marker_key is not None:
            change.update_marker(marker_key, marker_value)
        return change

    def insert_changes(self):
        self.create_change().save()
        self.create_change().save()
        self.create_change().save()

    def test_can_remove_all_entries_that_are_already_set_for_a_sprint(self):
        self.insert_changes()
        self.confirmer.remove_old_changes_for_sprint(self.sprint)
        self.assert_length(0, self.changes())

    # TODO: might need to care about the key so story burndown values are not / too removed?
    def test_will_not_touch_entries_for_other_sprints_when_removing(self):
        self.insert_changes()
        self.create_change(scope='fnord').save()
        self.confirmer.remove_old_changes_for_sprint(self.sprint)
        self.assert_length(0, self.changes())
        self.assert_length(1, self.changes(sprint='fnord'))

    def test_sum_remaining_time_in_sprint(self):
        self.create_change(delta=3).save()
        self.assert_equals(
            3, self.confirmer.sum_remaining_time_for_sprint(self.sprint))
        self.create_change(delta=5).save()
        self.assert_equals(
            8, self.confirmer.sum_remaining_time_for_sprint(self.sprint))
        self.create_change(delta=10).save()
        self.assert_equals(
            18, self.confirmer.sum_remaining_time_for_sprint(self.sprint))
        self.create_change(delta=-12).save()
        self.assert_equals(
            6, self.confirmer.sum_remaining_time_for_sprint(self.sprint))

    def test_adding_initial_remaining_time(self):
        self.confirmer.add_initial_change_for_sprint_with_remaining_time(
            self.sprint, 10)
        self.assert_length(1, self.changes())
        self.assert_equals(10, self.changes()[0].delta())

    def test_created_initial_burndown_element_has_correct_time(self):
        self.confirmer.add_initial_change_for_sprint_with_remaining_time(
            self.sprint, 23)
        self.assert_almost_equals(now(),
                                  self.changes()[0].when,
                                  max_delta=timedelta(seconds=2))

    def test_enters_sum_of_removed_burndown_as_first_entry_after_removing_all_others(
            self):
        self.create_change(delta=10).save()
        self.create_change(delta=7).save()
        self.create_change(delta=6).save()
        self.confirmer.confirm_commitment_for_sprint(self.sprint)
        self.assert_length(1, self.changes())
        self.assert_equals(23, self.changes()[0].delta())

    # TODO: Consider to only remove ticket-change-entries until the target time!
    def test_can_set_time_of_commitment(self):
        self.create_change(delta=30).save()
        some_time_ago = now() - timedelta(days=3)
        self.confirmer.confirm_commitment_for_sprint(self.sprint,
                                                     when=some_time_ago)
        self.assert_length(1, self.changes())
        self.assert_almost_equals(some_time_ago,
                                  self.changes()[0].when,
                                  max_delta=timedelta(seconds=2))

    def test_can_add_metadata_with_commitment_for_each_component_if_filtered_burndown_is_activated(
            self):
        self.teh.enable_burndown_filter()
        self.create_change(delta=3,
                           when=self.sprint.start,
                           marker_key=Key.COMPONENT,
                           marker_value='foo').save()
        self.create_change(delta=5,
                           when=self.sprint.start,
                           marker_key=Key.COMPONENT,
                           marker_value='bar').save()
        self.create_change(delta=7, when=self.sprint.start).save()
        self.confirmer.confirm_commitment_for_sprint(self.sprint,
                                                     when=self.sprint.start)
        self.assert_length(1, self.changes())
        change = self.changes()[0]
        self.assert_true(
            change.has_marker(BurndownDataConstants.DELTAS_BY_COMPONENT))
        deltas = change.marker_value(BurndownDataConstants.DELTAS_BY_COMPONENT)
        self.assert_equals(dict(foo=3, bar=5), deltas)

    def test_stores_correct_component_metadata_if_confirm_commitment_is_pressed_twice(
            self):
        self.teh.enable_burndown_filter()
        self.create_change(delta=3,
                           when=self.sprint.start,
                           marker_key=Key.COMPONENT,
                           marker_value='foo').save()
        self.confirmer.confirm_commitment_for_sprint(self.sprint,
                                                     when=self.sprint.start)
        self.confirmer.confirm_commitment_for_sprint(self.sprint,
                                                     when=self.sprint.start)

        self.assert_length(1, self.changes())
        change = self.changes()[0]
        deltas = change.marker_value(BurndownDataConstants.DELTAS_BY_COMPONENT)
        self.assert_equals(dict(foo=3), deltas)