• 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

86.17
/src/app/pods.rs
1
use k8s_openapi::{
1✔
2
  api::core::v1::{
3
    Container, ContainerPort, ContainerState, ContainerStateWaiting, ContainerStatus, Pod, PodSpec,
4
    PodStatus,
5
  },
6
  chrono::Utc,
7
};
8

9
use super::{
10
  models::KubeResource,
11
  utils::{self, UNKNOWN},
12
};
13

14
#[derive(Clone, Default, Debug, PartialEq)]
24✔
15
pub struct KubePod {
16
  pub namespace: String,
12✔
17
  pub name: String,
12!
18
  pub ready: (i32, i32),
12!
19
  pub status: String,
12!
20
  pub restarts: i32,
12!
21
  pub cpu: String,
12!
22
  pub mem: String,
12!
23
  pub age: String,
12!
24
  pub containers: Vec<KubeContainer>,
12!
25
  k8s_obj: Pod,
12!
26
}
27

28
#[derive(Clone, Default, Debug, Eq, PartialEq)]
24✔
29
pub struct KubeContainer {
30
  pub name: String,
12✔
31
  pub image: String,
12!
32
  pub ready: String,
12!
33
  pub status: String,
12!
34
  pub restarts: i32,
12!
35
  pub liveliness_probe: bool,
12!
36
  pub readiness_probe: bool,
12!
37
  pub ports: String,
12!
38
  pub age: String,
12!
39
  pub pod_name: String,
12!
40
  pub init: bool,
12!
41
}
42

43
impl From<Pod> for KubePod {
44
  fn from(pod: Pod) -> Self {
13✔
45
    let age = utils::to_age(pod.metadata.creation_timestamp.as_ref(), Utc::now());
13✔
46
    let pod_name = pod.metadata.name.clone().unwrap_or_default();
13✔
47
    let (status, cr, restarts, c_stats_len, containers) = match &pod.status {
26!
48
      Some(status) => {
13✔
49
        let (mut cr, mut rc) = (0, 0);
13✔
50
        let c_stats_len = match status.container_statuses.as_ref() {
13✔
51
          Some(c_stats) => {
7✔
52
            c_stats.iter().for_each(|cs| {
14✔
53
              if cs.ready {
7✔
54
                cr += 1;
4✔
55
              }
56
              rc += cs.restart_count;
7✔
57
            });
7✔
58
            c_stats.len()
7✔
59
          }
60
          None => 0,
6✔
61
        };
62

63
        let mut containers: Vec<KubeContainer> = pod
39✔
64
          .spec
65
          .as_ref()
66
          .unwrap_or(&PodSpec::default())
13✔
67
          .containers
68
          .iter()
69
          .map(|c| {
26✔
70
            KubeContainer::from_api(
13✔
71
              c,
72
              pod_name.to_owned(),
13✔
73
              age.to_owned(),
13✔
74
              &status.container_statuses,
13✔
75
              false,
76
            )
77
          })
13✔
78
          .collect();
13✔
79

80
        let mut init_containers: Vec<KubeContainer> = pod
52✔
81
          .spec
82
          .as_ref()
83
          .unwrap_or(&PodSpec::default())
13✔
84
          .init_containers
85
          .as_ref()
86
          .unwrap_or(&vec![])
13✔
87
          .iter()
88
          .map(|c| {
17✔
89
            KubeContainer::from_api(
4✔
90
              c,
91
              pod_name.to_owned(),
4✔
92
              age.to_owned(),
4✔
93
              &status.init_container_statuses,
4✔
94
              true,
95
            )
96
          })
4✔
97
          .collect();
13✔
98

99
        // merge containers and init-containers into single array
100
        containers.append(&mut init_containers);
13✔
101

102
        (get_status(status, &pod), cr, rc, c_stats_len, containers)
13✔
103
      }
13✔
104
      _ => (UNKNOWN.into(), 0, 0, 0, vec![]),
×
105
    };
106

107
    KubePod {
13✔
108
      name: pod_name,
13✔
109
      namespace: pod.metadata.namespace.clone().unwrap_or_default(),
13✔
110
      ready: (cr, c_stats_len as i32),
13✔
111
      restarts,
112
      // TODO implement pod metrics
113
      cpu: String::default(),
13✔
114
      mem: String::default(),
13✔
115
      status,
13✔
116
      age,
13✔
117
      containers,
13✔
118
      k8s_obj: utils::sanitize_obj(pod),
13✔
119
    }
120
  }
13✔
121
}
122

123
impl KubeResource<Pod> for KubePod {
124
  fn get_k8s_obj(&self) -> &Pod {
1✔
125
    &self.k8s_obj
126
  }
1✔
127
}
128

129
impl KubeContainer {
130
  pub fn from_api(
17✔
131
    container: &Container,
132
    pod_name: String,
133
    age: String,
134
    c_stats_ref: &Option<Vec<ContainerStatus>>,
135
    init: bool,
136
  ) -> Self {
137
    let (mut ready, mut status, mut restarts) = ("false".to_string(), "<none>".to_string(), 0);
17✔
138
    if let Some(c_stats) = c_stats_ref {
17✔
139
      if let Some(c_stat) = c_stats.iter().find(|cs| cs.name == container.name) {
24!
140
        ready = c_stat.ready.to_string();
11✔
141
        status = get_container_state(c_stat.state.clone());
11✔
142
        restarts = c_stat.restart_count;
11✔
143
      }
144
    }
145

146
    KubeContainer {
17✔
147
      name: container.name.clone(),
17✔
148
      pod_name,
17✔
149
      image: container.image.clone().unwrap_or_default(),
17✔
150
      ready,
17✔
151
      status,
17✔
152
      restarts,
17✔
153
      liveliness_probe: container.liveness_probe.is_some(),
17✔
154
      readiness_probe: container.readiness_probe.is_some(),
17✔
155
      ports: get_container_ports(&container.ports).unwrap_or_default(),
17✔
156
      age,
17✔
157
      init,
158
    }
159
  }
17✔
160
}
161

162
fn get_container_state(os: Option<ContainerState>) -> String {
11✔
163
  match os {
11!
164
    Some(s) => {
11✔
165
      if let Some(sw) = s.waiting {
11✔
166
        sw.reason.unwrap_or_else(|| "Waiting".into())
2✔
167
      } else if let Some(st) = s.terminated {
11✔
168
        st.reason.unwrap_or_else(|| "Terminating".into())
4✔
169
      } else if s.running.is_some() {
9!
170
        "Running".into()
5✔
171
      } else {
172
        "<none>".into()
×
173
      }
174
    }
11!
175
    None => "<none>".into(),
×
176
  }
177
}
11✔
178

179
fn get_status(stat: &PodStatus, pod: &Pod) -> String {
13✔
180
  let status = match &stat.phase {
13!
181
    Some(phase) => phase.to_owned(),
13✔
182
    _ => UNKNOWN.into(),
×
183
  };
184
  let status = match &stat.reason {
13✔
185
    Some(reason) => {
2✔
186
      if reason == "NodeLost" && pod.metadata.deletion_timestamp.is_some() {
2!
187
        UNKNOWN.into()
×
188
      } else {
189
        reason.to_owned()
2✔
190
      }
191
    }
192
    None => status,
11✔
193
  };
194

195
  // get int container status
196
  let status = match &stat.init_container_statuses {
13✔
197
    Some(ics) => {
2✔
198
      for (i, cs) in ics.iter().enumerate() {
5✔
199
        let c_status = match &cs.state {
4!
200
          Some(s) => {
4✔
201
            if let Some(st) = &s.terminated {
4✔
202
              if st.exit_code == 0 {
3!
203
                "".into()
3✔
204
              } else if st.reason.as_ref().unwrap_or(&String::default()).is_empty() {
×
205
                format!("Init:{}", st.reason.as_ref().unwrap())
×
206
              } else if st.signal.unwrap_or_default() != 0 {
×
207
                format!("Init:Signal:{}", st.signal.unwrap())
×
208
              } else {
209
                format!("Init:ExitCode:{}", st.exit_code)
×
210
              }
211
            } else if is_pod_init(s.waiting.clone()) {
1!
212
              format!(
×
213
                "Init:{}",
214
                s.waiting
×
215
                  .as_ref()
216
                  .unwrap()
217
                  .reason
218
                  .as_ref()
219
                  .unwrap_or(&String::default())
×
220
              )
221
            } else {
222
              format!(
2✔
223
                "Init:{}/{}",
224
                i,
225
                pod
1✔
226
                  .spec
227
                  .as_ref()
228
                  .and_then(|ps| ps.init_containers.as_ref().map(|pic| pic.len()))
2✔
229
                  .unwrap_or(0)
230
              )
231
            }
232
          }
233
          None => "".into(),
×
234
        };
235
        if !c_status.is_empty() {
4✔
236
          return c_status;
1✔
237
        }
238
      }
3✔
239
      status
1✔
240
    }
1✔
241
    None => status,
11✔
242
  };
243

244
  let (mut status, running) = match &stat.container_statuses {
24✔
245
    Some(css) => {
6✔
246
      let mut running = false;
6✔
247
      let status = css
12✔
248
        .iter()
249
        .rev()
250
        .find_map(|cs| {
12✔
251
          cs.state.as_ref().and_then(|s| {
12✔
252
            if cs.ready && s.running.is_some() {
6✔
253
              running = true;
4✔
254
            }
255
            if s
6✔
256
              .waiting
257
              .as_ref()
258
              .and_then(|w| w.reason.as_ref().map(|v| !v.is_empty()))
2✔
259
              .unwrap_or_default()
260
            {
261
              s.waiting.as_ref().and_then(|w| w.reason.clone())
2✔
262
            } else if s
5✔
263
              .terminated
264
              .as_ref()
265
              .and_then(|w| w.reason.as_ref().map(|v| !v.is_empty()))
2✔
266
              .unwrap_or_default()
267
            {
268
              s.terminated.as_ref().and_then(|w| w.reason.clone())
2✔
269
            } else if let Some(st) = &s.terminated {
4!
270
              if st.signal.unwrap_or_default() != 0 {
×
271
                Some(format!("Signal:{}", st.signal.unwrap_or_default()))
×
272
              } else {
273
                Some(format!("ExitCode:{}", st.exit_code))
×
274
              }
275
            } else {
276
              Some(status.clone())
4✔
277
            }
278
          })
6✔
279
        })
6✔
280
        .unwrap_or_default();
281
      (status, running)
6✔
282
    }
6✔
283
    None => (status, false),
6✔
284
  };
285

286
  if running && status == "Completed" {
12!
287
    status = "Running".into();
×
288
  }
289

290
  if pod.metadata.deletion_timestamp.is_none() {
12!
291
    return status;
12✔
292
  }
293

294
  "Terminating".into()
×
295
}
13!
296

297
fn is_pod_init(sw: Option<ContainerStateWaiting>) -> bool {
1✔
298
  sw.map(|w| w.reason.unwrap_or_default() != "PodInitializing")
1✔
299
    .unwrap_or_default()
300
}
1✔
301

302
fn get_container_ports(ports_ref: &Option<Vec<ContainerPort>>) -> Option<String> {
17✔
303
  ports_ref.as_ref().map(|ports| {
27✔
304
    ports
10✔
305
      .iter()
306
      .map(|c_port| {
13✔
307
        let mut port = String::new();
13✔
308
        if let Some(name) = c_port.name.clone() {
13✔
309
          port = format!("{}:", name);
2✔
310
        }
13!
311
        port = format!("{}{}", port, c_port.container_port);
13✔
312
        if let Some(protocol) = c_port.protocol.clone() {
13✔
313
          if protocol != "TCP" {
12✔
314
            port = format!("{}/{}", port, c_port.protocol.clone().unwrap());
3✔
315
          }
316
        }
13!
317
        port
318
      })
13✔
319
      .collect::<Vec<_>>()
320
      .join(", ")
321
  })
10✔
322
}
17✔
323

324
#[cfg(test)]
325
mod tests {
326
  use super::*;
327
  use crate::app::test_utils::*;
328

329
  #[test]
330
  fn test_pod_from_api() {
2✔
331
    let (pods, pods_list): (Vec<KubePod>, Vec<_>) = convert_resource_from_file("pods");
1✔
332

333
    assert_eq!(pods.len(), 13);
1!
334
    assert_eq!(
1✔
335
      pods[0],
1✔
336
      KubePod {
1✔
337
        namespace: "default".into(),
1✔
338
        name: "adservice-f787c8dcd-tb6x2".into(),
1✔
339
        ready: (0, 0),
1✔
340
        status: "Pending".into(),
1✔
341
        restarts: 0,
342
        cpu: "".into(),
1✔
343
        mem: "".into(),
1✔
344
        age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
345
        containers: vec![KubeContainer {
2✔
346
          name: "server".into(),
1✔
347
          image: "gcr.io/google-samples/microservices-demo/adservice:v0.2.2".into(),
1✔
348
          ready: "false".into(),
1✔
349
          status: "<none>".into(),
1✔
350
          restarts: 0,
351
          liveliness_probe: true,
352
          readiness_probe: true,
353
          ports: "9555".into(),
1✔
354
          age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
355
          pod_name: "adservice-f787c8dcd-tb6x2".into(),
1✔
356
          init: false,
357
        }],
358
        k8s_obj: pods_list[0].clone()
1!
359
      }
360
    );
361
    assert_eq!(
1✔
362
      pods[1],
1✔
363
      KubePod {
1✔
364
        namespace: "default".into(),
1✔
365
        name: "cartservice-67b89ffc69-s5qp8".into(),
1✔
366
        ready: (0, 1),
1✔
367
        status: "CrashLoopBackOff".into(),
1✔
368
        restarts: 896,
369
        cpu: "".into(),
1✔
370
        mem: "".into(),
1✔
371
        age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
372
        containers: vec![KubeContainer {
2✔
373
          name: "server".into(),
1✔
374
          image: "gcr.io/google-samples/microservices-demo/cartservice:v0.2.2".into(),
1✔
375
          ready: "false".into(),
1✔
376
          status: "CrashLoopBackOff".into(),
1✔
377
          restarts: 896,
378
          liveliness_probe: true,
379
          readiness_probe: true,
380
          ports: "7070".into(),
1✔
381
          age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
382
          pod_name: "cartservice-67b89ffc69-s5qp8".into(),
1✔
383
          init: false,
384
        }],
385
        k8s_obj: pods_list[1].clone()
1!
386
      }
387
    );
388
    assert_eq!(
1✔
389
      pods[3],
1✔
390
      KubePod {
1✔
391
        namespace: "default".into(),
1✔
392
        name: "emailservice-5f8fc7dbb4-5lqdb".into(),
1✔
393
        ready: (1, 1),
1✔
394
        status: "Running".into(),
1✔
395
        restarts: 3,
396
        cpu: "".into(),
1✔
397
        mem: "".into(),
1✔
398
        age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
399
        containers: vec![KubeContainer {
2✔
400
          name: "server".into(),
1✔
401
          image: "gcr.io/google-samples/microservices-demo/emailservice:v0.2.2".into(),
1✔
402
          ready: "true".into(),
1✔
403
          status: "Running".into(),
1✔
404
          restarts: 3,
405
          liveliness_probe: true,
406
          readiness_probe: true,
407
          ports: "8080".into(),
1✔
408
          age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
409
          pod_name: "emailservice-5f8fc7dbb4-5lqdb".into(),
1✔
410
          init: false,
411
        }],
412
        k8s_obj: pods_list[3].clone()
1!
413
      }
414
    );
415
    assert_eq!(
1✔
416
      pods[4],
1✔
417
      KubePod {
1✔
418
        namespace: "default".into(),
1✔
419
        name: "frontend-5c4745dfdb-6k8wf".into(),
1✔
420
        ready: (0, 0),
1✔
421
        status: "OutOfcpu".into(),
1✔
422
        restarts: 0,
423
        cpu: "".into(),
1✔
424
        mem: "".into(),
1✔
425
        age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
426
        containers: vec![KubeContainer {
2✔
427
          name: "server".into(),
1✔
428
          image: "gcr.io/google-samples/microservices-demo/frontend:v0.2.2".into(),
1✔
429
          ready: "false".into(),
1✔
430
          status: "<none>".into(),
1✔
431
          restarts: 0,
432
          liveliness_probe: true,
433
          readiness_probe: true,
434
          ports: "8080".into(),
1✔
435
          age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
436
          pod_name: "frontend-5c4745dfdb-6k8wf".into(),
1✔
437
          init: false,
438
        }],
439
        k8s_obj: pods_list[4].clone()
1!
440
      }
441
    );
442
    assert_eq!(
1✔
443
      pods[5],
1✔
444
      KubePod {
1✔
445
        namespace: "default".into(),
1✔
446
        name: "frontend-5c4745dfdb-qz7fg".into(),
1✔
447
        ready: (0, 0),
1✔
448
        status: "Preempting".into(),
1✔
449
        restarts: 0,
450
        cpu: "".into(),
1✔
451
        mem: "".into(),
1✔
452
        age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
453
        containers: vec![KubeContainer {
2✔
454
          name: "server".into(),
1✔
455
          image: "gcr.io/google-samples/microservices-demo/frontend:v0.2.2".into(),
1✔
456
          ready: "false".into(),
1✔
457
          status: "<none>".into(),
1✔
458
          restarts: 0,
459
          liveliness_probe: false,
460
          readiness_probe: true,
461
          ports: "8080/HTTP".into(),
1✔
462
          age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
463
          pod_name: "frontend-5c4745dfdb-qz7fg".into(),
1✔
464
          init: false,
465
        }],
466
        k8s_obj: pods_list[5].clone()
1!
467
      }
468
    );
469
    assert_eq!(
1✔
470
      pods[6],
1✔
471
      KubePod {
1✔
472
        namespace: "default".into(),
1✔
473
        name: "frontend-5c4745dfdb-6k8wf".into(),
1✔
474
        ready: (0, 0),
1✔
475
        status: "Failed".into(),
1✔
476
        restarts: 0,
477
        cpu: "".into(),
1✔
478
        mem: "".into(),
1✔
479
        age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
480
        containers: vec![KubeContainer {
2✔
481
          name: "server".into(),
1✔
482
          image: "gcr.io/google-samples/microservices-demo/frontend:v0.2.2".into(),
1✔
483
          ready: "false".into(),
1✔
484
          status: "<none>".into(),
1✔
485
          restarts: 0,
486
          liveliness_probe: true,
487
          readiness_probe: true,
488
          ports: "8080, 8081/UDP, Foo:8082/UDP, 8083".into(),
1✔
489
          age: utils::to_age(Some(&get_time("2021-04-27T10:13:58Z")), Utc::now()),
1✔
490
          pod_name: "frontend-5c4745dfdb-6k8wf".into(),
1✔
491
          init: false,
492
        }],
493
        k8s_obj: pods_list[6].clone()
1!
494
      }
495
    );
496
    assert_eq!(
1✔
497
      pods[11],
1✔
498
      KubePod {
1✔
499
        namespace: "default".into(),
1✔
500
        name: "pod-init-container".into(),
1✔
501
        ready: (0, 1),
1✔
502
        status: "Init:1/2".into(),
1✔
503
        restarts: 0,
504
        cpu: "".into(),
1✔
505
        mem: "".into(),
1✔
506
        age: utils::to_age(Some(&get_time("2021-06-18T08:57:56Z")), Utc::now()),
1✔
507
        containers: vec![
2✔
508
          KubeContainer {
1✔
509
            name: "main-busybox".into(),
1✔
510
            image: "busybox".into(),
1✔
511
            ready: "false".into(),
1✔
512
            status: "PodInitializing".into(),
1✔
513
            restarts: 0,
514
            liveliness_probe: false,
515
            readiness_probe: false,
516
            ports: "".into(),
1✔
517
            age: utils::to_age(Some(&get_time("2021-06-18T08:57:56Z")), Utc::now()),
1✔
518
            pod_name: "pod-init-container".into(),
1✔
519
            init: false,
520
          },
521
          KubeContainer {
1✔
522
            name: "init-busybox1".into(),
1✔
523
            image: "busybox".into(),
1✔
524
            ready: "true".into(),
1✔
525
            status: "Completed".into(),
1✔
526
            restarts: 0,
527
            liveliness_probe: false,
528
            readiness_probe: false,
529
            ports: "".into(),
1✔
530
            age: utils::to_age(Some(&get_time("2021-06-18T08:57:56Z")), Utc::now()),
1✔
531
            pod_name: "pod-init-container".into(),
1✔
532
            init: true,
533
          },
534
          KubeContainer {
1✔
535
            name: "init-busybox2".into(),
1✔
536
            image: "busybox".into(),
1✔
537
            ready: "false".into(),
1✔
538
            status: "Running".into(),
1✔
539
            restarts: 0,
540
            liveliness_probe: false,
541
            readiness_probe: false,
542
            ports: "".into(),
1✔
543
            age: utils::to_age(Some(&get_time("2021-06-18T08:57:56Z")), Utc::now()),
1✔
544
            pod_name: "pod-init-container".into(),
1✔
545
            init: true,
546
          }
547
        ],
548
        k8s_obj: pods_list[11].clone()
1!
549
      }
550
    );
551
    assert_eq!(
1✔
552
      pods[12],
1✔
553
      KubePod {
1✔
554
        namespace: "default".into(),
1✔
555
        name: "pod-init-container-2".into(),
1✔
556
        ready: (0, 1),
1✔
557
        status: "Completed".into(),
1✔
558
        restarts: 0,
559
        cpu: "".into(),
1✔
560
        mem: "".into(),
1✔
561
        age: utils::to_age(Some(&get_time("2021-06-18T09:26:11Z")), Utc::now()),
1✔
562
        containers: vec![
2✔
563
          KubeContainer {
1✔
564
            name: "main-busybox".into(),
1✔
565
            image: "busybox".into(),
1✔
566
            ready: "false".into(),
1✔
567
            status: "Completed".into(),
1✔
568
            restarts: 0,
569
            liveliness_probe: false,
570
            readiness_probe: false,
571
            ports: "".into(),
1✔
572
            age: utils::to_age(Some(&get_time("2021-06-18T09:26:11Z")), Utc::now()),
1✔
573
            pod_name: "pod-init-container-2".into(),
1✔
574
            init: false,
575
          },
576
          KubeContainer {
1✔
577
            name: "init-busybox1".into(),
1✔
578
            image: "busybox".into(),
1✔
579
            ready: "true".into(),
1✔
580
            status: "Completed".into(),
1✔
581
            restarts: 0,
582
            liveliness_probe: false,
583
            readiness_probe: false,
584
            ports: "".into(),
1✔
585
            age: utils::to_age(Some(&get_time("2021-06-18T09:26:11Z")), Utc::now()),
1✔
586
            pod_name: "pod-init-container-2".into(),
1✔
587
            init: true,
588
          },
589
          KubeContainer {
1✔
590
            name: "init-busybox2".into(),
1✔
591
            image: "busybox".into(),
1✔
592
            ready: "true".into(),
1✔
593
            status: "Completed".into(),
1✔
594
            restarts: 0,
595
            liveliness_probe: false,
596
            readiness_probe: false,
597
            ports: "".into(),
1✔
598
            age: utils::to_age(Some(&get_time("2021-06-18T09:26:11Z")), Utc::now()),
1✔
599
            pod_name: "pod-init-container-2".into(),
1✔
600
            init: true,
601
          }
602
        ],
603
        k8s_obj: pods_list[12].clone()
1!
604
      }
605
    );
606
    // TODO add tests for NodeLost case
607
  }
2✔
608
}
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