11# Copyright (c) Meta Platforms, Inc. and affiliates.
22# All rights reserved.
3+ # Copyright 2026 Arm Limited and/or its affiliates.
34#
45# This source code is licensed under the BSD-style license found in the
56# LICENSE file in the root directory of this source tree.
1213# clone and install FACTO by running pip install . from the FACTO source
1314# directory. Then, from the executorch root directory, run the following:
1415#
15- # python -m unittest backends.test.operators.test_facto.FactoTestsXNNPACK
16+ # python -m unittest backends.test.facto.test_facto.FactoTestsXNNPACK
17+ #
18+ # Useful environment variables:
19+ # FACTO_OPS="abs.default,acos.default" limits generated tests to selected ops.
20+ # FACTO_MAX_CASES=10 limits the number of generated cases per op.
1621#
1722
1823import copy
24+ import fnmatch
1925import functools
26+ import os
2027import traceback
2128import unittest
2229from typing import Any , Callable , Sequence
2330
2431import torch
25- from executorch .backends .test .harness .tester import Tester as TesterBase
32+ from executorch .backends .test .harness .tester import Tester as BackendTester
2633from executorch .backends .xnnpack .test .tester .tester import Tester as XnnpackTester
27- from facto .inputgen .argtuple .gen import ArgumentTupleGenerator
28- from facto .inputgen .specs .model import ConstraintProducer as cp , Spec
29- from facto .inputgen .utils .random_manager import random_manager
30- from facto .specdb .db import SpecDictDB
34+
35+ try :
36+ from facto .inputgen .argtuple .gen import ArgumentTupleGenerator
37+ from facto .inputgen .specs .model import ConstraintProducer as cp , Spec
38+ from facto .inputgen .utils .random_manager import random_manager
39+ from facto .specdb .db import SpecDictDB
40+ except ImportError as exc :
41+ raise ImportError (
42+ "FACTO is required to run generated operator tests. Install it with "
43+ "`pip install -e backends/cadence/utils/FACTO`."
44+ ) from exc
3145from torch ._ops import OpOverload
3246
3347from .facto_specs import ExtraSpecDB
5569}
5670
5771
72+ TesterFactory = Callable [[torch .nn .Module , tuple [Any , ...]], BackendTester ]
73+
74+
75+ def _facto_max_cases () -> int | None :
76+ max_cases = os .environ .get ("FACTO_MAX_CASES" )
77+ if max_cases is None :
78+ return None
79+ return int (max_cases )
80+
81+
82+ def _selected_op_names () -> list [str ]:
83+ op_patterns = os .environ .get ("FACTO_OPS" )
84+ if op_patterns is None or not op_patterns .strip ():
85+ return list (CombinedSpecDB .keys ())
86+
87+ patterns = [pattern .strip () for pattern in op_patterns .split ("," )]
88+ patterns = [pattern for pattern in patterns if pattern ]
89+ return [
90+ op_name
91+ for op_name in CombinedSpecDB .keys ()
92+ if any (fnmatch .fnmatch (op_name , pattern ) for pattern in patterns )
93+ ]
94+
95+
5896def _patch_spec (spec : Spec ) -> Spec :
5997 spec = copy .deepcopy (spec )
6098 for inspec in spec .inspec :
@@ -139,12 +177,14 @@ def get_module_for_op(op: OpOverload):
139177
140178
141179class FactoTestsBase (unittest .TestCase ):
142- def __init__ (self , tester_factory : Callable [[], TesterBase ], * args , ** kwargs ):
180+ __test__ = False
181+
182+ def __init__ (self , tester_factory : TesterFactory , * args , ** kwargs ):
143183 super ().__init__ (* args , ** kwargs )
144184 self ._tester_factory = tester_factory
145185
146- @staticmethod
147- def _generate_test (op_name : str ) -> None :
186+ @classmethod
187+ def _generate_test (cls , op_name : str ) -> None :
148188 # Find the torch op with the given name.
149189 sections = op_name .split ("." )
150190 torch_op = functools .reduce (getattr , sections , torch .ops .aten )
@@ -154,7 +194,7 @@ def _generate_test(op_name: str) -> None:
154194 def test_body (self ):
155195 self ._test_op (torch_op )
156196
157- setattr (FactoTestsBase , test_name , test_body )
197+ setattr (cls , test_name , test_body )
158198
159199 @staticmethod
160200 def get_runtime_input_count (spec : Spec ):
@@ -180,6 +220,27 @@ def get_runtime_input_count(spec: Spec):
180220 def setUp (self ):
181221 torch .set_printoptions (threshold = 3 )
182222
223+ def _patch_spec_for_backend (self , spec : Spec , op_name : str ) -> Spec :
224+ return spec
225+
226+ def _should_skip_case (self , posargs : Sequence [Any ]) -> bool :
227+ return False
228+
229+ def _run_delegated_case (self , tester : BackendTester ) -> None :
230+ tester .to_executorch ().serialize ().run_method_and_compare_outputs ()
231+
232+ def _should_fail_on_failures (self ) -> bool :
233+ return False
234+
235+ def _count_as_test_failures (
236+ self ,
237+ * ,
238+ eager_fail_count : int ,
239+ export_fail_count : int ,
240+ fail_count : int ,
241+ ) -> int :
242+ return fail_count
243+
183244 def _test_op (self , op : OpOverload ) -> None : # noqa: C901
184245 random_manager .seed (0 )
185246
@@ -194,27 +255,29 @@ def _test_op(self, op: OpOverload) -> None: # noqa: C901
194255 if op_name not in CombinedSpecDB :
195256 raise ValueError (f"Operator { op_name } not found in SpecDictDB." )
196257 spec = _patch_spec (CombinedSpecDB [op_name ])
258+ spec = self ._patch_spec_for_backend (spec , op_name )
197259
198260 runtime_input_count = FactoTestsBase .get_runtime_input_count (spec )
199261
200262 print (f"Op: { op_name } , { runtime_input_count } runtime inputs" )
201263
202264 # Run test cases
265+ eager_fail_count = 0
266+ export_fail_count = 0
203267 success_count_delegated = 0
204268 success_count_undelegated = 0
205269 fail_count = 0
206270
207- i = 0
208- for posargs , inkwargs , _ in ArgumentTupleGenerator (spec ).gen ():
209- i += 1
271+ max_cases = _facto_max_cases ()
272+ for case_index , (posargs , inkwargs , _ ) in enumerate (
273+ ArgumentTupleGenerator (spec ).gen ()
274+ ):
275+ if max_cases is not None and case_index >= max_cases :
276+ break
210277
211278 try :
212- if isinstance (posargs [0 ], torch .Tensor ):
213- # Temporary for getting around XNN crashes (https://github.com/pytorch/executorch/issues/10960).
214- # TODO Re-enable when resolved.
215- if posargs [0 ].dtype in {torch .int8 , torch .uint8 }:
216- print ("Skipping (u)int8 case." )
217- continue
279+ if self ._should_skip_case (posargs ):
280+ continue
218281
219282 module_cls = get_module_for_op (op )
220283 model = module_cls (
@@ -226,6 +289,7 @@ def _test_op(self, op: OpOverload) -> None: # noqa: C901
226289 try :
227290 model (* posargs [:runtime_input_count ])
228291 except Exception as e :
292+ eager_fail_count += 1
229293 print (f"Eager execution failed: { e } " )
230294 continue
231295
@@ -237,6 +301,7 @@ def _test_op(self, op: OpOverload) -> None: # noqa: C901
237301 try :
238302 tester .export ()
239303 except Exception :
304+ export_fail_count += 1
240305 print ("Export failed." )
241306 continue
242307
@@ -250,11 +315,7 @@ def _test_op(self, op: OpOverload) -> None: # noqa: C901
250315
251316 # Only run the runtime test if the op was delegated.
252317 if is_delegated :
253- (
254- tester .to_executorch ()
255- .serialize ()
256- .run_method_and_compare_outputs ()
257- )
318+ self ._run_delegated_case (tester )
258319
259320 if is_delegated :
260321 success_count_delegated += 1
@@ -277,25 +338,54 @@ def _test_op(self, op: OpOverload) -> None: # noqa: C901
277338 print (
278339 f" { success_count_delegated } DELEGATED, { success_count_undelegated } UNDELEGATED"
279340 )
341+ print (f" { eager_fail_count } EAGER_FAILED, { export_fail_count } EXPORT_FAILED" )
280342
281-
282- # Programatically generate tests for each operator.
283- for op_name in CombinedSpecDB .keys ():
284- FactoTestsBase ._generate_test (op_name )
343+ counted_failures = self ._count_as_test_failures (
344+ eager_fail_count = eager_fail_count ,
345+ export_fail_count = export_fail_count ,
346+ fail_count = fail_count ,
347+ )
348+ if counted_failures and self ._should_fail_on_failures ():
349+ self .fail (
350+ f"{ op_name } : { counted_failures } FACTO-generated case(s) failed "
351+ f"(runtime={ fail_count } , export={ export_fail_count } , "
352+ f"eager={ eager_fail_count } )."
353+ )
285354
286355
287356# TODO Figure out where to put these
288357class FactoTestsXNNPACK (FactoTestsBase ):
358+ __test__ = True
359+
289360 def __init__ (self , * args , ** kwargs ):
290361 super ().__init__ (XnnpackTester , * args , ** kwargs )
291362
363+ def _should_skip_case (self , posargs : Sequence [Any ]) -> bool :
364+ if isinstance (posargs [0 ], torch .Tensor ):
365+ # Temporary for getting around XNN crashes
366+ # (https://github.com/pytorch/executorch/issues/10960).
367+ # TODO Re-enable when resolved.
368+ if posargs [0 ].dtype in {torch .int8 , torch .uint8 }:
369+ print ("Skipping (u)int8 case." )
370+ return True
371+ return False
372+
373+
374+ for op_name in _selected_op_names ():
375+ FactoTestsXNNPACK ._generate_test (op_name )
376+
292377
293378try :
294379 from executorch .backends .apple .coreml .test .tester import CoreMLTester
295380
296381 class FactoTestsCoreML (FactoTestsBase ):
382+ __test__ = True
383+
297384 def __init__ (self , * args , ** kwargs ):
298385 super ().__init__ (CoreMLTester , * args , ** kwargs )
299386
387+ for op_name in _selected_op_names ():
388+ FactoTestsCoreML ._generate_test (op_name )
389+
300390except :
301391 print ("Skipping Core ML facto tests as Core ML AOT is not available." )
0 commit comments