• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kdash-rs / kdash / 3750513782

pending completion
3750513782

push

github

Deepu
fix tests

982 of 4441 branches covered (22.11%)

Branch coverage included in aggregate %.

2731 of 4724 relevant lines covered (57.81%)

8.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

39.0
/src/app/models.rs
1
use std::collections::VecDeque;
2

3
use serde::Serialize;
4
use tui::{
5
  backend::Backend,
6
  layout::Rect,
7
  style::{Modifier, Style},
8
  text::Span,
9
  widgets::{Block, List, ListItem, ListState, TableState},
10
  Frame,
11
};
12

13
use super::Route;
14

15
pub trait KubeResource<T: Serialize> {
16
  fn get_k8s_obj(&self) -> &T;
17

18
  /// generate YAML from the original kubernetes resource
19
  fn resource_to_yaml(&self) -> String {
2✔
20
    match serde_yaml::to_string(&self.get_k8s_obj()) {
2!
21
      Ok(yaml) => yaml,
2✔
22
      Err(_) => "".into(),
×
23
    }
24
  }
2!
25
}
26

27
pub trait Scrollable {
28
  fn handle_scroll(&mut self, up: bool, page: bool) {
9✔
29
    // support page up/down
30
    let inc_or_dec = if page { 10 } else { 1 };
9!
31
    if up {
9!
32
      self.scroll_up(inc_or_dec);
4✔
33
    } else {
34
      self.scroll_down(inc_or_dec);
5✔
35
    }
36
  }
9✔
37
  fn scroll_down(&mut self, inc_or_dec: usize);
38
  fn scroll_up(&mut self, inc_or_dec: usize);
39
}
40

41
pub struct StatefulList<T> {
42
  pub state: ListState,
43
  pub items: Vec<T>,
44
}
45

46
impl<T> StatefulList<T> {
47
  pub fn with_items(items: Vec<T>) -> StatefulList<T> {
14✔
48
    let mut state = ListState::default();
14✔
49
    if !items.is_empty() {
14!
50
      state.select(Some(0));
14✔
51
    }
52
    StatefulList { state, items }
14✔
53
  }
14✔
54
}
55

56
impl<T> Scrollable for StatefulList<T> {
57
  // for lists we cycle back to the beginning when we reach the end
58
  fn scroll_down(&mut self, increment: usize) {
×
59
    let i = match self.state.selected() {
×
60
      Some(i) => {
×
61
        if i >= self.items.len().saturating_sub(increment) {
×
62
          0
×
63
        } else {
64
          i + increment
×
65
        }
66
      }
67
      None => 0,
×
68
    };
69
    self.state.select(Some(i));
×
70
  }
×
71
  // for lists we cycle back to the end when we reach the beginning
72
  fn scroll_up(&mut self, decrement: usize) {
×
73
    let i = match self.state.selected() {
×
74
      Some(i) => {
×
75
        if i == 0 {
×
76
          self.items.len().saturating_sub(decrement)
×
77
        } else {
78
          i.saturating_sub(decrement)
×
79
        }
80
      }
81
      None => 0,
×
82
    };
83
    self.state.select(Some(i));
×
84
  }
×
85
}
86

87
#[derive(Clone)]
88
pub struct StatefulTable<T> {
89
  pub state: TableState,
90
  pub items: Vec<T>,
91
}
92

93
impl<T> StatefulTable<T> {
94
  pub fn new() -> StatefulTable<T> {
312✔
95
    StatefulTable {
312✔
96
      state: TableState::default(),
312✔
97
      items: Vec::new(),
312✔
98
    }
99
  }
312✔
100

101
  pub fn with_items(items: Vec<T>) -> StatefulTable<T> {
15✔
102
    let mut table = StatefulTable::new();
15✔
103
    if !items.is_empty() {
15!
104
      table.state.select(Some(0));
15✔
105
    }
106
    table.set_items(items);
15✔
107
    table
108
  }
15✔
109

110
  pub fn set_items(&mut self, items: Vec<T>) {
23✔
111
    let item_len = items.len();
23✔
112
    self.items = items;
23✔
113
    if !self.items.is_empty() {
23!
114
      let i = self.state.selected().map_or(0, |i| {
40✔
115
        if i > 0 && i < item_len {
17!
116
          i
1✔
117
        } else if i >= item_len {
16!
118
          item_len - 1
1✔
119
        } else {
120
          0
15✔
121
        }
122
      });
17✔
123
      self.state.select(Some(i));
23✔
124
    }
125
  }
23✔
126
}
127

128
impl<T> Scrollable for StatefulTable<T> {
129
  fn scroll_down(&mut self, increment: usize) {
8✔
130
    if let Some(i) = self.state.selected() {
8!
131
      if (i + increment) < self.items.len() {
8!
132
        self.state.select(Some(i + increment));
4✔
133
      } else {
134
        self.state.select(Some(self.items.len().saturating_sub(1)));
4✔
135
      }
136
    }
137
  }
8✔
138

139
  fn scroll_up(&mut self, decrement: usize) {
5✔
140
    if let Some(i) = self.state.selected() {
5!
141
      if i != 0 {
5!
142
        self.state.select(Some(i.saturating_sub(decrement)));
4✔
143
      }
144
    }
145
  }
5✔
146
}
147

148
impl<T: Clone> StatefulTable<T> {
149
  /// a clone of the currently selected item.
150
  /// for mutable ref use state.selected() and fetch from items when needed
151
  pub fn get_selected_item_copy(&self) -> Option<T> {
1✔
152
    if !self.items.is_empty() {
1!
153
      self.state.selected().map(|i| self.items[i].clone())
2✔
154
    } else {
155
      None
×
156
    }
157
  }
1✔
158
}
159

160
#[derive(Clone)]
×
161
pub struct TabRoute {
162
  pub title: String,
×
163
  pub route: Route,
×
164
}
165

166
pub struct TabsState {
167
  pub items: Vec<TabRoute>,
168
  pub index: usize,
169
}
170

171
impl TabsState {
172
  pub fn new(items: Vec<TabRoute>) -> TabsState {
29✔
173
    TabsState { items, index: 0 }
29✔
174
  }
29✔
175
  pub fn set_index(&mut self, index: usize) -> &TabRoute {
4✔
176
    self.index = index;
4✔
177
    &self.items[self.index]
4✔
178
  }
4✔
179
  pub fn get_active_route(&self) -> &Route {
5✔
180
    &self.items[self.index].route
5✔
181
  }
5✔
182

183
  pub fn next(&mut self) {
2✔
184
    self.index = (self.index + 1) % self.items.len();
2!
185
  }
2✔
186
  pub fn previous(&mut self) {
2✔
187
    if self.index > 0 {
2✔
188
      self.index -= 1;
1✔
189
    } else {
190
      self.index = self.items.len() - 1;
1✔
191
    }
192
  }
2✔
193
}
194

195
#[derive(Debug, Eq, PartialEq)]
×
196
pub struct ScrollableTxt {
197
  items: Vec<String>,
×
198
  pub offset: u16,
×
199
}
200

201
impl ScrollableTxt {
202
  pub fn new() -> ScrollableTxt {
15✔
203
    ScrollableTxt {
15✔
204
      items: vec![],
15✔
205
      offset: 0,
206
    }
207
  }
15✔
208

209
  pub fn with_string(item: String) -> ScrollableTxt {
4✔
210
    let items: Vec<&str> = item.split('\n').collect();
4✔
211
    let items: Vec<String> = items.iter().map(|it| it.to_string()).collect();
30✔
212
    ScrollableTxt { items, offset: 0 }
4✔
213
  }
4✔
214

215
  pub fn get_txt(&self) -> String {
5✔
216
    self.items.join("\n")
5✔
217
  }
5✔
218
}
219

220
impl Scrollable for ScrollableTxt {
221
  fn scroll_down(&mut self, increment: usize) {
5✔
222
    // scroll only if offset is less than total lines in text
223
    // we subtract increment + 2 to keep the text in view. Its just an arbitrary number that works
224
    if self.offset < self.items.len().saturating_sub(increment + 2) as u16 {
5✔
225
      self.offset += increment as u16;
3✔
226
    }
227
  }
5✔
228
  fn scroll_up(&mut self, decrement: usize) {
3✔
229
    // scroll up and avoid going negative
230
    if self.offset > 0 {
3✔
231
      self.offset = self.offset.saturating_sub(decrement as u16);
2✔
232
    }
233
  }
3✔
234
}
235

236
// TODO implement line buffer to avoid gathering too much data in memory
237
#[derive(Debug, Clone)]
×
238
pub struct LogsState {
239
  /// Stores the log messages to be displayed
240
  ///
241
  /// (original_message, (wrapped_message, wrapped_at_width))
242
  #[allow(clippy::type_complexity)]
243
  records: VecDeque<(String, Option<(Vec<ListItem<'static>>, u16)>)>,
×
244
  wrapped_length: usize,
×
245
  pub state: ListState,
×
246
  pub id: String,
×
247
}
248

249
impl LogsState {
250
  pub fn new(id: String) -> LogsState {
15✔
251
    LogsState {
15✔
252
      records: VecDeque::with_capacity(512),
15✔
253
      state: ListState::default(),
15✔
254
      wrapped_length: 0,
255
      id,
15✔
256
    }
257
  }
15✔
258

259
  /// get a plain text version of the logs
260
  pub fn get_plain_text(&self) -> String {
1✔
261
    self.records.iter().fold(String::new(), |mut acc, v| {
3✔
262
      acc.push('\n');
2✔
263
      acc.push_str(v.0.as_str());
2✔
264
      acc
2✔
265
    })
2✔
266
  }
1✔
267

268
  /// Render the current state as a list widget
269
  pub fn render_list<B: Backend>(
5✔
270
    &mut self,
271
    f: &mut Frame<'_, B>,
272
    logs_area: Rect,
273
    block: Block<'_>,
274
    style: Style,
275
    follow: bool,
276
  ) {
277
    let available_lines = logs_area.height as usize;
5✔
278
    let logs_area_width = logs_area.width as usize;
5✔
279

280
    let num_records = self.records.len();
5✔
281
    // Keep track of the number of lines after wrapping so we can skip lines as
282
    // needed below
283
    let mut wrapped_lines_len = 0;
5✔
284

285
    let mut items = Vec::with_capacity(logs_area.height as usize);
5✔
286

287
    let lines_to_skip = if follow {
5!
288
      self.unselect();
2✔
289
      num_records.saturating_sub(available_lines)
2✔
290
    } else {
291
      0
3✔
292
    };
293

294
    items.extend(
5✔
295
      self
15✔
296
        .records
297
        .iter_mut()
298
        // Only wrap the records we could potentially be displaying
299
        .skip(lines_to_skip)
5✔
300
        .flat_map(|r| {
46✔
301
          // See if we can use a cached wrapped line
302
          if let Some(wrapped) = &r.1 {
41!
303
            if wrapped.1 as usize == logs_area_width {
31!
304
              wrapped_lines_len += wrapped.0.len();
31✔
305
              return wrapped.0.clone();
31✔
306
            }
307
          }
308

309
          // If not, wrap the line and cache it
310
          r.1 = Some((
10✔
311
            textwrap::wrap(r.0.as_ref(), logs_area_width)
20✔
312
              .into_iter()
313
              .map(|s| s.to_string())
15✔
314
              .map(|c| Span::styled(c, style))
25✔
315
              .map(ListItem::new)
316
              .collect::<Vec<ListItem<'_>>>(),
317
            logs_area.width,
10✔
318
          ));
319

320
          wrapped_lines_len += r.1.as_ref().unwrap().0.len();
10✔
321
          r.1.as_ref().unwrap().0.clone()
10✔
322
        }),
41✔
323
    );
324

325
    let wrapped_lines_to_skip = if follow {
5!
326
      wrapped_lines_len.saturating_sub(available_lines)
2✔
327
    } else {
328
      0
3✔
329
    };
330

331
    let items = items
10✔
332
      .into_iter()
333
      // Wrapping could have created more lines than what we can display;
334
      // skip them
335
      .skip(wrapped_lines_to_skip)
5✔
336
      .collect::<Vec<_>>();
337

338
    self.wrapped_length = items.len();
5✔
339

340
    // TODO: All this is a workaround. we should be wrapping text with paragraph, but it currently
341
    // doesn't support wrapping and staying scrolled to the bottom
342
    //
343
    // see https://github.com/fdehau/tui-rs/issues/89
344
    let list = List::new(items)
15✔
345
      .block(block)
5✔
346
      .highlight_style(Style::default().add_modifier(Modifier::BOLD));
5✔
347

348
    f.render_stateful_widget(list, logs_area, &mut self.state);
5✔
349
  }
5✔
350
  /// Add a record to be displayed
351
  pub fn add_record(&mut self, record: String) {
13✔
352
    self.records.push_back((record, None));
13✔
353
  }
13✔
354

355
  fn unselect(&mut self) {
2✔
356
    self.state.select(None);
2✔
357
  }
2✔
358
}
359

360
impl Scrollable for LogsState {
361
  fn scroll_down(&mut self, increment: usize) {
1✔
362
    let i = self.state.selected().map_or(0, |i| {
2✔
363
      if i >= self.wrapped_length.saturating_sub(increment) {
1!
364
        i
×
365
      } else {
366
        i + increment
1✔
367
      }
368
    });
1✔
369
    self.state.select(Some(i));
1✔
370
  }
1✔
371

372
  fn scroll_up(&mut self, decrement: usize) {
2✔
373
    let i = self.state.selected().map_or(0, |i| {
2✔
374
      if i != 0 {
×
375
        i.saturating_sub(decrement)
×
376
      } else {
377
        0
×
378
      }
379
    });
×
380
    self.state.select(Some(i));
2✔
381
  }
2✔
382
}
383

384
#[cfg(test)]
385
mod tests {
386
  use k8s_openapi::api::core::v1::Namespace;
387
  use kube::api::ObjectMeta;
388
  use tui::{backend::TestBackend, buffer::Buffer, Terminal};
389

390
  use super::*;
391
  use crate::app::{ns::KubeNs, ActiveBlock, RouteId};
392

393
  #[test]
394
  fn test_kube_resource() {
2✔
395
    struct TestStruct {
396
      k8s_obj: Namespace,
397
    }
398
    impl KubeResource<Namespace> for TestStruct {
399
      fn get_k8s_obj(&self) -> &Namespace {
1✔
400
        &self.k8s_obj
401
      }
1✔
402
    }
403
    let ts = TestStruct {
1✔
404
      k8s_obj: Namespace {
1✔
405
        metadata: ObjectMeta {
1✔
406
          name: Some("test".into()),
1✔
407
          namespace: Some("test".into()),
1✔
408
          ..ObjectMeta::default()
1✔
409
        },
410
        ..Namespace::default()
1✔
411
      },
412
    };
1✔
413
    assert_eq!(
1✔
414
      ts.resource_to_yaml(),
1!
415
      "apiVersion: v1\nkind: Namespace\nmetadata:\n  name: test\n  namespace: test\n"
416
    )
417
  }
2✔
418

419
  #[test]
420
  fn test_stateful_table() {
2✔
421
    let mut sft: StatefulTable<KubeNs> = StatefulTable::new();
1✔
422

423
    assert_eq!(sft.items.len(), 0);
1!
424
    assert_eq!(sft.state.selected(), None);
1!
425
    // check default selection on set
426
    sft.set_items(vec![KubeNs::default(), KubeNs::default()]);
1✔
427
    assert_eq!(sft.items.len(), 2);
1!
428
    assert_eq!(sft.state.selected(), Some(0));
1!
429
    // check selection retain on set
430
    sft.state.select(Some(1));
1✔
431
    sft.set_items(vec![
2✔
432
      KubeNs::default(),
1✔
433
      KubeNs::default(),
1✔
434
      KubeNs::default(),
1✔
435
    ]);
436
    assert_eq!(sft.items.len(), 3);
1!
437
    assert_eq!(sft.state.selected(), Some(1));
1!
438
    // check selection overflow prevention
439
    sft.state.select(Some(2));
1✔
440
    sft.set_items(vec![KubeNs::default(), KubeNs::default()]);
1✔
441
    assert_eq!(sft.items.len(), 2);
1!
442
    assert_eq!(sft.state.selected(), Some(1));
1!
443
    // check scroll down
444
    sft.state.select(Some(0));
1✔
445
    assert_eq!(sft.state.selected(), Some(0));
1!
446
    sft.scroll_down(1);
1✔
447
    assert_eq!(sft.state.selected(), Some(1));
1!
448
    // check scroll overflow
449
    sft.scroll_down(1);
1✔
450
    assert_eq!(sft.state.selected(), Some(1));
1!
451
    sft.scroll_up(1);
1✔
452
    assert_eq!(sft.state.selected(), Some(0));
1!
453
    // check scroll overflow
454
    sft.scroll_up(1);
1✔
455
    assert_eq!(sft.state.selected(), Some(0));
1!
456
    // check increment
457
    sft.scroll_down(10);
1✔
458
    assert_eq!(sft.state.selected(), Some(1));
1!
459

460
    let sft2 = StatefulTable::with_items(vec![KubeNs::default(), KubeNs::default()]);
1✔
461
    assert_eq!(sft2.state.selected(), Some(0));
1!
462
  }
2✔
463

464
  #[test]
465
  fn test_handle_table_scroll() {
2✔
466
    let mut item: StatefulTable<&str> = StatefulTable::new();
1✔
467
    item.set_items(vec!["A", "B", "C"]);
1✔
468

469
    assert_eq!(item.state.selected(), Some(0));
1!
470

471
    item.handle_scroll(false, false);
1✔
472
    assert_eq!(item.state.selected(), Some(1));
1!
473

474
    item.handle_scroll(false, false);
1✔
475
    assert_eq!(item.state.selected(), Some(2));
1!
476

477
    item.handle_scroll(false, false);
1✔
478
    assert_eq!(item.state.selected(), Some(2));
1!
479
    // previous
480
    item.handle_scroll(true, false);
1✔
481
    assert_eq!(item.state.selected(), Some(1));
1!
482
    // page down
483
    item.handle_scroll(false, true);
1✔
484
    assert_eq!(item.state.selected(), Some(2));
1!
485
    // page up
486
    item.handle_scroll(true, true);
1✔
487
    assert_eq!(item.state.selected(), Some(0));
1!
488
  }
2✔
489

490
  #[test]
491
  fn test_stateful_tab() {
2✔
492
    let mut tab = TabsState::new(vec![
2✔
493
      TabRoute {
1✔
494
        title: "Hello".into(),
1✔
495
        route: Route {
1✔
496
          active_block: ActiveBlock::Pods,
497
          id: RouteId::Home,
498
        },
499
      },
500
      TabRoute {
1✔
501
        title: "Test".into(),
1✔
502
        route: Route {
1✔
503
          active_block: ActiveBlock::Nodes,
504
          id: RouteId::Home,
505
        },
506
      },
507
    ]);
508

509
    assert_eq!(tab.index, 0);
1!
510
    assert_eq!(tab.get_active_route().active_block, ActiveBlock::Pods);
1!
511
    tab.next();
1✔
512
    assert_eq!(tab.index, 1);
1!
513
    assert_eq!(tab.get_active_route().active_block, ActiveBlock::Nodes);
1!
514
    tab.next();
1✔
515
    assert_eq!(tab.index, 0);
1!
516
    assert_eq!(tab.get_active_route().active_block, ActiveBlock::Pods);
1!
517
    tab.previous();
1✔
518
    assert_eq!(tab.index, 1);
1!
519
    assert_eq!(tab.get_active_route().active_block, ActiveBlock::Nodes);
1!
520
    tab.previous();
1✔
521
    assert_eq!(tab.index, 0);
1!
522
    assert_eq!(tab.get_active_route().active_block, ActiveBlock::Pods);
1!
523
  }
2✔
524

525
  #[test]
526
  fn test_scrollable_txt() {
2✔
527
    let mut stxt = ScrollableTxt::with_string("test\n multiline\n string".into());
1✔
528

529
    assert_eq!(stxt.offset, 0);
1!
530
    assert_eq!(stxt.items.len(), 3);
1!
531

532
    assert_eq!(stxt.get_txt(), "test\n multiline\n string");
1!
533

534
    stxt.scroll_down(1);
1✔
535
    assert_eq!(stxt.offset, 0);
1!
536

537
    let mut stxt2 = ScrollableTxt::with_string("te\nst\nmul\ntil\ni\nne\nstr\ni\nn\ng".into());
1✔
538
    assert_eq!(stxt2.items.len(), 10);
1!
539
    stxt2.scroll_down(1);
1✔
540
    assert_eq!(stxt2.offset, 1);
1!
541
    stxt2.scroll_down(1);
1✔
542
    assert_eq!(stxt2.offset, 2);
1!
543
    stxt2.scroll_down(5);
1✔
544
    assert_eq!(stxt2.offset, 7);
1!
545
    stxt2.scroll_down(1);
1✔
546
    // no overflow past (len - 2)
547
    assert_eq!(stxt2.offset, 7);
1!
548
    stxt2.scroll_up(1);
1✔
549
    assert_eq!(stxt2.offset, 6);
1!
550
    stxt2.scroll_up(6);
1✔
551
    assert_eq!(stxt2.offset, 0);
1!
552
    stxt2.scroll_up(1);
1✔
553
    // no overflow past (0)
554
    assert_eq!(stxt2.offset, 0);
1!
555
  }
2✔
556

557
  #[test]
558
  fn test_logs_state() {
2✔
559
    let mut log = LogsState::new("1".into());
1✔
560
    log.add_record("record 1".into());
1✔
561
    log.add_record("record 2".into());
1✔
562

563
    assert_eq!(log.get_plain_text(), "\nrecord 1\nrecord 2");
1!
564

565
    let backend = TestBackend::new(20, 7);
1✔
566
    let mut terminal = Terminal::new(backend).unwrap();
1✔
567

568
    log.add_record("record 4 should be long enough to be wrapped".into());
1✔
569
    log.add_record("record 5".into());
1✔
570
    log.add_record("record 6".into());
1✔
571
    log.add_record("record 7".into());
1✔
572
    log.add_record("record 8".into());
1✔
573

574
    terminal
1✔
575
      .draw(|f| log.render_list(f, f.size(), Block::default(), Style::default(), true))
2✔
576
      .unwrap();
577

578
    let expected = Buffer::with_lines(vec![
1✔
579
      "record 4 should be  ",
580
      "long enough to be   ",
581
      "wrapped             ",
582
      "record 5            ",
583
      "record 6            ",
584
      "record 7            ",
585
      "record 8            ",
586
    ]);
587

588
    terminal.backend().assert_buffer(&expected);
1✔
589

590
    terminal
1✔
591
      .draw(|f| log.render_list(f, f.size(), Block::default(), Style::default(), false))
2✔
592
      .unwrap();
593

594
    let expected2 = Buffer::with_lines(vec![
1✔
595
      "record 1            ",
596
      "record 2            ",
597
      "record 4 should be  ",
598
      "long enough to be   ",
599
      "wrapped             ",
600
      "record 5            ",
601
      "record 6            ",
602
    ]);
603

604
    terminal.backend().assert_buffer(&expected2);
1✔
605

606
    log.add_record("record 9".into());
1✔
607
    log.add_record("record 10 which is again looooooooooooooooooooooooooooooonnnng".into());
1✔
608
    log.add_record("record 11".into());
1✔
609
    // enabling follow should scroll back to bottom
610
    terminal
1✔
611
      .draw(|f| log.render_list(f, f.size(), Block::default(), Style::default(), true))
2✔
612
      .unwrap();
613

614
    let expected3 = Buffer::with_lines(vec![
1✔
615
      "record 8            ",
616
      "record 9            ",
617
      "record 10           ",
618
      "which is again      ",
619
      "looooooooooooooooooo",
620
      "oooooooooooonnnng   ",
621
      "record 11           ",
622
    ]);
623

624
    terminal.backend().assert_buffer(&expected3);
1✔
625

626
    terminal
1✔
627
      .draw(|f| log.render_list(f, f.size(), Block::default(), Style::default(), false))
2✔
628
      .unwrap();
629

630
    let expected4 = Buffer::with_lines(vec![
1✔
631
      "record 1            ",
632
      "record 2            ",
633
      "record 4 should be  ",
634
      "long enough to be   ",
635
      "wrapped             ",
636
      "record 5            ",
637
      "record 6            ",
638
    ]);
639

640
    terminal.backend().assert_buffer(&expected4);
1✔
641

642
    log.scroll_up(1); // to reset select state
1✔
643
    log.scroll_down(11);
1✔
644

645
    terminal
1✔
646
      .draw(|f| log.render_list(f, f.size(), Block::default(), Style::default(), false))
2✔
647
      .unwrap();
648

649
    let mut expected5 = Buffer::with_lines(vec![
1✔
650
      "record 5            ",
651
      "record 6            ",
652
      "record 7            ",
653
      "record 8            ",
654
      "record 9            ",
655
      "record 10           ",
656
      "which is again      ",
657
    ]);
658

659
    // Second row table header style
660
    for col in 0..=19 {
21✔
661
      expected5
40✔
662
        .get_mut(col, 6)
663
        .set_style(Style::default().add_modifier(Modifier::BOLD));
20✔
664
    }
665

666
    terminal.backend().assert_buffer(&expected5);
1✔
667
  }
2✔
668
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc