1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//! Tests to verify examples handle EOF correctly without infinite loops.
//!
//! ## Bug Background (Issue #2)
//!
//! ### Root Cause
//! Interactive examples using `io::stdin().read_line()` were not properly handling
//! the EOF condition when running in non-interactive environments. When `read_line()`
//! returns `Ok(0)` (indicating EOF), the original code would print a message but
//! continue looping, causing infinite "You : " output.
//!
//! ### Why Not Caught
//! 1. Examples are typically tested manually in interactive terminals where EOF doesn't occur
//! 2. No automated tests existed for example executables
//! 3. CI environments dont run interactive examples by default
//!
//! ### Fix Applied
//! Modified all interactive examples to:
//! 1. Capture the return value from `read_line()` to check bytes read
//! 2. Explicitly check if `bytes_read == 0` (EOF condition)
//! 3. Print graceful exit message and **break** from the loop immediately
//!
//! ### Prevention
//! 1. Created automated tests that simulate EOF by providing empty stdin
//! 2. Tests verify examples exit gracefully without infinite loops
//! 3. Tests use timeouts to detect infinite loop conditions
//!
//! ### Pitfall
//! Interactive programs must always handle EOF explicitly. In Rust, `stdin().read_line()`
//! returns `Ok(0)` on EOF, not an error. Code that only checks for `Err` cases will
//! miss EOF and may loop infinitely. Always check the byte count returned : `if bytes_read == 0 { break; }`.
#[ cfg( feature = "enabled" ) ]
mod private
{
use std::process::{ Command, Stdio };
use core::time::Duration;
/// Test that `ollama_chat_assistant` handles EOF gracefully without infinite loops.
///
/// **Fix(issue-002)**: Added EOF detection to break from input loop immediately.
/// **Root cause**: `stdin().read_line()` returns `Ok(0)` on EOF but code continued looping.
/// **Pitfall**: Always check byte count from `read_line()`: `if bytes_read == 0 { break; }`.
#[ tokio::test ]
async fn test_chat_assistant_eof_handling()
{
let output = Command::new( "cargo" )
.args( [ "run", "--all-features", "--example", "ollama_chat_assistant" ] )
.stdin( Stdio::null() ) // Empty stdin to trigger EOF
.stdout( Stdio::piped() )
.stderr( Stdio::piped() )
.output()
.expect( "Failed to execute example" );
let stdout = String::from_utf8_lossy( &output.stdout );
// Verify example exits gracefully
assert!( stdout.contains( "EOF" ) || stdout.contains( "No input available" ),
"Example should detect EOF and exit gracefully. Got : {stdout}" );
// Verify no infinite "You : " loop (should appear max 2 times : initial + after empty read)
let you_count = stdout.matches( "You : " ).count();
assert!( you_count <= 3,
"Example appears to loop infinitely. 'You : ' appeared {you_count} times. Output : {stdout}" );
}
/// Test that `ollama_chat_interactive` handles EOF gracefully.
///
/// **Fix(issue-002)**: Added EOF detection with immediate break.
/// **Root cause**: Missing EOF check allowed infinite loop on empty stdin.
/// **Pitfall**: In non-interactive mode, `read_line()` returns EOF immediately.
#[ tokio::test ]
async fn test_chat_interactive_eof_handling()
{
let output = Command::new( "cargo" )
.args( [ "run", "--all-features", "--example", "ollama_chat_interactive" ] )
.stdin( Stdio::null() )
.stdout( Stdio::piped() )
.stderr( Stdio::piped() )
.output()
.expect( "Failed to execute example" );
let stdout = String::from_utf8_lossy( &output.stdout );
assert!( stdout.contains( "EOF" ) || stdout.contains( "No input available" ),
"Example should detect EOF. Got : {stdout}" );
let you_count = stdout.matches( "You : " ).count();
assert!( you_count <= 3,
"Example appears to loop infinitely. 'You : ' appeared {you_count} times" );
}
/// Test that `ollama_chat_cached_interactive` handles EOF gracefully.
///
/// **Fix(issue-002)**: Added EOF detection in interactive input loop.
/// **Root cause**: EOF handling missing from input processing logic.
/// **Pitfall**: Cached examples still need proper EOF handling despite caching layer.
#[ tokio::test ]
async fn test_cached_interactive_eof_handling()
{
let output = Command::new( "cargo" )
.args( [ "run", "--all-features", "--example", "ollama_chat_cached_interactive" ] )
.stdin( Stdio::null() )
.stdout( Stdio::piped() )
.stderr( Stdio::piped() )
.output()
.expect( "Failed to execute example" );
let stdout = String::from_utf8_lossy( &output.stdout );
assert!( stdout.contains( "EOF" ) || stdout.contains( "No input available" ),
"Example should detect EOF. Got : {stdout}" );
// This example uses "> " as prompt
let prompt_count = stdout.matches( "> " ).count();
assert!( prompt_count <= 3,
"Example appears to loop infinitely. Prompt appeared {prompt_count} times" );
}
/// Test that `ollama_chat_streaming` handles EOF gracefully.
///
/// **Fix(issue-002)**: Added EOF detection in streaming input loop.
/// **Root cause**: Streaming feature didn't prevent EOF loop issue.
/// **Pitfall**: Streaming examples need same EOF handling as non-streaming.
#[ tokio::test ]
async fn test_streaming_eof_handling()
{
let output = Command::new( "cargo" )
.args( [ "run", "--all-features", "--example", "ollama_chat_streaming" ] )
.stdin( Stdio::null() )
.stdout( Stdio::piped() )
.stderr( Stdio::piped() )
.output()
.expect( "Failed to execute example" );
let stdout = String::from_utf8_lossy( &output.stdout );
assert!( stdout.contains( "EOF" ) || stdout.contains( "No input available" ),
"Example should detect EOF. Got : {stdout}" );
let you_count = stdout.matches( "You : " ).count();
assert!( you_count <= 3,
"Example appears to loop infinitely. 'You : ' appeared {you_count} times" );
}
/// Test that `ollama_multimodal_vision` handles EOF gracefully.
///
/// **Fix(issue-002)**: Added EOF detection in vision example's interactive mode.
/// **Root cause**: Vision examples had same EOF handling gap as other interactive examples.
/// **Pitfall**: Multimodal examples with file I/O still need proper stdin EOF handling.
#[ tokio::test ]
async fn test_multimodal_vision_eof_handling()
{
let output = Command::new( "cargo" )
.args( [ "run", "--all-features", "--example", "ollama_multimodal_vision" ] )
.stdin( Stdio::null() )
.stdout( Stdio::piped() )
.stderr( Stdio::piped() )
.output()
.expect( "Failed to execute example" );
let stdout = String::from_utf8_lossy( &output.stdout );
// This example may show EOF in interactive section
// Verify it doesn't loop infinitely by checking prompt count
let prompt_count = stdout.matches( "Enter your question" ).count();
assert!( prompt_count <= 2,
"Example appears to loop infinitely. Prompt appeared {prompt_count} times" );
}
/// Test that examples complete quickly when given EOF (dont hang).
///
/// This test uses a timeout to verify that examples complete within a reasonable
/// time when given EOF, rather than hanging indefinitely due to infinite loops.
///
/// **Pitfall**: Tests need timeout protection to catch infinite loops in CI.
#[ tokio::test ]
async fn test_example_completes_quickly_on_eof()
{
let result = tokio::time::timeout(
Duration::from_secs( 30 ), // Allow time for compilation + execution
tokio ::task::spawn_blocking( ||
{
Command::new( "cargo" )
.args( [ "run", "--all-features", "--example", "ollama_chat_assistant" ] )
.stdin( Stdio::null() )
.output()
.expect( "Failed to execute" )
})
).await;
match result
{
Ok( Ok( output ) ) =>
{
// Success - example completed within timeout
let stdout = String::from_utf8_lossy( &output.stdout );
assert!( stdout.contains( "EOF" ) || stdout.contains( "No input available" ),
"Example should detect EOF gracefully" );
},
Ok( Err( e ) ) => panic!( "Example execution failed : {e}" ),
Err( e ) => panic!( "Example timed out after 30s - likely infinite loop bug not fixed : {e}" ),
}
}
}