1use tabnas_alchemy::shared::{Cell, Code, Fail, Flow, PublicColumn, TableEvent, TableSink};
18
19use crate::number::{check_number, write_value};
20use crate::text::TextOut;
21
22pub use tabnas_alchemy::shared::csv::{CsvOptions, MissingText, Newline, Quoting};
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26enum Phase {
27 BeforeSchema,
28 Rows,
29 Done,
30}
31
32pub struct CsvRenderer<O: TextOut> {
42 out: O,
43 options: CsvOptions,
44 delimiter: String,
45 phase: Phase,
46 labels: Vec<Box<str>>,
47 rows: u64,
48 emitted: bool,
49 scratch: String,
50}
51
52impl<O: TextOut> CsvRenderer<O> {
53 pub fn new(out: O, options: CsvOptions) -> Result<Self, Fail> {
56 if matches!(options.delimiter, '"' | '\r' | '\n' | '\0') {
57 return Err(Fail::new(
58 Code::TargetValueUnrepresentable,
59 format!(
60 "{:?} cannot be a CSV delimiter: it is the quote, a line break or NUL",
61 options.delimiter
62 ),
63 ));
64 }
65 Ok(CsvRenderer {
66 delimiter: options.delimiter.to_string(),
67 out,
68 options,
69 phase: Phase::BeforeSchema,
70 labels: Vec::new(),
71 rows: 0,
72 emitted: false,
73 scratch: String::new(),
74 })
75 }
76
77 pub fn options(&self) -> &CsvOptions {
78 &self.options
79 }
80
81 pub fn rows(&self) -> u64 {
83 self.rows
84 }
85
86 pub fn is_done(&self) -> bool {
88 self.phase == Phase::Done
89 }
90
91 pub fn into_inner(self) -> O {
92 self.out
93 }
94
95 fn fail(&self, f: Fail) -> Fail {
99 if self.emitted && self.out.has_committed() {
100 f.committed()
101 } else {
102 f
103 }
104 }
105
106 fn schema(&mut self, columns: &[PublicColumn]) -> Result<(), Fail> {
107 match self.phase {
108 Phase::BeforeSchema => {}
109 Phase::Rows => return Err(self.fail(Fail::protocol("a second schema"))),
110 Phase::Done => return Err(self.fail(Fail::protocol("a schema after the end"))),
111 }
112 if columns.is_empty() {
113 return Err(Fail::new(
114 Code::TargetValueUnrepresentable,
115 "a table with no columns has no CSV form",
116 ));
117 }
118 self.labels = columns.iter().map(|c| c.label.clone()).collect();
119 self.phase = Phase::Rows;
120 if self.options.header {
121 self.emitted = true;
122 for (i, label) in self.labels.iter().enumerate() {
123 if i > 0 {
124 self.out.write_str(&self.delimiter)?;
125 }
126 write_field(
127 &mut self.out,
128 self.options.quoting,
129 self.options.delimiter,
130 label,
131 )?;
132 }
133 self.out.write_str(self.options.newline.as_str())?;
134 }
135 Ok(())
136 }
137
138 fn row(&mut self, cells: &[Cell]) -> Result<(), Fail> {
139 match self.phase {
140 Phase::Rows => {}
141 Phase::BeforeSchema => return Err(Fail::protocol("a row before the schema")),
142 Phase::Done => return Err(self.fail(Fail::protocol("a row after the end"))),
143 }
144 if cells.len() != self.labels.len() {
145 return Err(self.fail(Fail::protocol(format!(
146 "row {} has {} cells; the schema has {} columns",
147 self.rows + 1,
148 cells.len(),
149 self.labels.len()
150 ))));
151 }
152 if let Err(f) = self.check(cells) {
153 return Err(self.fail(f));
154 }
155 for (i, cell) in cells.iter().enumerate() {
156 if i > 0 {
157 self.out.write_str(&self.delimiter)?;
158 }
159 let text: &str = match cell {
160 Cell::Null => &self.options.null_text,
161 Cell::Bool(true) => "true",
162 Cell::Bool(false) => "false",
163 Cell::Number {
166 lexeme: Some(l), ..
167 } => l,
168 Cell::Number { value, .. } => write_value(*value, &mut self.scratch),
169 Cell::String(s) => s,
170 Cell::Missing => match &self.options.missing {
171 MissingText::Text(t) => t,
172 MissingText::Error => continue,
174 },
175 };
176 self.emitted = true;
177 write_field(
178 &mut self.out,
179 self.options.quoting,
180 self.options.delimiter,
181 text,
182 )?;
183 }
184 self.emitted = true;
185 self.out.write_str(self.options.newline.as_str())?;
186 self.rows += 1;
187 Ok(())
188 }
189
190 fn check(&self, cells: &[Cell]) -> Result<(), Fail> {
195 for (i, cell) in cells.iter().enumerate() {
196 match cell {
197 Cell::Number { value, lexeme } => {
198 check_number(*value, lexeme.as_deref()).map_err(|f| {
199 f.at_path(format!(
200 "column {:?}, row {}",
201 self.labels[i],
202 self.rows + 1
203 ))
204 })?;
205 }
206 Cell::Missing if self.options.missing == MissingText::Error => {
207 return Err(Fail::new(
208 Code::MissingValue,
209 format!(
210 "row {} has no value for column {:?}",
211 self.rows + 1,
212 self.labels[i]
213 ),
214 ));
215 }
216 _ => {}
217 }
218 }
219 Ok(())
220 }
221
222 fn end(&mut self) -> Result<(), Fail> {
223 match self.phase {
224 Phase::Rows => {}
225 Phase::BeforeSchema => return Err(Fail::protocol("the end before the schema")),
226 Phase::Done => return Err(self.fail(Fail::protocol("a second end"))),
227 }
228 self.out.flush()?;
231 self.phase = Phase::Done;
232 Ok(())
233 }
234}
235
236impl<O: TextOut> TableSink for CsvRenderer<O> {
237 fn table_event(&mut self, ev: TableEvent<'_>) -> Result<Flow, Fail> {
238 match ev {
239 TableEvent::Schema(columns) => self.schema(columns)?,
240 TableEvent::Row(cells) => self.row(cells)?,
241 TableEvent::End => self.end()?,
242 }
243 Ok(Flow::Continue)
244 }
245}
246
247fn write_field<O: TextOut>(
250 out: &mut O,
251 quoting: Quoting,
252 delimiter: char,
253 text: &str,
254) -> Result<(), Fail> {
255 let quote = match quoting {
256 Quoting::Always => true,
257 Quoting::Minimal => text
258 .chars()
259 .any(|c| c == delimiter || matches!(c, '"' | '\r' | '\n')),
260 };
261 if !quote {
262 return out.write_str(text);
263 }
264 out.write_str("\"")?;
265 let mut rest = text;
266 while let Some(i) = rest.find('"') {
267 out.write_str(&rest[..=i])?;
269 out.write_str("\"")?;
270 rest = &rest[i + 1..];
271 }
272 if !rest.is_empty() {
273 out.write_str(rest)?;
274 }
275 out.write_str("\"")
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::text::{StringOut, WriteOut};
282
283 fn cols(labels: &[&str]) -> Vec<PublicColumn> {
284 labels.iter().map(|l| PublicColumn::new(*l)).collect()
285 }
286
287 fn s(text: &str) -> Cell {
288 Cell::String(text.into())
289 }
290
291 fn num(lexeme: &str) -> Cell {
292 Cell::Number {
293 value: lexeme.parse().unwrap_or(0.0),
294 lexeme: Some(lexeme.into()),
295 }
296 }
297
298 fn render(options: CsvOptions, labels: &[&str], rows: &[Vec<Cell>]) -> Result<String, Fail> {
300 let mut r = CsvRenderer::new(StringOut::new(), options)?;
301 let columns = cols(labels);
302 r.table_event(TableEvent::Schema(&columns))?;
303 for row in rows {
304 r.table_event(TableEvent::Row(row))?;
305 }
306 r.table_event(TableEvent::End)?;
307 Ok(r.into_inner().into_string())
308 }
309
310 fn standard(labels: &[&str], rows: &[Vec<Cell>]) -> String {
311 render(CsvOptions::default(), labels, rows).unwrap()
312 }
313
314 #[test]
315 fn every_field_is_quoted_and_every_record_ends_with_crlf() {
316 assert_eq!(
317 standard(&["name", "age"], &[vec![s("ada"), num("36")]]),
318 "\"name\",\"age\"\r\n\"ada\",\"36\"\r\n"
319 );
320 }
321
322 #[test]
323 fn empty_fields_are_an_empty_quote_pair() {
324 assert_eq!(
325 standard(&["a", "b"], &[vec![s(""), s("")]]),
326 "\"a\",\"b\"\r\n\"\",\"\"\r\n"
327 );
328 }
329
330 #[test]
331 fn commas_in_a_field_are_kept_inside_the_quotes() {
332 assert_eq!(
333 standard(&["a"], &[vec![s("x, y, z")]]),
334 "\"a\"\r\n\"x, y, z\"\r\n"
335 );
336 }
337
338 #[test]
339 fn quotes_in_a_field_are_doubled() {
340 assert_eq!(
341 standard(
342 &["a"],
343 &[vec![s("say \"hi\"")], vec![s("\"")], vec![s("\"\"")]]
344 ),
345 "\"a\"\r\n\"say \"\"hi\"\"\"\r\n\"\"\"\"\r\n\"\"\"\"\"\"\r\n"
346 );
347 }
348
349 #[test]
350 fn cr_and_lf_inside_a_field_are_written_as_they_are() {
351 assert_eq!(
352 standard(&["a"], &[vec![s("line1\r\nline2\nline3\rend")]]),
353 "\"a\"\r\n\"line1\r\nline2\nline3\rend\"\r\n"
354 );
355 }
356
357 #[test]
358 fn unicode_passes_through_unchanged() {
359 assert_eq!(
360 standard(&["名"], &[vec![s("héllo 日本語 🚀")]]),
361 "\"名\"\r\n\"héllo 日本語 🚀\"\r\n"
362 );
363 }
364
365 #[test]
366 fn booleans_write_true_and_false() {
367 assert_eq!(
368 standard(&["a", "b"], &[vec![Cell::Bool(false), Cell::Bool(true)]]),
369 "\"a\",\"b\"\r\n\"false\",\"true\"\r\n"
370 );
371 }
372
373 #[test]
374 fn zero_and_other_values_without_a_lexeme_take_the_shortest_form() {
375 let row = vec![
376 Cell::Number {
377 value: 0.0,
378 lexeme: None,
379 },
380 Cell::Number {
381 value: 50.25,
382 lexeme: None,
383 },
384 Cell::Number {
385 value: 1e20,
386 lexeme: None,
387 },
388 Cell::Number {
389 value: 1e21,
390 lexeme: None,
391 },
392 Cell::Number {
393 value: 1e-300,
394 lexeme: None,
395 },
396 ];
397 assert_eq!(
398 standard(&["z", "b", "big", "bigger", "tiny"], &[row]),
399 "\"z\",\"b\",\"big\",\"bigger\",\"tiny\"\r\n\"0\",\"50.25\",\"100000000000000000000\",\"1e21\",\"1e-300\"\r\n"
400 );
401 }
402
403 #[test]
404 fn big_lexemes_are_written_verbatim() {
405 let row = vec![
406 num("123456789012345678901234567890"),
407 num("0.1000000000000000055511151231257827"),
408 num("-1.5E+308"),
409 num("50.250"),
410 ];
411 assert_eq!(
412 standard(&["a", "b", "c", "d"], &[row]),
413 "\"a\",\"b\",\"c\",\"d\"\r\n\"123456789012345678901234567890\",\"0.1000000000000000055511151231257827\",\"-1.5E+308\",\"50.250\"\r\n"
414 );
415 }
416
417 #[test]
418 fn a_lexeme_that_is_not_a_json_number_is_invalid_number() {
419 for bad in ["1.", "01", "NaN", "0x10", "1_000", ""] {
420 let err = render(CsvOptions::default(), &["a"], &[vec![num(bad)]]).unwrap_err();
421 assert_eq!(err.code, Code::InvalidNumber, "{bad:?}");
422 assert!(err.committed_output, "the header was already written");
423 }
424 }
425
426 #[test]
427 fn nan_and_infinity_are_unrepresentable_with_or_without_a_lexeme() {
428 for v in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
429 let row = vec![Cell::Number {
430 value: v,
431 lexeme: None,
432 }];
433 let err = render(CsvOptions::default(), &["a"], &[row]).unwrap_err();
434 assert_eq!(err.code, Code::TargetValueUnrepresentable);
435 }
436 let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
439 let columns = cols(&["a", "b"]);
440 r.table_event(TableEvent::Schema(&columns)).unwrap();
441 let row = [
442 s("x"),
443 Cell::Number {
444 value: f64::INFINITY,
445 lexeme: Some("1e999".into()),
446 },
447 ];
448 let err = r.table_event(TableEvent::Row(&row)).unwrap_err();
449 assert_eq!(err.code, Code::TargetValueUnrepresentable);
450 assert_eq!(err.path.as_deref(), Some("column \"b\", row 1"));
451 assert_eq!(r.into_inner().as_str(), "\"a\",\"b\"\r\n");
452 }
453
454 #[test]
455 fn null_writes_the_null_text_empty_by_default() {
456 assert_eq!(
457 standard(&["a", "b"], &[vec![Cell::Null, s("x")]]),
458 "\"a\",\"b\"\r\n\"\",\"x\"\r\n"
459 );
460 let options = CsvOptions {
461 null_text: "NULL".into(),
462 ..CsvOptions::default()
463 };
464 assert_eq!(
465 render(options, &["a"], &[vec![Cell::Null]]).unwrap(),
466 "\"a\"\r\n\"NULL\"\r\n"
467 );
468 }
469
470 #[test]
471 fn missing_is_an_error_unless_a_text_is_configured() {
472 let err = render(
473 CsvOptions::default(),
474 &["a", "b"],
475 &[vec![s("x"), Cell::Missing]],
476 )
477 .unwrap_err();
478 assert_eq!(err.code, Code::MissingValue);
479 assert!(err.message.contains("\"b\""));
480 assert!(err.committed_output);
481 let options = CsvOptions {
482 missing: MissingText::Text("N/A".into()),
483 ..CsvOptions::default()
484 };
485 assert_eq!(
486 render(options, &["a", "b"], &[vec![Cell::Missing, s("x")]]).unwrap(),
487 "\"a\",\"b\"\r\n\"N/A\",\"x\"\r\n"
488 );
489 let options = CsvOptions {
490 missing: MissingText::Text("".into()),
491 ..CsvOptions::default()
492 };
493 assert_eq!(
494 render(options, &["a"], &[vec![Cell::Missing]]).unwrap(),
495 "\"a\"\r\n\"\"\r\n"
496 );
497 }
498
499 #[test]
500 fn a_row_that_fails_writes_nothing_so_records_stay_whole() {
501 let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
502 let columns = cols(&["a", "b"]);
503 r.table_event(TableEvent::Schema(&columns)).unwrap();
504 let failing = [
505 (vec![s("x"), Cell::Missing], Code::MissingValue),
506 (vec![s("x"), num("1.")], Code::InvalidNumber),
507 (
508 vec![
509 s("x"),
510 Cell::Number {
511 value: f64::NAN,
512 lexeme: None,
513 },
514 ],
515 Code::TargetValueUnrepresentable,
516 ),
517 ];
518 for (row, code) in &failing {
519 let err = r.table_event(TableEvent::Row(row)).unwrap_err();
520 assert_eq!(err.code, *code);
521 assert_eq!(r.out.as_str(), "\"a\",\"b\"\r\n", "{code}");
522 }
523 r.table_event(TableEvent::Row(&[s("y"), s("z")])).unwrap();
524 r.table_event(TableEvent::End).unwrap();
525 assert_eq!(r.rows(), 1);
526 assert_eq!(r.into_inner().as_str(), "\"a\",\"b\"\r\n\"y\",\"z\"\r\n");
527 }
528
529 #[test]
530 fn duplicate_labels_are_allowed() {
531 assert_eq!(
532 standard(&["a", "a"], &[vec![s("1"), s("2")]]),
533 "\"a\",\"a\"\r\n\"1\",\"2\"\r\n"
534 );
535 }
536
537 #[test]
538 fn an_empty_row_sequence_still_writes_the_header() {
539 assert_eq!(standard(&["a", "b"], &[]), "\"a\",\"b\"\r\n");
540 }
541
542 #[test]
543 fn the_header_can_be_turned_off() {
544 let options = CsvOptions {
545 header: false,
546 ..CsvOptions::default()
547 };
548 assert_eq!(
549 render(options.clone(), &["a"], &[vec![s("x")]]).unwrap(),
550 "\"x\"\r\n"
551 );
552 assert_eq!(render(options, &["a"], &[]).unwrap(), "");
553 }
554
555 #[test]
556 fn the_final_record_ends_with_the_newline_too() {
557 let out = standard(&["a"], &[vec![s("1")], vec![s("2")]]);
558 assert!(out.ends_with("\"2\"\r\n"));
559 assert_eq!(out.matches("\r\n").count(), 3);
560 }
561
562 #[test]
563 fn lf_is_a_dialect() {
564 let options = CsvOptions {
565 newline: Newline::Lf,
566 ..CsvOptions::default()
567 };
568 assert_eq!(
569 render(options, &["a"], &[vec![s("x")]]).unwrap(),
570 "\"a\"\n\"x\"\n"
571 );
572 }
573
574 #[test]
575 fn a_tab_delimiter_is_a_dialect() {
576 let options = CsvOptions {
577 delimiter: '\t',
578 ..CsvOptions::default()
579 };
580 assert_eq!(
581 render(options, &["a", "b"], &[vec![s("x,y"), s("z")]]).unwrap(),
582 "\"a\"\t\"b\"\r\n\"x,y\"\t\"z\"\r\n"
583 );
584 }
585
586 #[test]
587 fn minimal_quoting_quotes_only_what_needs_it() {
588 let options = CsvOptions {
589 quoting: Quoting::Minimal,
590 ..CsvOptions::default()
591 };
592 let row = vec![
593 s("plain"),
594 s(""),
595 s("a,b"),
596 s("say \"hi\""),
597 s("x\ny"),
598 s("x\ry"),
599 num("1.50"),
600 Cell::Null,
601 ];
602 assert_eq!(
603 render(
604 options,
605 &["p", "e", "c", "q", "lf", "cr", "n", "nul"],
606 &[row]
607 )
608 .unwrap(),
609 "p,e,c,q,lf,cr,n,nul\r\nplain,,\"a,b\",\"say \"\"hi\"\"\",\"x\ny\",\"x\ry\",1.50,\r\n"
610 );
611 }
612
613 #[test]
614 fn minimal_quoting_quotes_a_field_holding_the_dialects_delimiter() {
615 let options = CsvOptions {
616 quoting: Quoting::Minimal,
617 delimiter: ';',
618 ..CsvOptions::default()
619 };
620 assert_eq!(
621 render(options, &["a", "b"], &[vec![s("x;y"), s("x,y")]]).unwrap(),
622 "a;b\r\n\"x;y\";x,y\r\n"
623 );
624 }
625
626 #[test]
627 fn the_quote_a_line_break_and_nul_cannot_be_the_delimiter() {
628 for bad in ['"', '\r', '\n', '\0'] {
629 let options = CsvOptions {
630 delimiter: bad,
631 ..CsvOptions::default()
632 };
633 let err = CsvRenderer::new(StringOut::new(), options).err().unwrap();
634 assert_eq!(err.code, Code::TargetValueUnrepresentable, "{bad:?}");
635 }
636 for ok in [',', ';', '\t', '|', ' ', 'x', '→'] {
637 let options = CsvOptions {
638 delimiter: ok,
639 ..CsvOptions::default()
640 };
641 assert!(
642 CsvRenderer::new(StringOut::new(), options).is_ok(),
643 "{ok:?}"
644 );
645 }
646 }
647
648 #[test]
649 fn zero_columns_is_unrepresentable() {
650 let err = render(CsvOptions::default(), &[], &[]).unwrap_err();
651 assert_eq!(err.code, Code::TargetValueUnrepresentable);
652 assert!(!err.committed_output);
653 }
654
655 #[test]
656 fn a_row_before_the_schema_is_a_protocol_error() {
657 let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
658 let err = r.table_event(TableEvent::Row(&[s("x")])).unwrap_err();
659 assert_eq!(err.code, Code::ProtocolOrderError);
660 assert!(!err.committed_output);
661 }
662
663 #[test]
664 fn a_second_schema_is_a_protocol_error() {
665 let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
666 let columns = cols(&["a"]);
667 r.table_event(TableEvent::Schema(&columns)).unwrap();
668 let err = r.table_event(TableEvent::Schema(&columns)).unwrap_err();
669 assert_eq!(err.code, Code::ProtocolOrderError);
670 assert!(err.committed_output);
671 }
672
673 #[test]
674 fn a_row_of_the_wrong_width_is_a_protocol_error() {
675 for row in [vec![], vec![s("1")], vec![s("1"), s("2"), s("3")]] {
676 let err = render(
677 CsvOptions::default(),
678 &["a", "b"],
679 std::slice::from_ref(&row),
680 )
681 .unwrap_err();
682 assert_eq!(err.code, Code::ProtocolOrderError, "{row:?}");
683 assert!(err.message.contains("row 1 has"), "{}", err.message);
684 }
685 }
686
687 #[test]
688 fn an_end_before_the_schema_is_a_protocol_error() {
689 let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
690 let err = r.table_event(TableEvent::End).unwrap_err();
691 assert_eq!(err.code, Code::ProtocolOrderError);
692 }
693
694 #[test]
695 fn events_after_the_end_are_protocol_errors() {
696 let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
697 let columns = cols(&["a"]);
698 r.table_event(TableEvent::Schema(&columns)).unwrap();
699 r.table_event(TableEvent::End).unwrap();
700 assert!(r.is_done());
701 for ev in [
702 TableEvent::End,
703 TableEvent::Row(&[s("x")]),
704 TableEvent::Schema(&columns),
705 ] {
706 let err = r.table_event(ev).unwrap_err();
707 assert_eq!(err.code, Code::ProtocolOrderError);
708 assert!(err.committed_output);
709 }
710 }
711
712 struct NoFlush;
714
715 impl std::io::Write for NoFlush {
716 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
717 Ok(buf.len())
718 }
719
720 fn flush(&mut self) -> std::io::Result<()> {
721 Err(std::io::Error::other("pipe closed"))
722 }
723 }
724
725 #[test]
726 fn a_failed_flush_at_end_leaves_the_renderer_not_done() {
727 let mut r = CsvRenderer::new(WriteOut::new(NoFlush), CsvOptions::default()).unwrap();
728 let columns = cols(&["a"]);
729 r.table_event(TableEvent::Schema(&columns)).unwrap();
730 let err = r.table_event(TableEvent::End).unwrap_err();
731 assert_eq!(err.code, Code::OutputFailed);
732 assert!(!r.is_done());
733 }
734
735 #[test]
736 fn committed_output_means_bytes_that_reached_the_writer() {
737 let mut r = CsvRenderer::new(WriteOut::new(Vec::new()), CsvOptions::default()).unwrap();
740 let columns = cols(&["a"]);
741 r.table_event(TableEvent::Schema(&columns)).unwrap();
742 let err = r.table_event(TableEvent::Schema(&columns)).unwrap_err();
743 assert_eq!(err.code, Code::ProtocolOrderError);
744 assert!(!err.committed_output);
745 assert_eq!(r.out.committed(), 0);
746 assert!(r.into_inner().into_inner().is_empty());
747
748 let out = WriteOut::new(Vec::new()).with_budget(0);
750 let mut r = CsvRenderer::new(out, CsvOptions::default()).unwrap();
751 r.table_event(TableEvent::Schema(&columns)).unwrap();
752 let err = r.table_event(TableEvent::Schema(&columns)).unwrap_err();
753 assert!(err.committed_output);
754 assert_eq!(r.into_inner().into_inner(), b"\"a\"\r\n");
755 }
756
757 #[test]
758 fn end_flushes_the_output_and_nothing_else_does() {
759 let out = WriteOut::new(Vec::new());
760 let mut r = CsvRenderer::new(out, CsvOptions::default()).unwrap();
761 let columns = cols(&["a"]);
762 r.table_event(TableEvent::Schema(&columns)).unwrap();
763 r.table_event(TableEvent::Row(&[s("x")])).unwrap();
764 assert_eq!(r.out.committed(), 0);
765 assert_eq!(r.table_event(TableEvent::End).unwrap(), Flow::Continue);
766 assert_eq!(r.rows(), 1);
767 assert_eq!(r.out.committed(), 10);
768 let bytes = r.into_inner().into_inner();
769 assert_eq!(bytes, b"\"a\"\r\n\"x\"\r\n");
770 }
771}