actions_rs/summary.rs
1//! Fluent builder for the job summary (`GITHUB_STEP_SUMMARY`).
2//!
3//! The summary is GitHub-Flavored Markdown with embedded HTML.\
4//! Buffer construction is pure (testable via [`Summary::stringify`]);
5//! only [`Summary::write`] / [`Summary::write_overwrite`] touch the filesystem and can fail.
6//!
7//! Text node content is escaped by default.\
8//! Use [`SummaryText::html`] or [`Summary::raw`] when you intentionally want raw HTML parity with `@actions/core`.
9
10use std::fmt::Write as _;
11use std::fs::OpenOptions;
12use std::io::ErrorKind;
13use std::io::Write as _;
14
15use crate::error::{Error, Result};
16
17/// GitHub's documented per-step summary size limit (1 MiB).
18const MAX_BYTES: usize = 1024 * 1024;
19
20/// Escape text destined for HTML element content.
21/// Without this, content like `DEMO_FLAG<<delim` is parsed by the browser as a bogus tag and truncated.
22fn esc_text(s: &str) -> String {
23 s.replace('&', "&")
24 .replace('<', "<")
25 .replace('>', ">")
26}
27
28/// Escape text destined for a double-quoted HTML attribute value.
29fn esc_attr(s: &str) -> String {
30 esc_text(s).replace('"', """)
31}
32
33/// Text destined for a summary element.
34///
35/// # Examples
36///
37/// ```
38/// use actions_rs::{Summary, SummaryText};
39///
40/// let mut s = Summary::new();
41/// s.heading(SummaryText::escaped("a<b"), 2) // escaped
42/// .heading(SummaryText::html("<em>raw</em>"), 3); // verbatim
43/// assert_eq!(s.stringify(), "<h2>a<b</h2>\n<h3><em>raw</em></h3>\n");
44/// ```
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum SummaryText {
47 /// Escape HTML metacharacters before rendering.
48 Escaped(String),
49 /// Insert trusted HTML verbatim.
50 Html(String),
51}
52
53impl SummaryText {
54 /// Escape `text` before rendering it into an element body.
55 ///
56 /// # Examples
57 ///
58 /// ```
59 /// use actions_rs::{Summary, SummaryText};
60 /// let mut s = Summary::new();
61 /// s.heading(SummaryText::escaped("1 < 2 & ok"), 1);
62 /// assert_eq!(s.stringify(), "<h1>1 < 2 & ok</h1>\n");
63 /// ```
64 #[must_use]
65 pub fn escaped(text: impl Into<String>) -> Self {
66 Self::Escaped(text.into())
67 }
68
69 /// Insert trusted HTML verbatim into an element body.
70 ///
71 /// # Examples
72 ///
73 /// ```
74 /// use actions_rs::{Summary, SummaryText};
75 /// let mut s = Summary::new();
76 /// s.heading(SummaryText::html("<strong>done</strong>"), 2);
77 /// assert_eq!(s.stringify(), "<h2><strong>done</strong></h2>\n");
78 /// ```
79 #[must_use]
80 pub fn html(html: impl Into<String>) -> Self {
81 Self::Html(html.into())
82 }
83
84 fn into_html(self) -> String {
85 match self {
86 SummaryText::Escaped(text) => esc_text(&text),
87 SummaryText::Html(html) => html,
88 }
89 }
90}
91
92impl From<&str> for SummaryText {
93 fn from(text: &str) -> Self {
94 SummaryText::escaped(text)
95 }
96}
97
98impl From<&String> for SummaryText {
99 fn from(text: &String) -> Self {
100 SummaryText::escaped(text.clone())
101 }
102}
103
104impl From<String> for SummaryText {
105 fn from(text: String) -> Self {
106 SummaryText::escaped(text)
107 }
108}
109
110/// A table cell.
111/// Use [`Cell::header`] for `<th>`; `colspan`/`rowspan` map to the matching HTML attributes.
112/// Cell content is escaped unless you pass [`SummaryText::html`].
113///
114/// # Examples
115///
116/// ```
117/// use actions_rs::{Cell, Summary};
118/// let mut s = Summary::new();
119/// s.table([vec![Cell::header("Name"), Cell::new("value")]]);
120/// assert!(s.stringify().contains(r#"<th colspan="1" rowspan="1">Name</th>"#));
121/// ```
122#[derive(Debug, Clone)]
123pub struct Cell {
124 data: SummaryText,
125 header: bool,
126 colspan: u32,
127 rowspan: u32,
128}
129
130impl Cell {
131 /// A `<td>` cell.
132 ///
133 /// # Examples
134 ///
135 /// ```
136 /// use actions_rs::{Cell, Summary};
137 /// let mut s = Summary::new();
138 /// s.table([vec![Cell::new("x")]]);
139 /// assert_eq!(s.stringify(), "<table><tr><td colspan=\"1\" rowspan=\"1\">x</td></tr></table>\n");
140 /// ```
141 #[must_use]
142 pub fn new(data: impl Into<SummaryText>) -> Self {
143 Self {
144 data: data.into(),
145 header: false,
146 colspan: 1,
147 rowspan: 1,
148 }
149 }
150
151 /// A `<th>` header cell.
152 ///
153 /// # Examples
154 ///
155 /// ```
156 /// use actions_rs::{Cell, Summary};
157 /// let mut s = Summary::new();
158 /// s.table([vec![Cell::header("Col")]]);
159 /// assert_eq!(s.stringify(), "<table><tr><th colspan=\"1\" rowspan=\"1\">Col</th></tr></table>\n");
160 /// ```
161 #[must_use]
162 pub fn header(data: impl Into<SummaryText>) -> Self {
163 Self {
164 header: true,
165 ..Self::new(data)
166 }
167 }
168
169 /// Set the column span (clamped to ≥ 1; the HTML spec forbids 0).
170 ///
171 /// # Examples
172 ///
173 /// ```
174 /// use actions_rs::{Cell, Summary};
175 /// let mut s = Summary::new();
176 /// s.table([vec![Cell::new("wide").colspan(0)]]); // 0 clamps to 1
177 /// assert!(s.stringify().contains(r#"colspan="1""#));
178 /// ```
179 #[must_use]
180 pub fn colspan(mut self, n: u32) -> Self {
181 self.colspan = n.max(1);
182 self
183 }
184
185 /// Set the row span (clamped to ≥ 1; the HTML spec forbids 0).
186 ///
187 /// # Examples
188 ///
189 /// ```
190 /// use actions_rs::{Cell, Summary};
191 /// let mut s = Summary::new();
192 /// s.table([vec![Cell::new("tall").rowspan(3)]]);
193 /// assert!(s.stringify().contains(r#"rowspan="3""#));
194 /// ```
195 #[must_use]
196 pub fn rowspan(mut self, n: u32) -> Self {
197 self.rowspan = n.max(1);
198 self
199 }
200}
201
202impl From<&str> for Cell {
203 fn from(s: &str) -> Self {
204 Cell::new(s)
205 }
206}
207
208impl From<String> for Cell {
209 fn from(s: String) -> Self {
210 Cell::new(s)
211 }
212}
213
214impl From<SummaryText> for Cell {
215 fn from(text: SummaryText) -> Self {
216 Cell::new(text)
217 }
218}
219
220/// Accumulating job-summary builder.
221/// Chain the builder methods, then [`write`](Summary::write) (append) or [`write_overwrite`](Summary::write_overwrite).
222/// Building is pure and inspectable via [`stringify`](Summary::stringify);
223/// only the `write*` methods touch `GITHUB_STEP_SUMMARY`.
224///
225/// # Examples
226///
227/// ```
228/// use actions_rs::Summary;
229///
230/// let mut s = Summary::new();
231/// s.heading("Build", 2)
232/// .code_block("cargo test", Some("sh"));
233///
234/// assert_eq!(
235/// s.stringify(),
236/// "<h2>Build</h2>\n<pre lang=\"sh\"><code>cargo test</code></pre>\n"
237/// );
238///
239/// // In a real action you would then persist it:
240/// // s.write()?; // appends to $GITHUB_STEP_SUMMARY
241/// ```
242#[derive(Debug, Clone, Default)]
243pub struct Summary {
244 buf: String,
245}
246
247impl Summary {
248 /// Create an empty summary buffer.
249 ///
250 /// # Examples
251 ///
252 /// ```
253 /// let s = actions_rs::Summary::new();
254 /// assert!(s.is_empty());
255 /// ```
256 #[must_use]
257 pub fn new() -> Self {
258 Self::default()
259 }
260
261 /// Append raw text **without HTML escaping**.
262 /// When `eol` is true a newline is appended after it.
263 ///
264 /// # Safety
265 /// This is the one builder method that does not escape `& < > "`.
266 /// Passing untrusted input here is an HTML-injection vector.
267 /// Use it only for trusted or already-escaped markup;
268 /// for arbitrary text prefer [`Summary::code_block`] / [`Summary::heading`] etc., which escape.
269 ///
270 /// # Examples
271 ///
272 /// ```
273 /// let mut s = actions_rs::Summary::new();
274 /// s.raw("<div class=\"trusted\">ok</div>", true);
275 /// assert_eq!(s.stringify(), "<div class=\"trusted\">ok</div>\n");
276 /// ```
277 pub fn raw(&mut self, text: impl AsRef<str>, eol: bool) -> &mut Self {
278 self.buf.push_str(text.as_ref());
279 if eol {
280 self.buf.push('\n');
281 }
282 self
283 }
284
285 /// Append a newline.
286 ///
287 /// # Examples
288 ///
289 /// ```
290 /// let mut s = actions_rs::Summary::new();
291 /// s.raw("a", false).eol().raw("b", false);
292 /// assert_eq!(s.stringify(), "a\nb");
293 /// ```
294 pub fn eol(&mut self) -> &mut Self {
295 self.buf.push('\n');
296 self
297 }
298
299 /// Append an `<h1>`–`<h6>` heading (`level` clamped to `1..=6`).
300 /// Text is escaped unless you pass [`SummaryText::html`].
301 ///
302 /// # Examples
303 ///
304 /// ```
305 /// let mut s = actions_rs::Summary::new();
306 /// s.heading("Result", 9); // level clamps to 6
307 /// assert_eq!(s.stringify(), "<h6>Result</h6>\n");
308 /// ```
309 pub fn heading(&mut self, text: impl Into<SummaryText>, level: u8) -> &mut Self {
310 let l = level.clamp(1, 6);
311 let text = text.into().into_html();
312 let _ = writeln!(self.buf, "<h{l}>{text}</h{l}>");
313 self
314 }
315
316 /// Append a fenced `<pre><code>` block with an optional language hint.
317 ///
318 /// # Examples
319 ///
320 /// ```
321 /// let mut s = actions_rs::Summary::new();
322 /// s.code_block("cargo test", Some("sh"));
323 /// assert_eq!(s.stringify(), "<pre lang=\"sh\"><code>cargo test</code></pre>\n");
324 /// ```
325 pub fn code_block(&mut self, code: impl AsRef<str>, lang: Option<&str>) -> &mut Self {
326 let code = esc_text(code.as_ref());
327 match lang {
328 Some(l) => {
329 let _ = writeln!(
330 self.buf,
331 "<pre lang=\"{}\"><code>{code}</code></pre>",
332 esc_attr(l)
333 );
334 }
335 None => {
336 let _ = writeln!(self.buf, "<pre><code>{code}</code></pre>");
337 }
338 }
339 self
340 }
341
342 /// Append a `<ul>` (or `<ol>` when `ordered`) of `items`.
343 ///
344 /// # Examples
345 ///
346 /// ```
347 /// let mut s = actions_rs::Summary::new();
348 /// s.list(["a", "b"], false);
349 /// assert_eq!(s.stringify(), "<ul><li>a</li><li>b</li></ul>\n");
350 /// ```
351 pub fn list<I, S>(&mut self, items: I, ordered: bool) -> &mut Self
352 where
353 I: IntoIterator<Item = S>,
354 S: Into<SummaryText>,
355 {
356 let tag = if ordered { "ol" } else { "ul" };
357 self.buf.push('<');
358 self.buf.push_str(tag);
359 self.buf.push('>');
360 for item in items {
361 let _ = write!(self.buf, "<li>{}</li>", item.into().into_html());
362 }
363 let _ = writeln!(self.buf, "</{tag}>");
364 self
365 }
366
367 /// Append a `<table>`.
368 /// Each row is a list of [`Cell`]s.
369 ///
370 /// # Examples
371 ///
372 /// ```
373 /// use actions_rs::{Cell, Summary};
374 /// let mut s = Summary::new();
375 /// s.table([vec![Cell::header("k"), Cell::new("v")]]);
376 /// assert_eq!(
377 /// s.stringify(),
378 /// "<table><tr><th colspan=\"1\" rowspan=\"1\">k</th>\
379 /// <td colspan=\"1\" rowspan=\"1\">v</td></tr></table>\n"
380 /// );
381 /// ```
382 pub fn table(&mut self, rows: impl IntoIterator<Item = Vec<Cell>>) -> &mut Self {
383 self.buf.push_str("<table>");
384 for row in rows {
385 self.buf.push_str("<tr>");
386 for cell in row {
387 let tag = if cell.header { "th" } else { "td" };
388 let _ = write!(
389 self.buf,
390 "<{tag} colspan=\"{}\" rowspan=\"{}\">{}</{tag}>",
391 cell.colspan,
392 cell.rowspan,
393 cell.data.into_html()
394 );
395 }
396 self.buf.push_str("</tr>");
397 }
398 self.buf.push_str("</table>\n");
399 self
400 }
401
402 /// Append a `<details>` block with a `<summary>` label.
403 /// Both text nodes are escaped unless you pass [`SummaryText::html`].
404 ///
405 /// # Examples
406 ///
407 /// ```
408 /// let mut s = actions_rs::Summary::new();
409 /// s.details("logs", "all green");
410 /// assert_eq!(s.stringify(), "<details><summary>logs</summary>all green</details>\n");
411 /// ```
412 pub fn details(
413 &mut self,
414 label: impl Into<SummaryText>,
415 content: impl Into<SummaryText>,
416 ) -> &mut Self {
417 let label = label.into().into_html();
418 let content = content.into().into_html();
419 let _ = writeln!(
420 self.buf,
421 "<details><summary>{}</summary>{}</details>",
422 label, content
423 );
424 self
425 }
426
427 /// Append an `<img>`.
428 /// `size` is an optional `(width, height)` in pixels.
429 ///
430 /// # Examples
431 ///
432 /// ```
433 /// let mut s = actions_rs::Summary::new();
434 /// s.image("cov.svg", "coverage", Some((120, 20)));
435 /// assert_eq!(
436 /// s.stringify(),
437 /// "<img src=\"cov.svg\" alt=\"coverage\" width=\"120\" height=\"20\">\n"
438 /// );
439 /// ```
440 pub fn image(
441 &mut self,
442 src: impl AsRef<str>,
443 alt: impl AsRef<str>,
444 size: Option<(u32, u32)>,
445 ) -> &mut Self {
446 self.buf.push_str("<img src=\"");
447 self.buf.push_str(&esc_attr(src.as_ref()));
448 self.buf.push_str("\" alt=\"");
449 self.buf.push_str(&esc_attr(alt.as_ref()));
450 self.buf.push('"');
451 if let Some((w, h)) = size {
452 let _ = write!(self.buf, " width=\"{w}\" height=\"{h}\"");
453 }
454 self.buf.push_str(">\n");
455 self
456 }
457
458 /// Append an `<a>` link.
459 /// The link text is escaped unless you pass [`SummaryText::html`];
460 /// `href` is always attribute-escaped.
461 ///
462 /// # Examples
463 ///
464 /// ```
465 /// let mut s = actions_rs::Summary::new();
466 /// s.link("run", "https://example.com/run/1");
467 /// assert_eq!(s.stringify(), "<a href=\"https://example.com/run/1\">run</a>\n");
468 /// ```
469 pub fn link(&mut self, text: impl Into<SummaryText>, href: impl AsRef<str>) -> &mut Self {
470 let text = text.into().into_html();
471 let _ = writeln!(
472 self.buf,
473 "<a href=\"{}\">{}</a>",
474 esc_attr(href.as_ref()),
475 text
476 );
477 self
478 }
479
480 /// Append a `<blockquote>` with an optional `cite` URL.
481 /// Quote text is escaped unless you pass [`SummaryText::html`].
482 ///
483 /// # Examples
484 ///
485 /// ```
486 /// let mut s = actions_rs::Summary::new();
487 /// s.quote("ship it", None);
488 /// assert_eq!(s.stringify(), "<blockquote>ship it</blockquote>\n");
489 /// ```
490 pub fn quote(&mut self, text: impl Into<SummaryText>, cite: Option<&str>) -> &mut Self {
491 let text = text.into().into_html();
492 match cite {
493 Some(c) => {
494 let _ = writeln!(
495 self.buf,
496 "<blockquote cite=\"{}\">{}</blockquote>",
497 esc_attr(c),
498 text
499 );
500 }
501 None => {
502 let _ = writeln!(self.buf, "<blockquote>{text}</blockquote>");
503 }
504 }
505 self
506 }
507
508 /// Append an `<hr>`.
509 ///
510 /// # Examples
511 ///
512 /// ```
513 /// let mut s = actions_rs::Summary::new();
514 /// s.separator();
515 /// assert_eq!(s.stringify(), "<hr>\n");
516 /// ```
517 pub fn separator(&mut self) -> &mut Self {
518 self.buf.push_str("<hr>\n");
519 self
520 }
521
522 /// Append a `<br>`.
523 ///
524 /// # Examples
525 ///
526 /// ```
527 /// let mut s = actions_rs::Summary::new();
528 /// s.break_();
529 /// assert_eq!(s.stringify(), "<br>\n");
530 /// ```
531 pub fn break_(&mut self) -> &mut Self {
532 self.buf.push_str("<br>\n");
533 self
534 }
535
536 /// The buffered summary content.
537 ///
538 /// # Examples
539 ///
540 /// ```
541 /// let mut s = actions_rs::Summary::new();
542 /// s.heading("Hi", 1);
543 /// assert_eq!(s.stringify(), "<h1>Hi</h1>\n");
544 /// ```
545 #[must_use]
546 pub fn stringify(&self) -> &str {
547 &self.buf
548 }
549
550 /// Whether nothing has been buffered yet.
551 ///
552 /// # Examples
553 ///
554 /// ```
555 /// let mut s = actions_rs::Summary::new();
556 /// assert!(s.is_empty());
557 /// s.eol();
558 /// assert!(!s.is_empty());
559 /// ```
560 #[must_use]
561 pub fn is_empty(&self) -> bool {
562 self.buf.is_empty()
563 }
564
565 /// Clear the buffer (does not touch the file).
566 ///
567 /// # Examples
568 ///
569 /// ```
570 /// let mut s = actions_rs::Summary::new();
571 /// s.heading("x", 1);
572 /// s.clear();
573 /// assert!(s.is_empty());
574 /// ```
575 pub fn clear(&mut self) -> &mut Self {
576 self.buf.clear();
577 self
578 }
579
580 fn write_inner(&mut self, append: bool) -> Result<()> {
581 let write_bytes = self.buf.len() as u64;
582 if write_bytes > MAX_BYTES as u64 {
583 return Err(Error::SummaryTooLarge {
584 bytes: self.buf.len(),
585 });
586 }
587 let Some(path) = std::env::var_os("GITHUB_STEP_SUMMARY") else {
588 // Not in Actions / summaries disabled: nothing to write to. This
589 // is a normal local-run condition, not an error — but the buffer
590 // is *kept* (no write happened, so draining it would lose data
591 // silently). The caller can still `stringify()` or retry.
592 return Ok(());
593 };
594 let existing_bytes = if append {
595 match std::fs::metadata(&path) {
596 Ok(meta) => meta.len(),
597 Err(err) if err.kind() == ErrorKind::NotFound => 0,
598 Err(err) => return Err(err.into()),
599 }
600 } else {
601 0
602 };
603 let total_bytes = existing_bytes.saturating_add(write_bytes);
604 if total_bytes > MAX_BYTES as u64 {
605 return Err(Error::SummaryTooLarge {
606 bytes: usize::try_from(total_bytes).unwrap_or(usize::MAX),
607 });
608 }
609 let mut file = OpenOptions::new()
610 .create(true)
611 .append(append)
612 .write(true)
613 .truncate(!append)
614 .open(path)?;
615 file.write_all(self.buf.as_bytes())?;
616 self.clear();
617 Ok(())
618 }
619
620 /// Append the buffer to the job summary file.
621 ///
622 /// # Errors
623 /// [`Error::SummaryTooLarge`] if the buffer exceeds 1 MiB, or an I/O error.
624 ///
625 /// # Examples
626 ///
627 /// ```no_run
628 /// let mut s = actions_rs::Summary::new();
629 /// s.heading("Build passed", 2);
630 /// s.write()?; // appends to $GITHUB_STEP_SUMMARY
631 /// # Ok::<(), actions_rs::Error>(())
632 /// ```
633 pub fn write(&mut self) -> Result<()> {
634 self.write_inner(true)
635 }
636
637 /// Overwrite the job summary file with the buffer.
638 ///
639 /// # Errors
640 /// [`Error::SummaryTooLarge`] if the buffer exceeds 1 MiB, or an I/O error.
641 ///
642 /// # Examples
643 ///
644 /// ```no_run
645 /// let mut s = actions_rs::Summary::new();
646 /// s.heading("Replaces any prior summary", 2);
647 /// s.write_overwrite()?;
648 /// # Ok::<(), actions_rs::Error>(())
649 /// ```
650 pub fn write_overwrite(&mut self) -> Result<()> {
651 self.write_inner(false)
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658
659 #[test]
660 fn heading_clamps_level() {
661 let mut s = Summary::new();
662 s.heading("Top", 9);
663 assert_eq!(s.stringify(), "<h6>Top</h6>\n");
664 }
665
666 #[test]
667 fn html_metachars_are_escaped() {
668 let mut s = Summary::new();
669 // The exact bug: `DEMO_FLAG<<delim` was eaten by the HTML parser.
670 s.code_block("DEMO_FLAG<<d & a>b", None);
671 assert_eq!(
672 s.stringify(),
673 "<pre><code>DEMO_FLAG<<d & a>b</code></pre>\n"
674 );
675
676 let mut h = Summary::new();
677 h.heading("a < b & c", 2);
678 assert_eq!(h.stringify(), "<h2>a < b & c</h2>\n");
679
680 // Attribute values also escape the double quote.
681 let mut l = Summary::new();
682 l.link("x", "https://e.com/?a=1\"&b=2");
683 assert_eq!(
684 l.stringify(),
685 "<a href=\"https://e.com/?a=1"&b=2\">x</a>\n"
686 );
687
688 // raw() stays raw by contract.
689 let mut r = Summary::new();
690 r.raw("<b>kept</b>", false);
691 assert_eq!(r.stringify(), "<b>kept</b>");
692 }
693
694 #[test]
695 fn raw_html_is_opt_in() {
696 let mut s = Summary::new();
697 s.details(
698 SummaryText::html("<b>open</b>"),
699 SummaryText::html("<p>surprise</p>"),
700 );
701 assert_eq!(
702 s.stringify(),
703 "<details><summary><b>open</b></summary><p>surprise</p></details>\n"
704 );
705 }
706
707 #[test]
708 fn chaining_builds_expected_html() {
709 let mut s = Summary::new();
710 s.heading("Report", 2)
711 .list(["a", "b"], false)
712 .code_block("cargo test", Some("sh"))
713 .separator();
714 assert_eq!(
715 s.stringify(),
716 "<h2>Report</h2>\n<ul><li>a</li><li>b</li></ul>\n\
717 <pre lang=\"sh\"><code>cargo test</code></pre>\n<hr>\n"
718 );
719 }
720
721 #[test]
722 fn table_with_header_and_spans() {
723 let mut s = Summary::new();
724 s.table([
725 vec![Cell::header("H1"), Cell::header("H2")],
726 vec![Cell::new("a").colspan(2)],
727 ]);
728 assert_eq!(
729 s.stringify(),
730 "<table><tr><th colspan=\"1\" rowspan=\"1\">H1</th>\
731 <th colspan=\"1\" rowspan=\"1\">H2</th></tr>\
732 <tr><td colspan=\"2\" rowspan=\"1\">a</td></tr></table>\n"
733 );
734 }
735
736 #[test]
737 fn span_zero_is_clamped_to_one() {
738 let mut s = Summary::new();
739 s.table([vec![Cell::new("x").colspan(0).rowspan(0)]]);
740 assert_eq!(
741 s.stringify(),
742 "<table><tr><td colspan=\"1\" rowspan=\"1\">x</td></tr></table>\n"
743 );
744 }
745
746 #[test]
747 fn oversized_buffer_rejected() {
748 let mut s = Summary::new();
749 s.raw("x".repeat(MAX_BYTES + 1), false);
750 let e = s.write_overwrite().unwrap_err();
751 assert!(matches!(e, Error::SummaryTooLarge { bytes } if bytes == MAX_BYTES + 1));
752 }
753
754 #[test]
755 fn empty_and_clear() {
756 let mut s = Summary::new();
757 assert!(s.is_empty());
758 s.raw("hi", true);
759 assert!(!s.is_empty());
760 s.clear();
761 assert!(s.is_empty());
762 }
763}