1+ # frozen_string_literal: true
2+
3+ # Released under the MIT License.
4+ # Copyright, 2025, by Samuel Williams.
5+
6+ require "async/safe"
7+ require "sus/fixtures/console/captured_logger"
8+ require "console"
9+
10+ describe "Async::Safe Logging" do
11+ include_context Sus ::Fixtures ::Console ::CapturedLogger
12+
13+ let ( :test_class ) do
14+ Class . new do
15+ def process
16+ "processed"
17+ end
18+ end
19+ end
20+
21+ after do
22+ Async ::Safe . disable! if Async ::Safe . monitor
23+ end
24+
25+ with "no logger (default)" do
26+ it "raises exceptions by default" do
27+ Async ::Safe . enable!
28+
29+ test_object = test_class . new
30+ test_object . process # Establish ownership in main fiber
31+
32+ expect do
33+ Fiber . new do
34+ test_object . process # Should raise
35+ end . resume
36+ end . to raise_exception ( Async ::Safe ::ViolationError )
37+ end
38+
39+ it "raises exceptions when no logger provided" do
40+ Async ::Safe . enable! ( logger : nil )
41+
42+ test_object = test_class . new
43+ test_object . process # Establish ownership in main fiber
44+
45+ expect do
46+ Fiber . new do
47+ test_object . process # Should raise
48+ end . resume
49+ end . to raise_exception ( Async ::Safe ::ViolationError )
50+ end
51+ end
52+
53+ with "logger: Console" do
54+ it "logs violations instead of raising when logger provided" do
55+ Async ::Safe . enable! ( logger : Console )
56+
57+ test_object = test_class . new
58+ test_object . process # Establish ownership in main fiber
59+
60+ # This should not raise an exception
61+ expect do
62+ Fiber . new do
63+ test_object . process # Should log instead of raise
64+ end . resume
65+ end . not . to raise_exception
66+
67+ # Check that a warning was logged with structured data
68+ last_log = console_capture . last
69+ expect ( last_log ) . to have_keys (
70+ severity : be == :warn ,
71+ subject : be_a ( Async ::Safe ::Monitor ) , # The subject is the monitor instance
72+ message : be == "Async::Safe violation detected!" , # The actual message
73+ klass : be_a ( Class ) ,
74+ method : be == :process ,
75+ owner : be_a ( Fiber ) ,
76+ current : be_a ( Fiber ) ,
77+ backtrace : be_a ( Array )
78+ )
79+
80+ # Verify the fibers are different
81+ expect ( last_log [ :owner ] ) . not . to be == last_log [ :current ]
82+ end
83+
84+ it "continues execution after logging violations" do
85+ Async ::Safe . enable! ( logger : Console )
86+
87+ test_object = test_class . new
88+ test_object . process # Establish ownership in main fiber
89+
90+ execution_completed = false
91+
92+ Fiber . new do
93+ test_object . process # Should log violation but not raise
94+ execution_completed = true # This should execute
95+ end . resume
96+
97+ expect ( execution_completed ) . to be == true
98+
99+ # Verify that a warning was logged with all expected structured data
100+ expect ( console_capture . last ) . to have_keys (
101+ severity : be == :warn ,
102+ subject : be_a ( Async ::Safe ::Monitor ) , # The subject is the monitor instance
103+ message : be == "Async::Safe violation detected!" , # The actual message
104+ klass : be_a ( Class ) ,
105+ method : be == :process ,
106+ owner : be_a ( Fiber ) ,
107+ current : be_a ( Fiber ) ,
108+ backtrace : be_a ( Array )
109+ )
110+ end
111+
112+ it "includes useful backtrace information in logs" do
113+ Async ::Safe . enable! ( logger : Console )
114+
115+ test_object = test_class . new
116+ test_object . process # Establish ownership in main fiber
117+
118+ Fiber . new do
119+ test_object . process # Should log violation with backtrace
120+ end . resume
121+
122+ last_log = console_capture . last
123+ backtrace = last_log [ :backtrace ]
124+
125+ # Backtrace should be non-empty
126+ expect ( backtrace . length ) . to be > 0
127+
128+ # The backtrace entries should be Thread::Backtrace::Location objects
129+ first_entry = backtrace . first
130+ expect ( first_entry ) . to be_a ( Thread ::Backtrace ::Location )
131+
132+ # The backtrace should contain entries - check if any reference the test files
133+ backtrace_strings = backtrace . map ( &:to_s )
134+ has_test_reference = backtrace_strings . any? { |s | s . match? ( /test.*logging\. rb/ ) }
135+ expect ( has_test_reference ) . to be == true
136+ end
137+ end
138+ end
0 commit comments