@@ -276,3 +276,146 @@ 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_outpath (self ):
298+ import concore
299+ old_outpath = concore .outpath
300+ yield
301+ concore .outpath = old_outpath
302+
303+ @pytest .fixture (autouse = True )
304+ def reset_zmq_ports (self ):
305+ import concore
306+ original_ports = concore .zmq_ports .copy ()
307+ yield
308+ concore .zmq_ports .clear ()
309+ concore .zmq_ports .update (original_ports )
310+
311+ # ---- Test Case 1: single-output write keeps simtime unchanged ----
312+
313+ def test_single_file_write_does_not_mutate_simtime (self , temp_dir ):
314+ """A single file-based write with delta must not change simtime."""
315+ import concore
316+
317+ concore .simtime = 10
318+ out_dir = os .path .join (temp_dir , "out1" )
319+ os .makedirs (out_dir , exist_ok = True )
320+ concore .outpath = os .path .join (temp_dir , "out" )
321+
322+ concore .write (1 , "v" , [5.0 ], delta = 1 )
323+
324+ assert concore .simtime == 10 , (
325+ "simtime must not be mutated by write(); "
326+ "was %s instead of 10" % concore .simtime
327+ )
328+
329+ def test_single_zmq_write_does_not_mutate_simtime (self ):
330+ """A single ZMQ-based write with delta must not change simtime."""
331+ import concore
332+
333+ class DummyPort :
334+ def send_json_with_retry (self , msg ):
335+ self .sent = msg
336+
337+ dummy = DummyPort ()
338+ concore .zmq_ports ["zmq_test" ] = dummy
339+ concore .simtime = 10
340+
341+ concore .write ("zmq_test" , "v" , [5.0 ], delta = 1 )
342+
343+ assert concore .simtime == 10 , (
344+ "simtime must not be mutated by ZMQ write(); "
345+ "was %s instead of 10" % concore .simtime
346+ )
347+
348+ # ---- Test Case 2: multi-port write → identical timestamps ----
349+
350+ def test_multi_port_file_writes_share_same_timestamp (self , temp_dir ):
351+ """Two consecutive file writes with delta=1 must produce the
352+ same timestamp (simtime+delta), proving simtime is not incremented
353+ between calls."""
354+ import concore
355+
356+ concore .simtime = 10
357+ concore .outpath = os .path .join (temp_dir , "out" )
358+ for p in (1 , 2 ):
359+ os .makedirs (os .path .join (temp_dir , "out" + str (p )), exist_ok = True )
360+
361+ concore .write (1 , "u" , [1.0 ], delta = 1 )
362+ concore .write (2 , "v" , [2.0 ], delta = 1 )
363+
364+ # Read back the written files and compare timestamps
365+ from ast import literal_eval
366+ payloads = []
367+ for p in (1 , 2 ):
368+ with open (os .path .join (temp_dir , "out" + str (p ),
369+ ("u" if p == 1 else "v" ))) as f :
370+ payloads .append (literal_eval (f .read ()))
371+
372+ ts1 , ts2 = payloads [0 ][0 ], payloads [1 ][0 ]
373+ assert ts1 == ts2 == 11 , (
374+ "Both ports must share timestamp simtime+delta=11; "
375+ "got %s and %s" % (ts1 , ts2 )
376+ )
377+
378+ def test_multi_port_zmq_writes_share_same_timestamp (self ):
379+ """Two consecutive ZMQ writes with delta=1 must produce the
380+ same timestamp."""
381+ import concore
382+
383+ class DummyPort :
384+ def __init__ (self ):
385+ self .sent = None
386+ def send_json_with_retry (self , msg ):
387+ self .sent = msg
388+
389+ d1 , d2 = DummyPort (), DummyPort ()
390+ concore .zmq_ports ["p1" ] = d1
391+ concore .zmq_ports ["p2" ] = d2
392+ concore .simtime = 10
393+
394+ concore .write ("p1" , "u" , [1.0 ], delta = 1 )
395+ concore .write ("p2" , "v" , [2.0 ], delta = 1 )
396+
397+ assert d1 .sent [0 ] == d2 .sent [0 ] == 11 , (
398+ "Both ZMQ ports must share timestamp 11; got %s and %s"
399+ % (d1 .sent [0 ], d2 .sent [0 ])
400+ )
401+
402+ # ---- Test Case 3: cross-language parity check ----
403+
404+ def test_write_timestamp_matches_cpp_semantics (self , temp_dir ):
405+ """C++ uses `simtime+delta` as a local expression without mutation.
406+ After N writes with delta=1, simtime must still be the original
407+ value — matching C++ behaviour."""
408+ import concore
409+
410+ concore .simtime = 0
411+ concore .outpath = os .path .join (temp_dir , "out" )
412+ for p in range (1 , 4 ):
413+ os .makedirs (os .path .join (temp_dir , "out" + str (p )), exist_ok = True )
414+
415+ for p in range (1 , 4 ):
416+ concore .write (p , "x" , [float (p )], delta = 1 )
417+
418+ assert concore .simtime == 0 , (
419+ "After 3 writes with delta=1 simtime must remain 0 "
420+ "(matching C++/MATLAB/Verilog); got %s" % concore .simtime
421+ )
0 commit comments