@@ -4,12 +4,198 @@ defmodule TerminationIndicatorTest do
44 @ moduletag :diagram_generation
55 @ moduletag timeout: :infinity
66
7+ # credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
8+ # credo:disable-for-this-file Credo.Check.Refactor.Nesting
9+
710 # Use fixed seed for deterministic diagram generation
811 setup_all do
912 :rand . seed ( :exsss , { 300 , 301 , 302 } )
1013 :ok
1114 end
1215
16+ # Helper for simple fork behavior (reduces complexity)
17+ defp simple_fork_behavior ( fork_name ) do
18+ fn msg , state ->
19+ case msg do
20+ { :request_fork , phil } ->
21+ if state . held_by == nil do
22+ { :send , [ { phil , { :fork_granted , fork_name } } ] , % { state | held_by: phil } }
23+ else
24+ { :send , [ { phil , { :fork_denied , fork_name } } ] , state }
25+ end
26+
27+ { :release_fork , phil } ->
28+ if state . held_by == phil do
29+ { :ok , % { state | held_by: nil } }
30+ else
31+ { :ok , state }
32+ end
33+
34+ _ ->
35+ { :ok , state }
36+ end
37+ end
38+ end
39+
40+ # Helper to create finite dining philosophers that reach quiescence
41+ defp create_finite_philosophers ( num_philosophers , opts ) do
42+ max_meals = Keyword . get ( opts , :max_meals , 2 )
43+ think_time = Keyword . get ( opts , :think_time , 100 )
44+ eat_time = Keyword . get ( opts , :eat_time , 50 )
45+
46+ simulation = ActorSimulation . new ( trace: true )
47+
48+ # Create forks (same as regular dining philosophers)
49+ simulation =
50+ Enum . reduce ( 0 .. ( num_philosophers - 1 ) , simulation , fn i , sim ->
51+ fork_name = :"fork_#{ i } "
52+
53+ ActorSimulation . add_actor ( sim , fork_name ,
54+ on_receive: simple_fork_behavior ( fork_name ) ,
55+ initial_state: % { held_by: nil }
56+ )
57+ end )
58+
59+ # Create finite philosophers (no periodic pattern!)
60+ Enum . reduce ( 0 .. ( num_philosophers - 1 ) , simulation , fn i , sim ->
61+ philosopher_name = :"philosopher_#{ i } "
62+ first_fork = :"fork_#{ i } "
63+ second_fork = :"fork_#{ rem ( i + 1 , num_philosophers ) } "
64+
65+ # Asymmetric fork ordering to prevent deadlock
66+ { first_fork , second_fork } =
67+ if rem ( i , 2 ) == 0 do
68+ { first_fork , second_fork }
69+ else
70+ { second_fork , first_fork }
71+ end
72+
73+ # Finite philosopher behavior
74+ philosopher_behavior = fn msg , state ->
75+ meals_eaten = Map . get ( state , :meals_eaten , 0 )
76+
77+ case msg do
78+ :start_eating ->
79+ # Only get hungry if haven't reached max meals
80+ if meals_eaten < max_meals do
81+ { :send_after , think_time ,
82+ [ { philosopher_name , { :get_hungry , first_fork , second_fork } } ] , state }
83+ else
84+ # Finished eating quota - no more messages (enables quiescence!)
85+ { :ok , state }
86+ end
87+
88+ { :get_hungry , ^ first_fork , ^ second_fork } ->
89+ # Try to get first fork
90+ { :send , [ { first_fork , { :request_fork , philosopher_name } } ] , state }
91+
92+ { :fork_granted , fork } ->
93+ cond do
94+ fork == first_fork && ! Map . get ( state , :first_fork_held , false ) ->
95+ # Got first fork, try for second
96+ { :send , [ { second_fork , { :request_fork , philosopher_name } } ] ,
97+ Map . put ( state , :first_fork_held , true ) }
98+
99+ fork == second_fork && Map . get ( state , :first_fork_held , false ) ->
100+ # Got both forks! Eat and then release
101+ new_meals = meals_eaten + 1
102+
103+ { :send_after , eat_time ,
104+ [
105+ { philosopher_name , { :mumble , "I'm full! (meal #{ new_meals } /#{ max_meals } )" } } ,
106+ { first_fork , { :release_fork , philosopher_name } } ,
107+ { second_fork , { :release_fork , philosopher_name } } ,
108+ { philosopher_name , :start_eating }
109+ ] ,
110+ % {
111+ state
112+ | first_fork_held: false ,
113+ second_fork_held: false ,
114+ meals_eaten: new_meals
115+ } }
116+
117+ true ->
118+ { :ok , state }
119+ end
120+
121+ { :fork_denied , _fork } ->
122+ # Release any held forks and try again later
123+ releases =
124+ if Map . get ( state , :first_fork_held , false ) do
125+ [ { first_fork , { :release_fork , philosopher_name } } ]
126+ else
127+ [ ]
128+ end
129+
130+ { :send_after , think_time ,
131+ releases ++ [ { philosopher_name , { :get_hungry , first_fork , second_fork } } ] ,
132+ Map . put ( state , :first_fork_held , false ) }
133+
134+ { :mumble , _message } ->
135+ { :ok , state }
136+
137+ _ ->
138+ { :ok , state }
139+ end
140+ end
141+
142+ ActorSimulation . add_actor ( sim , philosopher_name ,
143+ # Start the first meal cycle with a single message (not periodic!)
144+ send_pattern: { :self_message , think_time , :start_eating } ,
145+ targets: [ philosopher_name ] ,
146+ on_receive: philosopher_behavior ,
147+ initial_state: % {
148+ name: philosopher_name ,
149+ first_fork: first_fork ,
150+ second_fork: second_fork ,
151+ meals_eaten: 0 ,
152+ first_fork_held: false ,
153+ second_fork_held: false
154+ }
155+ )
156+ end )
157+ end
158+
159+ # Helper to generate Mermaid HTML
160+ defp generate_mermaid_html ( mermaid , title , opts \\ [ ] ) do
161+ model_source = Keyword . get ( opts , :model_source , "" )
162+ description = Keyword . get ( opts , :description , "" )
163+
164+ """
165+ <!DOCTYPE html>
166+ <html>
167+ <head>
168+ <meta charset="utf-8">
169+ <title>#{ title } </title>
170+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
171+ </head>
172+ <body>
173+ <h1>#{ title } </h1>
174+ <p>#{ description } </p>
175+
176+ <h2>🎯 Key Features Demonstrated</h2>
177+ <ul>
178+ <li><strong>Virtual Time:</strong> Simulation runs much faster than real time</li>
179+ <li><strong>Quiescence Detection:</strong> System stops naturally when no more events are scheduled</li>
180+ <li><strong>Actor Coordination:</strong> Complex multi-actor interaction patterns</li>
181+ </ul>
182+
183+ <h2>📊 Sequence Diagram</h2>
184+ <div class="mermaid">
185+ #{ mermaid }
186+ </div>
187+
188+ <h2>💻 Source Code</h2>
189+ <pre><code>#{ model_source } </code></pre>
190+
191+ <script>
192+ mermaid.initialize({startOnLoad: true});
193+ </script>
194+ </body>
195+ </html>
196+ """
197+ end
198+
13199 describe "Termination indicators in diagrams" do
14200 @ describetag :serial
15201 test "Mermaid diagram shows termination note when condition met" do
@@ -191,5 +377,65 @@ defmodule TerminationIndicatorTest do
191377
192378 ActorSimulation . stop ( simulation )
193379 end
380+
381+ test "3 dining philosophers - quiescence demonstration" do
382+ # Create FINITE philosophers that stop after eating twice (demonstrate quiescence)
383+ simulation =
384+ create_finite_philosophers ( 3 , max_meals: 2 , think_time: 100 , eat_time: 50 )
385+ |> ActorSimulation . run (
386+ # Long max_duration but expecting quiescence much earlier
387+ max_duration: 30_000 ,
388+ # KEY: Use :quiescence to detect when system naturally stops
389+ terminate_when: :quiescence
390+ )
391+
392+ # Should terminate due to quiescence, not timeout
393+ assert simulation . termination_reason == :quiescence
394+ assert simulation . terminated_early == true
395+ assert simulation . actual_duration < 30_000
396+
397+ # Generate diagram showing quiescence termination
398+ mermaid =
399+ ActorSimulation . trace_to_mermaid ( simulation ,
400+ enhanced: true ,
401+ show_termination: true
402+ )
403+
404+ # Should show quiescence termination
405+ assert String . contains? ( mermaid , "⚡ Terminated" )
406+
407+ model_source = """
408+ # FINITE philosophers (each eats only 2 meals, then stops)
409+ simulation = create_finite_philosophers(3, max_meals: 2, think_time: 100, eat_time: 50)
410+ |> ActorSimulation.run(
411+ max_duration: 30_000,
412+ terminate_when: :quiescence # Wait for natural end!
413+ )
414+
415+ # Result: termination_reason == :quiescence
416+ # (system naturally stops when no more events are scheduled)
417+ """
418+
419+ html =
420+ generate_mermaid_html ( mermaid , "🍴 3 Dining Philosophers - Quiescence Detection" ,
421+ model_source: model_source ,
422+ description:
423+ "Demonstrates quiescence detection: simulation stops naturally when no more events are scheduled"
424+ )
425+
426+ File . write! ( "generated/examples/dining_philosophers_quiescence.html" , html )
427+
428+ IO . puts (
429+ "\n ✅ Generated quiescence demonstration: generated/examples/dining_philosophers_quiescence.html"
430+ )
431+
432+ IO . puts ( " 🎯 KEY POINT: Simulation terminated due to QUIESCENCE" )
433+ IO . puts ( " Virtual time: #{ simulation . actual_duration } ms (stopped naturally)" )
434+ IO . puts ( " Max time: 30,000ms (but didn't need it!)" )
435+ IO . puts ( " Termination: #{ simulation . termination_reason } " )
436+ IO . puts ( " This shows virtual time can detect when systems reach natural stable states!" )
437+
438+ ActorSimulation . stop ( simulation )
439+ end
194440 end
195441end
0 commit comments