2525import newton .viewer
2626from newton .tests .unittest_utils import (
2727 USD_AVAILABLE ,
28- StdOutCapture ,
28+ CheckOutput ,
2929 add_function_test ,
3030 get_selected_cuda_test_devices ,
3131 get_test_devices ,
32- is_noise_line ,
3332)
3433
3534
@@ -69,6 +68,7 @@ def add_example_test(
6968 test_suffix : str | None = None ,
7069 expected_output : list [str ] | None = None ,
7170 expected_output_cpu : list [str ] | None = None ,
71+ allowed_output : list [str ] | None = None ,
7272):
7373 """Registers a Newton example to run on ``devices`` as a TestCase.
7474
@@ -79,6 +79,9 @@ def add_example_test(
7979 matched by at least one pattern fails the test.
8080 expected_output_cpu: Like *expected_output* but only asserted on
8181 CPU devices.
82+ allowed_output: Regex patterns for stdout lines that are allowed
83+ but not required. These prevent unexpected-output failures
84+ without requiring a match.
8285 """
8386
8487 # verify the module exists (use package-relative path so this works from any CWD)
@@ -152,40 +155,33 @@ def run(test, device):
152155 else :
153156 i += 1
154157
155- # Create viewer and run example in-process, capturing warnings + stdout
156- viewer = newton .viewer .ViewerNull (num_frames = args .num_frames )
157- factory = getattr (mod , "create_example" , None ) or mod .Example
158- capture = StdOutCapture ()
159- capture .begin ()
160- try :
161- with wp .ScopedDevice (device ), warnings .catch_warnings (record = True ) as caught :
162- warnings .simplefilter ("always" )
163- example = factory (viewer , args )
164- newton .examples .run (example , args )
165- wp .synchronize ()
166-
167- stdout = capture .end ()
168- except BaseException :
169- error_stdout = capture .end ()
170- if error_stdout .strip ():
171- print (f"Captured stdout before crash:\n { error_stdout .rstrip ()} " )
172- raise
173-
174- # Build expected pattern list
158+ # Build expected pattern list (used for both warning and stdout checks)
175159 is_cpu = wp .get_device (device ).is_cpu
176160 expected_patterns = list (expected_output or [])
177161 if is_cpu :
178162 expected_patterns .extend (expected_output_cpu or [])
179163
164+ # CheckOutput uses both expected and allowed patterns for filtering;
165+ # only expected_patterns are asserted to appear at least once.
166+ all_patterns = expected_patterns + list (allowed_output or [])
180167 compiled_patterns = [re .compile (p ) for p in expected_patterns ] if expected_patterns else []
181168
182- # Combine warnings and stdout for expected-pattern matching
169+ # Run example in-process.
170+ # CheckOutput captures stdout and fails on unexpected lines.
171+ # warnings.catch_warnings captures Python warnings for separate validation.
172+ viewer = newton .viewer .ViewerNull (num_frames = args .num_frames )
173+ factory = getattr (mod , "create_example" , None ) or mod .Example
174+ with CheckOutput (test , expected_patterns = all_patterns ) as check :
175+ with wp .ScopedDevice (device ), warnings .catch_warnings (record = True ) as caught :
176+ warnings .simplefilter ("always" )
177+ example = factory (viewer , args )
178+ newton .examples .run (example , args )
179+
180+ # Assert each expected pattern appears in warnings or stdout
183181 warning_messages = [str (w .message ) for w in caught ]
184182 all_text = "\n " .join (warning_messages )
185- if stdout .strip ():
186- all_text += "\n " + stdout
187-
188- # Assert each expected pattern appears at least once
183+ if check .output .strip ():
184+ all_text += "\n " + check .output
189185 for pattern in compiled_patterns :
190186 test .assertRegex (all_text , pattern , f"Expected output pattern not found: { pattern .pattern } " )
191187
@@ -198,16 +194,6 @@ def run(test, device):
198194 if not any (p .search (msg ) for p in compiled_patterns ):
199195 test .fail (f"Unexpected warning: { msg } " )
200196
201- # Fail on unexpected stdout (same strictness as warnings above)
202- if stdout .strip ():
203- filtered = [
204- line
205- for line in stdout .splitlines ()
206- if not is_noise_line (line ) and not any (p .search (line ) for p in compiled_patterns )
207- ]
208- if filtered :
209- test .fail ("Unexpected stdout:\n " + "\n " .join (filtered ))
210-
211197 test_name = f"test_{ name } _{ test_suffix } " if test_suffix else f"test_{ name } "
212198 add_function_test (cls , test_name , run , devices = devices , check_output = False )
213199
@@ -713,10 +699,8 @@ class TestMPMExamples(unittest.TestCase):
713699 name = "basic.example_basic_plotting" ,
714700 devices = test_devices ,
715701 test_options = {"num-frames" : 200 },
716- expected_output = [
717- "Diagnostics plot saved to|Simulation diagnostics summary" ,
718- r"^\s+(Iterations|Kinetic E|Potential E|Constraints):" ,
719- ],
702+ expected_output = ["Diagnostics plot saved to|Simulation diagnostics summary" ],
703+ allowed_output = [r"^\s+(Iterations|Kinetic E|Potential E|Constraints):" ],
720704)
721705
722706
0 commit comments