Skip to main content

llama_cpp_log_decoder/
log_decoder.rs

1use crate::decode_anomaly::DecodeAnomaly;
2use crate::decode_output::DecodeOutput;
3use crate::decode_result::DecodeResult;
4use crate::incoming_log_level::IncomingLogLevel;
5use crate::log_level::LogLevel;
6use crate::log_line::LogLine;
7
8pub struct LogDecoder {
9    buffered: Option<(LogLevel, String)>,
10    previous_level: LogLevel,
11}
12
13impl LogDecoder {
14    #[must_use]
15    pub const fn new() -> Self {
16        Self {
17            buffered: None,
18            previous_level: LogLevel::None,
19        }
20    }
21
22    pub fn feed(&mut self, level: IncomingLogLevel, text: &str) -> DecodeResult {
23        match level {
24            IncomingLogLevel::Cont => self.feed_cont(text),
25            IncomingLogLevel::Debug => self.feed_non_cont(LogLevel::Debug, text),
26            IncomingLogLevel::Error => self.feed_non_cont(LogLevel::Error, text),
27            IncomingLogLevel::Info => self.feed_non_cont(LogLevel::Info, text),
28            IncomingLogLevel::None => self.feed_non_cont(LogLevel::None, text),
29            IncomingLogLevel::Unknown(raw) => self.feed_non_cont(LogLevel::Unknown(raw), text),
30            IncomingLogLevel::Warn => self.feed_non_cont(LogLevel::Warn, text),
31        }
32    }
33
34    fn feed_cont(&mut self, text: &str) -> DecodeResult {
35        if let Some((level, mut buffer)) = self.buffered.take() {
36            buffer.push_str(text);
37            if let Some(without_newline) = buffer.strip_suffix('\n') {
38                DecodeResult {
39                    output: DecodeOutput::Line(LogLine {
40                        level,
41                        text: without_newline.to_owned(),
42                    }),
43                    anomaly: None,
44                }
45            } else {
46                self.buffered = Some((level, buffer));
47                DecodeResult {
48                    output: DecodeOutput::None,
49                    anomaly: None,
50                }
51            }
52        } else {
53            self.feed_orphan_cont(text)
54        }
55    }
56
57    fn feed_orphan_cont(&mut self, text: &str) -> DecodeResult {
58        let level = self.previous_level;
59        if let Some(without_newline) = text.strip_suffix('\n') {
60            DecodeResult {
61                output: DecodeOutput::Line(LogLine {
62                    level,
63                    text: without_newline.to_owned(),
64                }),
65                anomaly: Some(DecodeAnomaly::OrphanCont),
66            }
67        } else {
68            self.buffered = Some((level, text.to_owned()));
69            DecodeResult {
70                output: DecodeOutput::None,
71                anomaly: Some(DecodeAnomaly::OrphanCont),
72            }
73        }
74    }
75
76    fn feed_non_cont(&mut self, level: LogLevel, text: &str) -> DecodeResult {
77        self.previous_level = level;
78        let stale = self.buffered.take();
79        match (text.strip_suffix('\n'), stale) {
80            (Some(without_newline), Some((stale_level, stale_text))) => DecodeResult {
81                output: DecodeOutput::TwoLines {
82                    earlier: LogLine {
83                        level: stale_level,
84                        text: stale_text,
85                    },
86                    current: LogLine {
87                        level,
88                        text: without_newline.to_owned(),
89                    },
90                },
91                anomaly: Some(DecodeAnomaly::StaleBufferAbandoned),
92            },
93            (Some(without_newline), None) => DecodeResult {
94                output: DecodeOutput::Line(LogLine {
95                    level,
96                    text: without_newline.to_owned(),
97                }),
98                anomaly: None,
99            },
100            (None, Some((stale_level, stale_text))) => {
101                self.buffered = Some((level, text.to_owned()));
102                DecodeResult {
103                    output: DecodeOutput::Line(LogLine {
104                        level: stale_level,
105                        text: stale_text,
106                    }),
107                    anomaly: Some(DecodeAnomaly::StaleBufferAbandoned),
108                }
109            }
110            (None, None) => {
111                self.buffered = Some((level, text.to_owned()));
112                DecodeResult {
113                    output: DecodeOutput::None,
114                    anomaly: None,
115                }
116            }
117        }
118    }
119}
120
121impl Default for LogDecoder {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::LogDecoder;
130    use crate::decode_anomaly::DecodeAnomaly;
131    use crate::decode_output::DecodeOutput;
132    use crate::decode_result::DecodeResult;
133    use crate::incoming_log_level::IncomingLogLevel;
134    use crate::log_level::LogLevel;
135    use crate::log_line::LogLine;
136
137    #[test]
138    fn feed_complete_info_line() {
139        let mut decoder = LogDecoder::new();
140        let result = decoder.feed(IncomingLogLevel::Info, "hello\n");
141
142        assert_eq!(
143            result,
144            DecodeResult {
145                output: DecodeOutput::Line(LogLine {
146                    level: LogLevel::Info,
147                    text: "hello".to_owned(),
148                }),
149                anomaly: None,
150            }
151        );
152    }
153
154    #[test]
155    fn feed_partial_without_newline() {
156        let mut decoder = LogDecoder::new();
157        let result = decoder.feed(IncomingLogLevel::Info, "hello");
158
159        assert_eq!(
160            result,
161            DecodeResult {
162                output: DecodeOutput::None,
163                anomaly: None,
164            }
165        );
166    }
167
168    #[test]
169    fn feed_cont_completion() {
170        let mut decoder = LogDecoder::new();
171        decoder.feed(IncomingLogLevel::Info, "hello ");
172        let result = decoder.feed(IncomingLogLevel::Cont, "world\n");
173
174        assert_eq!(
175            result,
176            DecodeResult {
177                output: DecodeOutput::Line(LogLine {
178                    level: LogLevel::Info,
179                    text: "hello world".to_owned(),
180                }),
181                anomaly: None,
182            }
183        );
184    }
185
186    #[test]
187    fn feed_multi_part_cont() {
188        let mut decoder = LogDecoder::new();
189        decoder.feed(IncomingLogLevel::Info, "part1 ");
190        decoder.feed(IncomingLogLevel::Cont, "part2 ");
191        let result = decoder.feed(IncomingLogLevel::Cont, "part3\n");
192
193        assert_eq!(
194            result,
195            DecodeResult {
196                output: DecodeOutput::Line(LogLine {
197                    level: LogLevel::Info,
198                    text: "part1 part2 part3".to_owned(),
199                }),
200                anomaly: None,
201            }
202        );
203    }
204
205    #[test]
206    fn feed_non_cont_while_buffering() {
207        let mut decoder = LogDecoder::new();
208        decoder.feed(IncomingLogLevel::Info, "stale");
209        let result = decoder.feed(IncomingLogLevel::Warn, "fresh\n");
210
211        assert_eq!(
212            result,
213            DecodeResult {
214                output: DecodeOutput::TwoLines {
215                    earlier: LogLine {
216                        level: LogLevel::Info,
217                        text: "stale".to_owned(),
218                    },
219                    current: LogLine {
220                        level: LogLevel::Warn,
221                        text: "fresh".to_owned(),
222                    },
223                },
224                anomaly: Some(DecodeAnomaly::StaleBufferAbandoned),
225            }
226        );
227    }
228
229    #[test]
230    fn feed_buffer_replacement() {
231        let mut decoder = LogDecoder::new();
232        decoder.feed(IncomingLogLevel::Info, "first");
233        let result = decoder.feed(IncomingLogLevel::Warn, "second");
234
235        assert_eq!(
236            result,
237            DecodeResult {
238                output: DecodeOutput::Line(LogLine {
239                    level: LogLevel::Info,
240                    text: "first".to_owned(),
241                }),
242                anomaly: Some(DecodeAnomaly::StaleBufferAbandoned),
243            }
244        );
245
246        let follow_up = decoder.feed(IncomingLogLevel::Cont, "more\n");
247        assert_eq!(
248            follow_up,
249            DecodeResult {
250                output: DecodeOutput::Line(LogLine {
251                    level: LogLevel::Warn,
252                    text: "secondmore".to_owned(),
253                }),
254                anomaly: None,
255            }
256        );
257    }
258
259    #[test]
260    fn feed_orphan_cont() {
261        let mut decoder = LogDecoder::new();
262        let result = decoder.feed(IncomingLogLevel::Cont, "ghost\n");
263
264        assert_eq!(
265            result,
266            DecodeResult {
267                output: DecodeOutput::Line(LogLine {
268                    level: LogLevel::None,
269                    text: "ghost".to_owned(),
270                }),
271                anomaly: Some(DecodeAnomaly::OrphanCont),
272            }
273        );
274    }
275
276    #[test]
277    fn feed_orphan_cont_previous_level() {
278        let mut decoder = LogDecoder::new();
279        decoder.feed(IncomingLogLevel::Warn, "complete\n");
280        let result = decoder.feed(IncomingLogLevel::Cont, "ghost\n");
281
282        assert_eq!(
283            result,
284            DecodeResult {
285                output: DecodeOutput::Line(LogLine {
286                    level: LogLevel::Warn,
287                    text: "ghost".to_owned(),
288                }),
289                anomaly: Some(DecodeAnomaly::OrphanCont),
290            }
291        );
292    }
293
294    #[test]
295    fn feed_none_level() {
296        let mut decoder = LogDecoder::new();
297        let result = decoder.feed(IncomingLogLevel::None, "no-level\n");
298
299        assert_eq!(
300            result,
301            DecodeResult {
302                output: DecodeOutput::Line(LogLine {
303                    level: LogLevel::None,
304                    text: "no-level".to_owned(),
305                }),
306                anomaly: None,
307            }
308        );
309    }
310
311    #[test]
312    fn feed_unknown_level() {
313        let mut decoder = LogDecoder::new();
314        let result = decoder.feed(IncomingLogLevel::Unknown(9999), "weird\n");
315
316        assert_eq!(
317            result,
318            DecodeResult {
319                output: DecodeOutput::Line(LogLine {
320                    level: LogLevel::Unknown(9999),
321                    text: "weird".to_owned(),
322                }),
323                anomaly: None,
324            }
325        );
326    }
327
328    #[test]
329    fn feed_debug_level() {
330        let mut decoder = LogDecoder::new();
331        let result = decoder.feed(IncomingLogLevel::Debug, "trace\n");
332
333        assert_eq!(
334            result,
335            DecodeResult {
336                output: DecodeOutput::Line(LogLine {
337                    level: LogLevel::Debug,
338                    text: "trace".to_owned(),
339                }),
340                anomaly: None,
341            }
342        );
343    }
344
345    #[test]
346    fn feed_error_level() {
347        let mut decoder = LogDecoder::new();
348        let result = decoder.feed(IncomingLogLevel::Error, "boom\n");
349
350        assert_eq!(
351            result,
352            DecodeResult {
353                output: DecodeOutput::Line(LogLine {
354                    level: LogLevel::Error,
355                    text: "boom".to_owned(),
356                }),
357                anomaly: None,
358            }
359        );
360    }
361
362    #[test]
363    fn feed_orphan_cont_without_newline_buffers_text() {
364        let mut decoder = LogDecoder::new();
365        let result = decoder.feed(IncomingLogLevel::Cont, "fragment");
366
367        assert_eq!(
368            result,
369            DecodeResult {
370                output: DecodeOutput::None,
371                anomaly: Some(DecodeAnomaly::OrphanCont),
372            }
373        );
374
375        let follow_up = decoder.feed(IncomingLogLevel::Cont, " rest\n");
376        assert_eq!(
377            follow_up,
378            DecodeResult {
379                output: DecodeOutput::Line(LogLine {
380                    level: LogLevel::None,
381                    text: "fragment rest".to_owned(),
382                }),
383                anomaly: None,
384            }
385        );
386    }
387
388    #[test]
389    fn default_construction() {
390        let mut default_decoder = LogDecoder::default();
391        let new_decoder_result = LogDecoder::new().feed(IncomingLogLevel::Info, "compare\n");
392        let default_result = default_decoder.feed(IncomingLogLevel::Info, "compare\n");
393
394        assert_eq!(default_result, new_decoder_result);
395    }
396}