diff --git a/changes.d/6388.fix.md b/changes.d/6388.fix.md new file mode 100644 index 0000000000..7fc3c8b734 --- /dev/null +++ b/changes.d/6388.fix.md @@ -0,0 +1 @@ +Fix task state filtering in Tui. diff --git a/cylc/flow/tui/data.py b/cylc/flow/tui/data.py index 948d1906fe..a6cd60c6bb 100644 --- a/cylc/flow/tui/data.py +++ b/cylc/flow/tui/data.py @@ -65,7 +65,7 @@ meanElapsedTime } } - familyProxies(exids: ["*/root"], states: $taskStates) { + familyProxies(exids: ["*/root"]) { id name cyclePoint @@ -78,13 +78,18 @@ name } } - cyclePoints: familyProxies(ids: ["*/root"], states: $taskStates) { + cyclePoints: familyProxies(ids: ["*/root"]) { id + name cyclePoint state isHeld isQueued isRunahead + firstParent { + id + name + } } } } diff --git a/cylc/flow/tui/updater.py b/cylc/flow/tui/updater.py index 84ad7e786b..fe19a34d0a 100644 --- a/cylc/flow/tui/updater.py +++ b/cylc/flow/tui/updater.py @@ -258,7 +258,10 @@ async def _run_update(self, data): ) ) - return compute_tree(data) + # are any task state filters active? + task_filters_active = not all(self.filters['tasks'].values()) + + return compute_tree(data, prune_families=task_filters_active) async def _update_workflow(self, w_id, client, data): if not client: diff --git a/cylc/flow/tui/util.py b/cylc/flow/tui/util.py index f7bc6b38d6..d0ea2a651c 100644 --- a/cylc/flow/tui/util.py +++ b/cylc/flow/tui/util.py @@ -21,7 +21,7 @@ from itertools import zip_longest import re from time import time -from typing import Tuple +from typing import Any, Dict, Optional, Set, Tuple import urwid @@ -44,6 +44,10 @@ ME = getuser() +Node = Dict[str, Any] +NodeStore = Dict[str, Dict[str, Node]] + + @contextmanager def suppress_logging(): """Suppress Cylc logging. @@ -139,12 +143,21 @@ def idpop(id_): return tokens.id -def compute_tree(data): - """Digest GraphQL data to produce a tree.""" - root_node = add_node('root', 'root', {}, data={}) +def compute_tree(data: dict, prune_families: bool = False) -> Node: + """Digest GraphQL data to produce a tree. + + Args: + data: + The workflow data as returned from the GraphQL query. + prune_families: + If True any empty families will be removed from the tree. + Turn this on if task state filters are active. + + """ + root_node: Node = add_node('root', 'root', create_node_store(), data={}) for flow in data['workflows']: - nodes = {} + nodes: NodeStore = create_node_store() # nodes for this workflow flow_node = add_node( 'workflow', flow['id'], nodes, data=flow) root_node['children'].append(flow_node) @@ -199,9 +212,13 @@ def compute_tree(data): job_node['children'] = [job_info_node] task_node['children'].append(job_node) + # trim empty families / cycles (cycles are just "root" families) + if prune_families: + _prune_empty_families(nodes) + # sort - for (type_, _), node in nodes.items(): - if type_ != 'task': + for type_ in ('workflow', 'cycle', 'family', 'job'): + for node in nodes[type_].values(): # NOTE: jobs are sorted by submit-num in the GraphQL query node['children'].sort( key=lambda x: NaturalSort(x['id_']) @@ -225,6 +242,62 @@ def compute_tree(data): return root_node +def _prune_empty_families(nodes: NodeStore) -> None: + """Prune empty families from the tree. + + Note, cycles are "root" families. + + We can end up with empty families when filtering by task state. We filter + tasks by state in the GraphQL query (so we don't need to perform this + filtering client-side), however, we cannot filter families by state because + the family state is an aggregate representing a collection of tasks (and or + families). + + Args: + nodes: Dictionary containing all nodes present in the tree. + + """ + # go through all families and cycles + stack: Set[str] = {*nodes['family'], *nodes['cycle']} + while stack: + family_id = stack.pop() + + if family_id in nodes['family']: + # this is a family + family = nodes['family'][family_id] + if len(family['children']) > 0: + continue + + # this family is empty -> find its parent (family/cycle) + _first_parent = family['data']['firstParent'] + if _first_parent['name'] == 'root': + parent_type = 'cycle' + parent_id = idpop(_first_parent['id']) + else: + parent_type = 'family' + parent_id = _first_parent['id'] + + elif family_id in nodes['cycle']: + # this is a cycle + family = nodes['cycle'][family_id] + if len(family['children']) > 0: + continue + + # this cycle is empty -> find its parent (workflow) + parent_type = 'workflow' + parent_id = idpop(family_id) + + else: + # this node has already been pruned + continue + + # remove the node from its parent + nodes[parent_type][parent_id]['children'].remove(family) + if parent_type in {'family', 'cycle'}: + # recurse up the family tree + stack.add(parent_id) + + class NaturalSort: """An object to use as a sort key for sorting strings as a human would. @@ -309,16 +382,39 @@ def __lt__(self, other): return False -def dummy_flow(data): +def dummy_flow(data) -> Node: return add_node( 'workflow', data['id'], - {}, + create_node_store(), data ) -def add_node(type_, id_, nodes, data=None): +def create_node_store() -> NodeStore: + """Returns a "node store" dictionary for use with add_nodes.""" + return { # node_type: {node_id: node} + # the root of the tree + 'root': {}, + # spring nodes load the workflow when visited + '#spring': {}, + # workflow//cycle//job + 'workflow': {}, + 'cycle': {}, + 'family': {}, + 'task': {}, + 'job': {}, + # the node under a job that contains metadata (platform, job_id, etc) + 'job_info': {}, + } + + +def add_node( + type_: str, + id_: str, + nodes: NodeStore, + data: Optional[dict] = None, +) -> Node: """Create a node add it to the store and return it. Arguments: @@ -336,14 +432,14 @@ def add_node(type_, id_, nodes, data=None): dict - The requested node. """ - if (type_, id_) not in nodes: - nodes[(type_, id_)] = { + if id_ not in nodes[type_]: + nodes[type_][id_] = { 'children': [], 'id_': id_, 'data': data or {}, 'type_': type_ } - return nodes[(type_, id_)] + return nodes[type_][id_] def get_job_icon(status): diff --git a/tests/integration/tui/screenshots/test_task_states.filter-not-waiting-or-expired.html b/tests/integration/tui/screenshots/test_task_states.filter-not-waiting-or-expired.html new file mode 100644 index 0000000000..3f9d6ee97e --- /dev/null +++ b/tests/integration/tui/screenshots/test_task_states.filter-not-waiting-or-expired.html @@ -0,0 +1,31 @@ +
Cylc Tui   tasks filtered (F - edit, R - reset)   workflows filtered (W - edit, 
+E - reset)                                                                      
+                                                                                
+- ~cylc                                                                         
+   - test_task_states - paused 1■ 1■ 1■ 1■ 1■                                   
+      - ̿⊗ 1                                                                     
+         - ̿● X                                                                  
+              ̿● a                                                               
+         - ̿⊗ Y                                                                  
+              ̿⊗ b                                                               
+      - ̿⊘ 2                                                                     
+         - ̿⊙ X                                                                  
+              ̿⊙ a                                                               
+         - ̿⊘ Y                                                                  
+            - ̿⊘ Y1                                                              
+                 ̿⊘ c                                                            
+              ̿⊙ b                                                               
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
+filter tasks: T f s r R  filter workflows: W E p                                
+
\ No newline at end of file diff --git a/tests/integration/tui/screenshots/test_task_states.filter-not-waiting.html b/tests/integration/tui/screenshots/test_task_states.filter-not-waiting.html new file mode 100644 index 0000000000..40bb126756 --- /dev/null +++ b/tests/integration/tui/screenshots/test_task_states.filter-not-waiting.html @@ -0,0 +1,31 @@ +
Cylc Tui   tasks filtered (F - edit, R - reset)   workflows filtered (W - edit, 
+E - reset)                                                                      
+                                                                                
+- ~cylc                                                                         
+   - test_task_states - paused 1■ 1■ 1■ 1■ 1■                                   
+      - ̿⊗ 1                                                                     
+         - ̿● X                                                                  
+              ̿● a                                                               
+         - ̿⊗ Y                                                                  
+            - ̿◌ Y1                                                              
+                 ̿◌ c                                                            
+              ̿⊗ b                                                               
+      - ̿⊘ 2                                                                     
+         - ̿⊙ X                                                                  
+              ̿⊙ a                                                               
+         - ̿⊘ Y                                                                  
+            - ̿⊘ Y1                                                              
+                 ̿⊘ c                                                            
+              ̿⊙ b                                                               
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
+filter tasks: T f s r R  filter workflows: W E p                                
+
\ No newline at end of file diff --git a/tests/integration/tui/screenshots/test_task_states.filter-submitted.html b/tests/integration/tui/screenshots/test_task_states.filter-submitted.html new file mode 100644 index 0000000000..7c00522a29 --- /dev/null +++ b/tests/integration/tui/screenshots/test_task_states.filter-submitted.html @@ -0,0 +1,31 @@ +
Cylc Tui   tasks filtered (F - edit, R - reset)   workflows filtered (W - edit, 
+E - reset)                                                                      
+                                                                                
+- ~cylc                                                                         
+   - test_task_states - paused 1■ 1■ 1■ 1■ 1■                                   
+      - ̿⊘ 2                                                                     
+         - ̿⊙ X                                                                  
+              ̿⊙ a                                                               
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
+filter tasks: T f s r R  filter workflows: W E p                                
+
\ No newline at end of file diff --git a/tests/integration/tui/screenshots/test_task_states.filter-waiting-or-expired.html b/tests/integration/tui/screenshots/test_task_states.filter-waiting-or-expired.html new file mode 100644 index 0000000000..62af8e44ba --- /dev/null +++ b/tests/integration/tui/screenshots/test_task_states.filter-waiting-or-expired.html @@ -0,0 +1,31 @@ +
Cylc Tui   tasks filtered (F - edit, R - reset)   workflows filtered (W - edit, 
+E - reset)                                                                      
+                                                                                
+- ~cylc                                                                         
+   - test_task_states - paused 1■ 1■ 1■ 1■ 1■                                   
+      - ̿⊗ 1                                                                     
+         - ̿⊗ Y                                                                  
+            - ̿◌ Y1                                                              
+                 ̿◌ c                                                            
+      - ̊○ 3                                                                     
+         - ̊○ X                                                                  
+              ̊○ a                                                               
+         - ̊○ Y                                                                  
+            - ̊○ Y1                                                              
+                 ̊○ c                                                            
+              ̊○ b                                                               
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+                                                                                
+quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
+filter tasks: T f s r R  filter workflows: W E p                                
+
\ No newline at end of file diff --git a/tests/integration/tui/screenshots/test_task_states.unfiltered.html b/tests/integration/tui/screenshots/test_task_states.unfiltered.html new file mode 100644 index 0000000000..9a9161d8ca --- /dev/null +++ b/tests/integration/tui/screenshots/test_task_states.unfiltered.html @@ -0,0 +1,31 @@ +
Cylc Tui   workflows filtered (W - edit, E - reset)                             
+                                                                                
+- ~cylc                                                                         
+   - test_task_states - paused 1■ 1■ 1■ 1■ 1■                                   
+      - ̿⊗ 1                                                                     
+         - ̿● X                                                                  
+              ̿● a                                                               
+         - ̿⊗ Y                                                                  
+            - ̿◌ Y1                                                              
+                 ̿◌ c                                                            
+              ̿⊗ b                                                               
+      - ̿⊘ 2                                                                     
+         - ̿⊙ X                                                                  
+              ̿⊙ a                                                               
+         - ̿⊘ Y                                                                  
+            - ̿⊘ Y1                                                              
+                 ̿⊘ c                                                            
+              ̿⊙ b                                                               
+      - ̊○ 3                                                                     
+         - ̊○ X                                                                  
+              ̊○ a                                                               
+         - ̊○ Y                                                                  
+            - ̊○ Y1                                                              
+                 ̊○ c                                                            
+              ̊○ b                                                               
+                                                                                
+                                                                                
+                                                                                
+quit: q  help: h  context: enter  tree: - ← + →  navigation: ↑ ↓ ↥ ↧ Home End   
+filter tasks: T f s r R  filter workflows: W E p                                
+
\ No newline at end of file diff --git a/tests/integration/tui/test_app.py b/tests/integration/tui/test_app.py index b9f50d7d02..bc38ef52fd 100644 --- a/tests/integration/tui/test_app.py +++ b/tests/integration/tui/test_app.py @@ -18,7 +18,16 @@ import pytest import urwid +from cylc.flow.cycling.integer import IntegerPoint from cylc.flow.task_outputs import TASK_OUTPUT_SUCCEEDED +from cylc.flow.task_state import ( + TASK_STATUS_EXPIRED, + TASK_STATUS_FAILED, + TASK_STATUS_RUNNING, + TASK_STATUS_SUBMITTED, + TASK_STATUS_SUBMIT_FAILED, + TASK_STATUS_SUCCEEDED, +) from cylc.flow.workflow_status import StopMode @@ -184,46 +193,83 @@ async def test_workflow_states(one_conf, flow, scheduler, start, rakiura): ) -# TODO: Task state filtering is currently broken -# see: https://github.com/cylc/cylc-flow/issues/5716 -# -# async def test_task_states(flow, scheduler, start, rakiura): -# id_ = flow({ -# 'scheduler': { -# 'allow implicit tasks': 'true', -# }, -# 'scheduling': { -# 'initial cycle point': '1', -# 'cycling mode': 'integer', -# 'runahead limit': 'P1', -# 'graph': { -# 'P1': ''' -# a => b => c -# b[-P1] => b -# ''' -# } -# } -# }, name='test_task_states') -# schd = scheduler(id_) -# async with start(schd): -# set_task_state( -# schd, -# [ -# (IntegerPoint('1'), 'a', TASK_STATUS_SUCCEEDED, False), -# # (IntegerPoint('1'), 'b', TASK_STATUS_FAILED, False), -# (IntegerPoint('1'), 'c', TASK_STATUS_RUNNING, False), -# # (IntegerPoint('2'), 'a', TASK_STATUS_RUNNING, False), -# (IntegerPoint('2'), 'b', TASK_STATUS_WAITING, True), -# ] -# ) -# await schd.update_data_structure() -# -# with rakiura(schd.tokens.id, size='80,20') as rk: -# rk.compare_screenshot('unfiltered') -# -# # filter out waiting tasks -# rk.user_input('T', 'down', 'enter', 'q') -# rk.compare_screenshot('filter-not-waiting') +async def test_task_states(flow, scheduler, start, rakiura): + id_ = flow({ + 'scheduler': { + 'allow implicit tasks': 'true', + }, + 'scheduling': { + 'initial cycle point': '1', + 'cycling mode': 'integer', + 'runahead limit': 'P1', + 'graph': { + 'P1': ''' + a & b & c + ''' + }, + }, + 'runtime': { + 'X': {}, + 'Y': {}, + 'Y1': {'inherit': 'Y'}, + 'a': {'inherit': 'X'}, + 'b': {'inherit': 'Y'}, + 'c': {'inherit': 'Y1'}, + }, + }, name='test_task_states') + schd = scheduler(id_) + async with start(schd): + set_task_state( + schd, + [ + (IntegerPoint('1'), 'a', TASK_STATUS_SUCCEEDED, False), + (IntegerPoint('1'), 'b', TASK_STATUS_FAILED, False), + (IntegerPoint('1'), 'c', TASK_STATUS_EXPIRED, False), + (IntegerPoint('2'), 'a', TASK_STATUS_SUBMITTED, False), + (IntegerPoint('2'), 'b', TASK_STATUS_RUNNING, True), + (IntegerPoint('2'), 'c', TASK_STATUS_SUBMIT_FAILED, True), + ] + ) + await schd.update_data_structure() + + with rakiura(schd.tokens.id, size='80,30') as rk: + rk.compare_screenshot( + 'unfiltered', + 'all tasks should be displayed' + ' (i.e. 1/*, 2/* and 3/* should be displayed)', + ) + + # filter OUT waiting tasks + rk.user_input('T', 'down', 'enter', 'q') # select waiting + rk.compare_screenshot( + 'filter-not-waiting', + 'waiting tasks should be filtered out' + ' (i.e. 1/* and 2/* should be displayed)', + ) + + # filter OUT waiting & expired tasks + rk.user_input('T', 'down', 'down', 'enter', 'q') # select expired + rk.compare_screenshot( + 'filter-not-waiting-or-expired', + 'waiting & expired tasks should be filtered out' + ' (i.e. only 1/a, 1/b and 2/* should be displayed)', + ) + + # filter FOR waiting & expired tasks + rk.user_input('T', 'enter', 'q') # select invert + rk.compare_screenshot( + 'filter-waiting-or-expired', + 'only waiting and expired tasks should be displayed' + ' (i.e. only 1/c and 3/* should be displayed)', + ) + + # filter FOR submitted tasks (using shortcuts) + rk.user_input('R', 's') # reset filters and apply submitted filter + rk.compare_screenshot( + 'filter-submitted', + 'only submitted tasks should be displayed' + ' (i.e. only 2/a should be displayed)', + ) async def test_navigation(flow, scheduler, start, rakiura):