diff --git a/app/models.py b/app/models.py index 69217b7892845855765b09e0302583776f123915..4832add6af07bc80d2a48b4bb82479538bdbf421 100644 --- a/app/models.py +++ b/app/models.py @@ -9,6 +9,7 @@ This module implements the models used in the app. :license: BSD 2-Clause, see LICENSE for more details. """ +import datetime import ipaddress import string import qrcode @@ -283,10 +284,24 @@ class User(db.Model, UserMixin): return Task.query.filter_by(name=name, status=JobStatus.STARTED).first() def is_task_waiting(self, name): - """Return True if a <name> task is waiting (queued or deferred)""" + """Return True if a <name> task is waiting + + Waiting means: + - queued + - deferred if not older than 30 minutes + + A deferred task can stay deferred forever if the task it depends on fails. + """ + thirty_minutes_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=30) count = ( Task.query.filter_by(name=name) - .filter(Task.status.in_([JobStatus.DEFERRED, JobStatus.QUEUED])) + .filter( + (Task.status == JobStatus.QUEUED) + | ( + (Task.status == JobStatus.DEFERRED) + & (Task.created_at > thirty_minutes_ago) + ) + ) .count() ) return count > 0 diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 0774d78e9cc4c657d47ca635572198ae22b2bc6b..7bace7fd3ce3cac2079cf717bd08e4b8a5078eb9 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -34,6 +34,7 @@ register(factories.MacFactory) register(factories.DomainFactory) register(factories.CnameFactory) register(factories.TagFactory) +register(factories.TaskFactory) @pytest.fixture(scope="session") diff --git a/tests/functional/factories.py b/tests/functional/factories.py index ace920c5d6b41f3764fbc44a230554ff2b872a42..04805e5c67568f18a64a53213229a2eaaa4cb020 100644 --- a/tests/functional/factories.py +++ b/tests/functional/factories.py @@ -11,14 +11,10 @@ This module defines models factories. """ import ipaddress import factory -from faker import Factory as FakerFactory from app import models from . import common -faker = FakerFactory.create() - - class UserFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = models.User @@ -210,3 +206,14 @@ class TagFactory(factory.alchemy.SQLAlchemyModelFactory): sqlalchemy_session_persistence = "commit" name = factory.Sequence(lambda n: f"Tag{n}") + + +class TaskFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = models.Task + sqlalchemy_session = common.Session + sqlalchemy_session_persistence = "commit" + + id = factory.Faker("uuid4") + name = factory.Sequence(lambda n: f"task{n}") + user = factory.SubFactory(UserFactory) diff --git a/tests/functional/test_models.py b/tests/functional/test_models.py index 96aa143c2759dac25255fe949dbcddbedda1f804..8bbdfaedd2112d3e01d20b5bc956f4a8ebf099f6 100644 --- a/tests/functional/test_models.py +++ b/tests/functional/test_models.py @@ -9,6 +9,7 @@ This module defines models tests. :license: BSD 2-Clause, see LICENSE for more details. """ +import datetime import ipaddress import pytest from wtforms import ValidationError @@ -287,3 +288,67 @@ def test_ansible_dynamic_network_scope_group( assert group_s1.hosts == [host1_s1, host2_s1] assert group_s2.hosts == [host1_s2] assert group_s3.hosts == [] + + +@pytest.mark.parametrize("status", [None, "FINISHED", "FAILED", "STARTED"]) +def test_no_task_waiting(status, user, task_factory): + if status is None: + task_factory(name="my-task", status=None) + else: + task_factory(name="my-task", status=models.JobStatus[status]) + assert not user.is_task_waiting("my-task") + + +@pytest.mark.parametrize("status", ["QUEUED", "DEFERRED"]) +def test_task_waiting(status, user, task_factory): + task_factory(name="my-task", status=models.JobStatus[status]) + assert user.is_task_waiting("my-task") + + +@pytest.mark.parametrize("minutes", [5, 10, 29]) +def test_task_waiting_with_recent_deferred(minutes, user, task_factory): + minutes_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=minutes) + task_factory( + created_at=minutes_ago, name="my-task", status=models.JobStatus.DEFERRED + ) + assert user.is_task_waiting("my-task") + + +@pytest.mark.parametrize("minutes", [31, 60, 7200]) +def test_no_task_waiting_with_old_deferred(minutes, user, task_factory): + minutes_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=minutes) + task_factory( + created_at=minutes_ago, name="my-task", status=models.JobStatus.DEFERRED + ) + assert not user.is_task_waiting("my-task") + + +@pytest.mark.parametrize("minutes", [5, 30, 7200]) +def test_task_waiting_with_old_queued(minutes, user, task_factory): + minutes_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=minutes) + task_factory(created_at=minutes_ago, name="my-task", status=models.JobStatus.QUEUED) + assert user.is_task_waiting("my-task") + + +def test_get_task_started_when_only_one(user, task_factory): + task_factory(name="my-task", status=models.JobStatus.QUEUED) + task = task_factory(name="my-task", status=models.JobStatus.STARTED) + task_factory(name="my-task", status=models.JobStatus.FINISHED) + task_factory(name="my-task", status=models.JobStatus.FAILED) + assert user.get_task_started("my-task") == task + + +def test_get_task_started_when_several(user, task_factory): + # Only the first started task is returned + task1 = task_factory(name="my-task", status=models.JobStatus.STARTED) + task_factory(name="my-task", status=models.JobStatus.STARTED) + assert user.get_task_started("my-task") == task1 + + +def test_get_tasks_in_progress(user, task_factory): + task1 = task_factory(name="my-task", status=models.JobStatus.QUEUED) + task2 = task_factory(name="my-task", status=models.JobStatus.STARTED) + task_factory(name="my-task", status=models.JobStatus.FINISHED) + task_factory(name="my-task", status=models.JobStatus.FAILED) + task3 = task_factory(name="my-task", status=models.JobStatus.DEFERRED) + assert user.get_tasks_in_progress("my-task") == [task1, task2, task3]