@@ -276,3 +276,139 @@ def recv_json_with_retry(self):
276276 # Read should return original data (simtime stripped)
277277 result = concore .read ("roundtrip_test" , "data" , "[]" )
278278 assert result == original_data
279+
280+
281+ class TestSimtimeNotMutatedByWrite :
282+ """Regression tests for issue #385:
283+ write() must NOT mutate global simtime. Simtime advancement happens
284+ only in read() via max(simtime, file_simtime). Mutating simtime in
285+ write() causes cascading timestamps in multi-output-port nodes and
286+ breaks cross-language determinism.
287+ """
288+
289+ @pytest .fixture (autouse = True )
290+ def reset_simtime (self ):
291+ import concore
292+ old_simtime = concore .simtime
293+ yield
294+ concore .simtime = old_simtime
295+
296+ @pytest .fixture (autouse = True )
297+ def reset_zmq_ports (self ):
298+ import concore
299+ original_ports = concore .zmq_ports .copy ()
300+ yield
301+ concore .zmq_ports .clear ()
302+ concore .zmq_ports .update (original_ports )
303+
304+ # ---- Test Case 1: single-output write keeps simtime unchanged ----
305+
306+ def test_single_file_write_does_not_mutate_simtime (self , temp_dir ):
307+ """A single file-based write with delta must not change simtime."""
308+ import concore
309+
310+ concore .simtime = 10
311+ out_dir = os .path .join (temp_dir , "out1" )
312+ os .makedirs (out_dir , exist_ok = True )
313+ concore .outpath = os .path .join (temp_dir , "out" )
314+
315+ concore .write (1 , "v" , [5.0 ], delta = 1 )
316+
317+ assert concore .simtime == 10 , (
318+ "simtime must not be mutated by write(); "
319+ "was %s instead of 10" % concore .simtime
320+ )
321+
322+ def test_single_zmq_write_does_not_mutate_simtime (self ):
323+ """A single ZMQ-based write with delta must not change simtime."""
324+ import concore
325+
326+ class DummyPort :
327+ def send_json_with_retry (self , msg ):
328+ self .sent = msg
329+
330+ dummy = DummyPort ()
331+ concore .zmq_ports ["zmq_test" ] = dummy
332+ concore .simtime = 10
333+
334+ concore .write ("zmq_test" , "v" , [5.0 ], delta = 1 )
335+
336+ assert concore .simtime == 10 , (
337+ "simtime must not be mutated by ZMQ write(); "
338+ "was %s instead of 10" % concore .simtime
339+ )
340+
341+
342+
343+ def test_multi_port_file_writes_share_same_timestamp (self , temp_dir ):
344+ """Two consecutive file writes with delta=1 must produce the
345+ same timestamp (simtime+delta), proving simtime is not incremented
346+ between calls."""
347+ import concore
348+
349+ concore .simtime = 10
350+ concore .outpath = os .path .join (temp_dir , "out" )
351+ for p in (1 , 2 ):
352+ os .makedirs (os .path .join (temp_dir , "out" + str (p )), exist_ok = True )
353+
354+ concore .write (1 , "u" , [1.0 ], delta = 1 )
355+ concore .write (2 , "v" , [2.0 ], delta = 1 )
356+
357+
358+ from ast import literal_eval
359+ payloads = []
360+ for p in (1 , 2 ):
361+ with open (os .path .join (temp_dir , "out" + str (p ),
362+ ("u" if p == 1 else "v" ))) as f :
363+ payloads .append (literal_eval (f .read ()))
364+
365+ ts1 , ts2 = payloads [0 ][0 ], payloads [1 ][0 ]
366+ assert ts1 == ts2 == 11 , (
367+ "Both ports must share timestamp simtime+delta=11; "
368+ "got %s and %s" % (ts1 , ts2 )
369+ )
370+
371+ def test_multi_port_zmq_writes_share_same_timestamp (self ):
372+ """Two consecutive ZMQ writes with delta=1 must produce the
373+ same timestamp."""
374+ import concore
375+
376+ class DummyPort :
377+ def __init__ (self ):
378+ self .sent = None
379+ def send_json_with_retry (self , msg ):
380+ self .sent = msg
381+
382+ d1 , d2 = DummyPort (), DummyPort ()
383+ concore .zmq_ports ["p1" ] = d1
384+ concore .zmq_ports ["p2" ] = d2
385+ concore .simtime = 10
386+
387+ concore .write ("p1" , "u" , [1.0 ], delta = 1 )
388+ concore .write ("p2" , "v" , [2.0 ], delta = 1 )
389+
390+ assert d1 .sent [0 ] == d2 .sent [0 ] == 11 , (
391+ "Both ZMQ ports must share timestamp 11; got %s and %s"
392+ % (d1 .sent [0 ], d2 .sent [0 ])
393+ )
394+
395+
396+
397+ def test_write_timestamp_matches_cpp_semantics (self , temp_dir ):
398+ """C++ uses `simtime+delta` as a local expression without mutation.
399+ After N writes with delta=1, simtime must still be the original
400+ value — matching C++ behaviour."""
401+ import concore
402+
403+ concore .simtime = 0
404+ concore .outpath = os .path .join (temp_dir , "out" )
405+ for p in range (1 , 4 ):
406+ os .makedirs (os .path .join (temp_dir , "out" + str (p )), exist_ok = True )
407+
408+ for p in range (1 , 4 ):
409+ concore .write (p , "x" , [float (p )], delta = 1 )
410+
411+ assert concore .simtime == 0 , (
412+ "After 3 writes with delta=1 simtime must remain 0 "
413+ "(matching C++/MATLAB/Verilog); got %s" % concore .simtime
414+ )
0 commit comments