From e1ec9e85de6090fd3ac0bcdbd8f7a25dc4b79f1f Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 10 Dec 2021 23:28:49 +0100 Subject: [PATCH] Add: Tests for automation history view Add: Support for Split/Join in history view --- runtests.py | 55 ++++++++++++----- src/automations/flow.py | 13 ++-- src/automations/models.py | 14 +++++ .../templates/automations/history.html | 40 ++++++------- .../automations/includes/history.html | 23 +++++--- .../automations/includes/history_item.html | 4 +- src/automations/templates/base.html | 3 + src/automations/tests/test_automations.py | 59 +++++++++++++++---- src/automations/views.py | 35 +++++++---- 9 files changed, 172 insertions(+), 74 deletions(-) create mode 100644 src/automations/templates/base.html diff --git a/runtests.py b/runtests.py index dc970ac..60109a3 100644 --- a/runtests.py +++ b/runtests.py @@ -9,13 +9,13 @@ from django.core.management import call_command -def runtests(test_path='automations.tests'): +def runtests(test_path="automations.tests"): if not settings.configured: # Choose database for settings DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", } } # test_db = os.environ.get('DB', 'sqlite') @@ -37,31 +37,54 @@ def runtests(test_path='automations.tests'): SECRET_KEY="verysecretkeyfortesting", DATABASES=DATABASES, INSTALLED_APPS=( - 'django.contrib.contenttypes', - 'django.contrib.auth', - 'django.contrib.sessions', - 'automations', + "django.contrib.contenttypes", + "django.contrib.auth", + "django.contrib.sessions", + "automations", ), ROOT_URLCONF="automations.urls", # tests override urlconf, but it still needs to be defined LANGUAGES=( - ('en', 'English'), - ('de', 'Deutsch'), - + ("en", "English"), + ("de", "Deutsch"), + ), + MIDDLEWARE=( + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", ), - MIDDLEWARE_CLASSES=(), + TEMPLATES=[ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ( + "src/automations/templates", + # insert your TEMPLATE_DIRS here + ), + "OPTIONS": { + "context_processors": ( + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.template.context_processors.request", + ), + }, + }, + ], ) django.setup() - warnings.simplefilter('always', DeprecationWarning) + warnings.simplefilter("always", DeprecationWarning) failures = call_command( - 'test', test_path, interactive=False, failfast=False, verbosity=2) + "test", test_path, interactive=False, failfast=False, verbosity=2 + ) sys.exit(bool(failures)) -if __name__ == '__main__': +if __name__ == "__main__": sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), "src")) parser = OptionParser() (options, args) = parser.parse_args() - runtests(*args) \ No newline at end of file + runtests(*args) diff --git a/src/automations/flow.py b/src/automations/flow.py index a4d4432..2fad191 100644 --- a/src/automations/flow.py +++ b/src/automations/flow.py @@ -404,12 +404,17 @@ def execute(self, task: models.AutomationTaskModel): if ( len(all_splits) > 1 ): # more than one split at the moment: close this split - if task.message == "Join": - self.store_result(task, "Join", task.result + [task.id]) - else: - self.store_result(task, "Join", [task.id]) self.leave(task) + self.store_result(task, "Open Join", []) # Flag as open return None + else: + all_path_ends = self._automation._db.automationtaskmodel_set.filter( + message="Open Join", status=task.status # Find open + ) + self.store_result( + task, "Joined", [tsk.id for tsk in all_path_ends] + [task.id] + ) + all_path_ends.update(message="Joined") # Join closed: clear flag return task def get_split(self, task): diff --git a/src/automations/models.py b/src/automations/models.py index 24962bd..b4b9243 100644 --- a/src/automations/models.py +++ b/src/automations/models.py @@ -182,6 +182,14 @@ def get_node(self): instance = self.automation.instance return getattr(instance, self.status) + def get_previous_tasks(self): + if self.message == "Joined" and self.result: + return self.__class__.objects.filter(id__in=self.result) + return [self.previous] if self.previous else [] + + def get_next_tasks(self): + return self.automationtaskmodel_set.all() + @classmethod def get_open_tasks(cls, user): candidates = cls.objects.filter(finished=None) @@ -215,6 +223,12 @@ def get_users_with_permission( users |= User.objects.filter(is_superuser=True) return users + def __str__(self): + return f"" + + def __repr__(self): + return self.__str__() + def swap_users_with_permission_model_method(model, settings_conf): """ diff --git a/src/automations/templates/automations/history.html b/src/automations/templates/automations/history.html index 7eff798..513afe1 100644 --- a/src/automations/templates/automations/history.html +++ b/src/automations/templates/automations/history.html @@ -1,26 +1,24 @@ {% extends "automations/base.html" %}{% load i18n %} {% block content_automations %}{% spaceless %} -

- {{ automation.get_automation_class.get_verbose_name }}
- {{ automation.automation_class }} -

-
-
- {% include "automations/includes/history.html" with blocks=blocks %} -
-
-
-

{% trans "Automation info" %} {{ automation.id }}

-
    -
  • {% trans "Updated:" %} {{ automation.updated }}
  • -
  • {% trans "Paused until:" %} {{ automation.paused_until }}
  • -
  • {% trans "Created:" %} {{ automation.created }}
  • -
-
-

{% trans "Data" %}

-
{{ automation.data }}
-
+
+

+ {{ automation.get_automation_class.get_verbose_name }} + {{ automation.automation_class }} +

+
    +
  • {% trans "Automation id" %} {{ automation.id }}
  • +
  • {% trans "Updated:" %} {{ automation.updated }}
  • +
  • {% trans "Paused until:" %} {{ automation.paused_until }}
  • +
  • {% trans "Created:" %} {{ automation.created }}
  • +
+ {% if automation.data %} +
+

{% trans "Data" %}

+
{{ automation.data }}
-
+ {% endif %} +
+ {% include "automations/includes/history.html" with tasks=tasks %} +
{% endspaceless %} {% endblock %} diff --git a/src/automations/templates/automations/includes/history.html b/src/automations/templates/automations/includes/history.html index dbdfb47..ba68dd0 100644 --- a/src/automations/templates/automations/includes/history.html +++ b/src/automations/templates/automations/includes/history.html @@ -1,10 +1,15 @@ -
- {% for block in blocks %} -
- {% if block.children %} - {% include "automations/includes/history.html" with blocks=block.children %} - {% endif %} - {% include "automations/includes/history_item.html" with task=block.task %} -
+{% spaceless %} + {% for task in tasks reversed %} + {% if task|length > 1 %} +
+ {% for sub_tree in task %} +
+ {% include "automations/includes/history.html" with tasks=sub_tree %} +
+ {% endfor %} +
+ {% else %} + {% include "automations/includes/history_item.html" with task=task %} + {% endif %} {% endfor %} -
+{% endspaceless %} diff --git a/src/automations/templates/automations/includes/history_item.html b/src/automations/templates/automations/includes/history_item.html index 9c05141..4ef38ed 100644 --- a/src/automations/templates/automations/includes/history_item.html +++ b/src/automations/templates/automations/includes/history_item.html @@ -1,8 +1,8 @@ {% load i18n %}{% spaceless %}
-

{{ task.status }} = flow.{{ task.get_node.node_name }}() +

{{ task.status }} = flow.{{ task.get_node.node_name }}() {% if task.finished %}{{ task.finished }}{% else %}{% trans "running" %}{% endif %}

- {% if task.message == "OK" %}
{{ task.result }}
{% endif %} + {% if task.message == "OK" and task.result %}
{{ task.result }}
{% endif %} {% if "Error" in task.message %}{% endif %}
{% endspaceless %} diff --git a/src/automations/templates/base.html b/src/automations/templates/base.html new file mode 100644 index 0000000..10d35e2 --- /dev/null +++ b/src/automations/templates/base.html @@ -0,0 +1,3 @@ +{# only for testing #} +{% block content %} +{% endblock %} \ No newline at end of file diff --git a/src/automations/tests/test_automations.py b/src/automations/tests/test_automations.py index 66b89c3..33357ad 100644 --- a/src/automations/tests/test_automations.py +++ b/src/automations/tests/test_automations.py @@ -9,7 +9,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.core.management import execute_from_command_line -from django.test import RequestFactory, TestCase, override_settings +from django.test import Client, RequestFactory, TestCase, override_settings from django.utils.timezone import now from django.utils.translation import gettext as _ @@ -88,14 +88,21 @@ class Meta: ) l10 = Print("Line 10").AfterWaitingUntil(now() - datetime.timedelta(minutes=1)) split = flow.Split().Next("self.t10").Next("self.t20").Next("self.t30") + join = flow.Join() l20 = Print("All joined now") l30 = flow.End() - t10 = Print("Thread 10").Next(this.join) + t10 = Print("Thread 10").Next(this.split_again) t20 = Print("Thread 20").Next(this.join) t30 = Print("Thread 30").Next(this.join) + split_again = flow.Split().Next(this.t40).Next(this.t50) + t40 = Print("Thread 40").Next(this.join_again) + t50 = Print("Thread 50").Next(this.join_again) + join_again = flow.Join() + going_back = Print("Sub split joined").Next(this.join) + class AtmTaskForm(forms.ModelForm): class Meta: @@ -132,7 +139,7 @@ class Looping(flow.Automation): class BoundToFail(flow.Automation): - start = Print("Will divide by zero.") + start = Print("Will divide by zero.").SkipAfter(datetime.timedelta(days=1)) div = flow.Execute(lambda x: 5 / 0).OnError(this.error_node) never = Print("This should NOT be printed") not_caught = flow.Execute(lambda x: 5 / 0) @@ -163,7 +170,9 @@ def test_modelsetup(self): x = TestAutomation(autorun=False) qs = AutomationModel.objects.all() self.assertEqual(len(qs), 1) - self.assertEqual(qs[0].automation_class, "automations.tests.test_automations.TestAutomation") + self.assertEqual( + qs[0].automation_class, "automations.tests.test_automations.TestAutomation" + ) self.assertEqual(get_automation_class(x._db.automation_class), TestAutomation) @@ -193,6 +202,9 @@ def test_split_join(self): self.assertIn("t10 Thread 10", output) self.assertIn("t20 Thread 20", output) self.assertIn("t30 Thread 30", output) + self.assertIn("t40 Thread 40", output) + self.assertIn("t50 Thread 50", output) + self.assertIn("going_back Sub split joined", output) self.assertEqual(atm.get_verbose_name(), "Allow to split and join") self.assertEqual(atm.get_verbose_name_plural(), "Allow splitS and joinS") @@ -258,6 +270,29 @@ def test_form(self): self.assertEqual(len(atm.form2.get_users_with_permission()), 0) +class HistoryTestCase(TestCase): + def setUp(self): + # Every test needs access to the request factory. + self.client = Client() + self.admin = User.objects.create_user( + username="admin", + email="admin@...", + password="Even More Secr3t", + is_staff=True, + is_superuser=True, + ) + self.admin.save() + self.assertEqual(self.admin.is_superuser, True) + login = self.client.login(username="admin", password="Even More Secr3t") + self.failUnless(login, "Could not login") + + def test_history_test(self): + atm = TestSplitJoin() + response = self.client.get(f"/dashboard/{atm._db.id}") + self.assertEqual(response.status_code, 200) + self.assertIn("split_again = flow.Split()", response.content.decode("utf8")) + + test_signal = django.dispatch.Signal() @@ -367,9 +402,9 @@ def test_managment_delete_command(self): execute_from_command_line(["manage.py", "automation_delete_history", "0"]) output = fake_out.getvalue().splitlines() self.assertIn( - "12 total objects deleted, including 1 AutomationModel instances, and 11 " + "18 total objects deleted, including 1 AutomationModel instances, and 17 " "AutomationTaskModel instances", - output + output, ) self.assertEqual(AutomationModel.objects.count(), 0) self.assertEqual(AutomationTaskModel.objects.count(), 0) @@ -513,7 +548,6 @@ def test_errors(self): self.assertEqual( atm._db.automationtaskmodel_set.all()[1].message, "SyntaxError('Darn, this is not good')", - ) self.assertIn( "error", @@ -631,9 +665,7 @@ def test_method_swaps(self): class AutomationReprTest(TestCase): - def test_automation_repr(self): - class TinyAutomation(flow.Automation): start = flow.Execute(this.init) intermediate = flow.Execute(this.end) @@ -644,8 +676,13 @@ def init(self, task, *args, **kwargs): automation_dict = str(TinyAutomation.__dict__) - self.assertIn("'__module__': 'automations.tests.test_automations'", automation_dict) + self.assertIn( + "'__module__': 'automations.tests.test_automations'", automation_dict + ) self.assertIn("'start': ", automation_dict) self.assertIn("'intermediate': ", automation_dict) self.assertIn("'end': ", automation_dict) - self.assertIn("'init': 1: # Split + lst = [] + for tsk in tasks: + sub_tree, next_task = self.build_tree(tsk) + lst.append(sub_tree) + result.append(lst) + if next_task: + result.append(next_task) + tasks = next_task.get_next_tasks() if next_task else [] else: - return + task = tasks[0] + if task.message == "Joined": # Closed Join + return result, task + result.append(task) + tasks = task.get_next_tasks() + return result, None + + def get_context_data(self, **kwargs): assert "automation_id" in kwargs automation = get_object_or_404( models.AutomationModel, id=kwargs.get("automation_id") ) - tasks = automation.automationtaskmodel_set.filter(previous__isnull=True) - blocks = [{"task": task, "children": get_child_tasks(task)} for task in tasks] + task = automation.automationtaskmodel_set.get(previous=None) + tasks, _ = self.build_tree(task) return dict( automation=automation, - blocks=blocks, + tasks=tasks, )