Skip to content

Commit d65661b

Browse files
authored
CHANGE @W-17100565@ FlowTest no longer requires PipX (#131)
1 parent 13a61d6 commit d65661b

25 files changed

+767
-1198
lines changed

packages/code-analyzer-flowtest-engine/FlowTest/flow_parser/parse.py

Lines changed: 95 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
"""
44

55
from __future__ import annotations
6-
from lxml import etree as ET
6+
7+
import sys
8+
9+
sys.modules['_elementtree'] = None
10+
import xml.etree.ElementTree as ET
711

812
from typing import Optional
913
import logging
@@ -27,7 +31,7 @@
2731
logger: logging.Logger = logging.getLogger(__name__)
2832

2933

30-
def get_root(path: str) -> ET._Element:
34+
def get_root(path: str) -> ET.Element:
3135
"""Get flow root
3236
3337
Args:
@@ -37,7 +41,7 @@ def get_root(path: str) -> ET._Element:
3741
the root of the xml file
3842
3943
"""
40-
return ET.parse(path).getroot()
44+
return ET.parse(path, parser=parse_utils.LineNumberingParser()).getroot()
4145

4246

4347
class Parser(FlowParser):
@@ -59,7 +63,7 @@ class Parser(FlowParser):
5963

6064
def __init__(self, root):
6165
#: XMl root of a single flow
62-
self.root: ET._Element = root
66+
self.root: ET.Element = root
6367

6468
#: current filepath of flow
6569
self.flow_path: str | None = None
@@ -74,7 +78,7 @@ def __init__(self, root):
7478
self.flow_type: FlowType | None = None
7579

7680
#: frozen set of all elements that have a child of <name> and are thus flow globals
77-
self.all_named_elems: frozenset[ET._Element] | None = None
81+
self.all_named_elems: frozenset[ET.Element] | None = None
7882

7983
#: variables marked 'available for input', as a pair (flow_path, name)
8084
self.input_variables: frozenset[(str, str)] | None = None
@@ -100,7 +104,7 @@ def get_declared_run_mode(self) -> RunMode:
100104
def get_filename(self) -> str:
101105
return self.flow_path
102106

103-
def get_root(self) -> ET._Element:
107+
def get_root(self) -> ET.Element:
104108
return self.root
105109

106110
def get_literal_var(self) -> VariableType:
@@ -124,41 +128,47 @@ def get_flow_type(self) -> FlowType:
124128
# Process Builder
125129
# no <start> but <startElementReference>
126130
res = get_by_tag(self.root, 'startElementReference')
127-
if len(res) > 0:
128-
flow_type = FlowType.ProcessBuilder
129-
130-
else:
131+
if len(res) == 0:
131132
res = get_by_tag(self.root, 'start')
132-
assert len(res) != 0
133-
start = res[0]
133+
if len(res) == 0:
134+
# this is an old format record trigger flow
135+
self.flow_type = FlowType.RecordTrigger
136+
return FlowType.RecordTrigger
137+
start = res[0]
134138

135-
# Trigger, record
136-
# <start> has a child <triggerType>
137-
child = get_by_tag(start, 'triggerType')
138-
if len(child) > 0:
139-
flow_type = FlowType.RecordTrigger
139+
# Trigger, record
140+
# <start> has a child <triggerType>
141+
child = get_by_tag(start, 'triggerType')
142+
if len(child) > 0:
143+
flow_type = FlowType.RecordTrigger
140144

141-
elif len(get_by_tag(start, 'schedule')) > 0:
142-
flow_type = FlowType.Scheduled
145+
elif len(get_by_tag(start, 'schedule')) > 0:
146+
flow_type = FlowType.Scheduled
143147

144-
else:
145-
# We couldn't determine flow type by looking at
146-
# <start> elem, so now look at processType elem
147-
pt = get_by_tag(self.root, 'processType')
148-
if len(pt) > 0:
149-
pt = pt[0].text
150-
151-
# Screen
152-
# <processType>Flow and start does not have trigger or schedule
153-
if pt == 'Flow' or len(get_by_tag(self.root, 'screens')) > 0:
154-
flow_type = FlowType.Screen
155-
156-
# AutoLaunched
157-
# Some teams have their own names, e.g. FooAutolaunchedFlow
158-
# Notice this messes up capitalization from normal 'AutoLaunchedFlow'
159-
# there are also recommendation strategies, etc.
160-
else:
161-
flow_type = FlowType.AutoLaunched
148+
else:
149+
# We couldn't determine flow type by looking at
150+
# <start> elem, so now look at processType elem
151+
pt = get_by_tag(self.root, 'processType')
152+
if len(pt) > 0:
153+
pt = pt[0].text
154+
155+
# Screen
156+
# <processType>Flow and start does not have trigger or schedule
157+
if pt == 'Flow' or len(get_by_tag(self.root, 'screens')) > 0:
158+
flow_type = FlowType.Screen
159+
160+
elif pt.lower() == 'workflow':
161+
flow_type = FlowType.Workflow
162+
163+
elif pt.lower() == 'invocableprocess':
164+
flow_type = FlowType.InvocableProcess
165+
166+
# AutoLaunched
167+
# Some teams have their own names, e.g. FooAutolaunchedFlow
168+
# Notice this messes up capitalization from normal 'AutoLaunchedFlow'
169+
# there are also recommendation strategies, etc.
170+
else:
171+
flow_type = FlowType.AutoLaunched
162172

163173
if flow_type is not None:
164174
self.flow_type = flow_type
@@ -174,7 +184,8 @@ def resolve_by_name(self, name: str, path: str | None = None,
174184
175185
"Account_var.Name" --> ("Account_var", "Name", VariableType)
176186
"account_var" --> (account_var, None, VariableType).
177-
(my_subflow.account.Name) --> (my_subflow.account, Name, VarType)
187+
(my_subflow.account.Name) --> (my_subflow.account, Name, VariableType)
188+
(my_action_call.account) --> (my_action_call.account, None, VariableType)
178189
179190
Args:
180191
name: raw name as it is used in the flow xml file (e.g. foo.bar.baz)
@@ -206,6 +217,9 @@ def resolve_by_name(self, name: str, path: str | None = None,
206217
if spl_len == 1:
207218
logger.warning(f"RESOLUTION ERROR {name}")
208219
if strict is False:
220+
# 'strict' = False means that any unknown variable name
221+
# is assumed to be a string literal that is hardcoded into
222+
# the flows runtime and so not declared in flow xml file
209223
return name, None, self.literal_var
210224
else:
211225
return None
@@ -231,7 +245,7 @@ def resolve_by_name(self, name: str, path: str | None = None,
231245

232246
@classmethod
233247
def from_file(cls, filepath: str, old_parser: Parser = None) -> Parser:
234-
root = ET.parse(filepath).getroot()
248+
root = ET.parse(filepath, parser=parse_utils.LineNumberingParser()).getroot()
235249
parser = Parser(root)
236250
parser.flow_path = filepath
237251
parser.update(old_parser=old_parser)
@@ -300,10 +314,10 @@ def get_input_variables(self, path: str | None = None) -> {(str, str)}:
300314
path = self.flow_path
301315
return {(x, y) for (x, y) in self.input_variables if x == path}
302316

303-
def get_input_field_elems(self) -> set[ET._Element] | None:
317+
def get_input_field_elems(self) -> set[ET.Element] | None:
304318
return parse_utils.get_input_fields(self.root)
305319

306-
def get_input_output_elems(self) -> {str: set[ET._Element]}:
320+
def get_input_output_elems(self) -> {str: set[ET.Element]}:
307321
"""
308322
Returns::
309323
{"input": input variable elements,
@@ -325,7 +339,7 @@ def get_input_output_elems(self) -> {str: set[ET._Element]}:
325339
"output": output_accum
326340
}
327341

328-
def get_by_name(self, name_to_match: str, scope: ET._Element | None = None) -> ET._Element | None:
342+
def get_by_name(self, name_to_match: str, scope: ET.Element | None = None) -> ET.Element | None:
329343
"""returns the first elem with the given name that is a child of the scope element"""
330344
if name_to_match == '*':
331345
return self.get_start_elem()
@@ -356,9 +370,17 @@ def get_run_mode(self) -> RunMode:
356370
357371
"""
358372
flow_type = self.get_flow_type()
359-
if flow_type is not FlowType.Screen and flow_type is not FlowType.AutoLaunched:
373+
374+
if flow_type is FlowType.InvocableProcess:
375+
# always runs in user mode
376+
return RunMode.DefaultMode
377+
378+
if flow_type in [FlowType.Workflow, FlowType.RecordTrigger, FlowType.Scheduled, FlowType.ProcessBuilder]:
379+
# always runs in system mode
360380
return RunMode.SystemModeWithoutSharing
361381

382+
# for screen and other autolaunched, check if there is a declaration
383+
# otherwise go with default
362384
elems = get_by_tag(self.root, 'runInMode')
363385
if len(elems) == 0:
364386
return RunMode.DefaultMode
@@ -368,48 +390,48 @@ def get_run_mode(self) -> RunMode:
368390
def get_api_version(self) -> str:
369391
return get_by_tag(self.root, 'apiVersion')[0].text
370392

371-
def get_all_traversable_flow_elements(self) -> [ET._Element]:
393+
def get_all_traversable_flow_elements(self) -> [ET.Element]:
372394
""" ignore start"""
373395
return [child for child in self.root if
374396
get_tag(child) in ['actionCalls', 'assignments', 'decisions', 'loops',
375397
'recordLookups', 'recordUpdates',
376398
'collectionProcessors', 'recordDeletes', 'recordCreates', 'screens', 'subflows',
377399
'waits', 'recordRollbacks']]
378400

379-
def get_all_variable_elems(self) -> [ET._Element] or None:
401+
def get_all_variable_elems(self) -> [ET.Element] or None:
380402
elems = get_by_tag(self.root, 'variables')
381403
if len(elems) == 0:
382404
return None
383405
else:
384406
return elems
385407

386-
def get_templates(self) -> [ET._Element]:
408+
def get_templates(self) -> [ET.Element]:
387409
"""Grabs all template elements.
388410
Returns empty list if none found
389411
"""
390412
templates = get_by_tag(self.root, 'textTemplates')
391413
return templates
392414

393-
def get_formulas(self) -> [ET._Element]:
415+
def get_formulas(self) -> [ET.Element]:
394416
"""Grabs all formula elements.
395417
Returns empty list if none found
396418
"""
397419
formulas = get_by_tag(self.root, 'formulas')
398420
return formulas
399421

400-
def get_choices(self) -> [ET._Element]:
422+
def get_choices(self) -> [ET.Element]:
401423
choices = get_by_tag(self.root, 'choices')
402424
return choices
403425

404-
def get_dynamic_choice_sets(self) -> [ET._Element]:
426+
def get_dynamic_choice_sets(self) -> [ET.Element]:
405427
dcc = get_by_tag(self.root, 'dynamicChoiceSets')
406428
return dcc
407429

408-
def get_constants(self) -> [ET._Element]:
430+
def get_constants(self) -> [ET.Element]:
409431
constants = get_by_tag(self.root, 'constants')
410432
return constants
411433

412-
def get_start_elem(self) -> ET._Element:
434+
def get_start_elem(self) -> ET.Element:
413435
"""Get first element of flow
414436
415437
Returns:
@@ -424,10 +446,16 @@ def get_start_elem(self) -> ET._Element:
424446
elif len(res2) == 1:
425447
return self.get_by_name(res2[0].text)
426448

449+
# Put in provision for older flows that are missing start elements but have only
450+
# a single crud element
451+
candidates = get_by_tag(self.root, 'recordUpdates')
452+
if len(candidates) == 1:
453+
return candidates[0]
454+
427455
else:
428456
raise RuntimeError("Currently only flows with a single 'start' or 'startElementReference' can be scanned")
429457

430-
def get_all_indirect_tuples(self) -> list[tuple[str, ET._Element]]:
458+
def get_all_indirect_tuples(self) -> list[tuple[str, ET.Element]]:
431459
"""returns a list of tuples of all indirect references, e.g.
432460
str, elem, where str influences elem.
433461
The elem is a formula or template element and
@@ -483,13 +511,13 @@ def __get_type(self, name: str, path: str | None = None, strict: bool = False) -
483511

484512
else:
485513
logger.info(f"Auto-resolving {name} in file {self.flow_path}")
486-
if strict is False:
514+
if strict is True:
487515
return self.literal_var
488516
else:
489517
return None
490518

491519

492-
def build_vartype_from_elem(elem: ET._Element) -> VariableType | None:
520+
def build_vartype_from_elem(elem: ET.Element) -> VariableType | None:
493521
"""Build VariableType from XML Element
494522
495523
The purpose of this function is to assign types to named
@@ -538,6 +566,15 @@ def build_vartype_from_elem(elem: ET._Element) -> VariableType | None:
538566
object_name=parse_utils.get_obj_name(elem),
539567
is_optional=nulls_provided is not None and nulls_provided is False)
540568

569+
if tag == 'actionCalls':
570+
is_ = parse_utils.is_auto_store(elem)
571+
if is_ is True:
572+
reference = ReferenceType.ActionCallReference
573+
# TODO: see if we can get datatype info from return value
574+
575+
return VariableType(tag=tag, datatype=DataType.StringValue,
576+
reference=reference, is_optional=False)
577+
541578
if tag == 'recordCreates':
542579
# Todo: get collection parsing correct, look if record being created is itself
543580
# a collection element - do examples of bulkified versions of commands.
@@ -640,7 +677,7 @@ def build_vartype_from_elem(elem: ET._Element) -> VariableType | None:
640677
return None
641678

642679

643-
def _get_global_flow_data(flow_path, root: ET._Element) -> ([ET._Element], {str: VariableType}):
680+
def _get_global_flow_data(flow_path, root: ET.Element) -> ([ET.Element], {str: VariableType}):
644681
all_named = get_named_elems(root)
645682

646683
# all named cannot be None, each flow must have at least one named element.
@@ -655,15 +692,15 @@ def _get_global_flow_data(flow_path, root: ET._Element) -> ([ET._Element], {str:
655692
try:
656693
var = build_vartype_from_elem(x)
657694
except Exception:
658-
logger.error(f"ERROR parsing element {ET.tounicode(x)}")
695+
logger.error(f"ERROR parsing element {ET.tostring(x, encoding='unicode')}")
659696
continue
660697
if var is not None:
661698
vars_[(flow_path, name_dict[x])] = var
662699

663-
if var.is_input and var.is_input is True:
700+
if var.is_input is True:
664701
inputs.append((flow_path, name_dict[x]))
665702

666-
if var.is_output and var.is_output is True:
703+
if var.is_output is True:
667704
outputs.append((flow_path, name_dict[x]))
668705

669706
return all_named, vars_, frozenset(inputs), frozenset(outputs)

0 commit comments

Comments
 (0)