1use std::collections::VecDeque;
14use std::time::{Duration, Instant};
15
16use gpui::{App, Global, SharedString};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct SourceRef {
22 pub file: SharedString,
23 pub line: u32,
24 pub column: u32,
25}
26
27impl SourceRef {
28 pub fn new(file: impl Into<SharedString>, line: u32, column: u32) -> Self {
29 SourceRef {
30 file: file.into(),
31 line,
32 column,
33 }
34 }
35
36 pub fn basename(&self) -> &str {
38 let file = self.file.as_ref();
39 match file.rsplit_once('/') {
40 Some((_, name)) => name,
41 None => file,
42 }
43 }
44
45 pub fn short(&self) -> String {
47 format!("{}:{}:{}", self.basename(), self.line, self.column)
48 }
49}
50
51impl From<&'static std::panic::Location<'static>> for SourceRef {
52 fn from(loc: &'static std::panic::Location<'static>) -> Self {
53 SourceRef::new(loc.file(), loc.line(), loc.column())
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
59pub enum LogLevel {
60 #[default]
62 Log,
63 Debug,
65 Info,
67 Warning,
69 Error,
71}
72
73impl LogLevel {
74 pub fn label(self) -> &'static str {
75 match self {
76 LogLevel::Log => "Log",
77 LogLevel::Debug => "Debug",
78 LogLevel::Info => "Info",
79 LogLevel::Warning => "Warning",
80 LogLevel::Error => "Error",
81 }
82 }
83
84 pub fn is_issue(self) -> bool {
86 matches!(self, LogLevel::Warning | LogLevel::Error)
87 }
88}
89
90#[derive(Debug, Clone)]
94pub struct LogRecord {
95 pub id: u64,
96 pub level: LogLevel,
97 pub message: SharedString,
98 pub details: Vec<(SharedString, SharedString)>,
101 pub source: Option<SourceRef>,
102 pub at: Duration,
103 pub count: usize,
104}
105
106impl LogRecord {
107 pub fn new(level: LogLevel, message: impl Into<SharedString>) -> Self {
108 LogRecord {
109 id: 0,
110 level,
111 message: message.into(),
112 details: Vec::new(),
113 source: None,
114 at: Duration::ZERO,
115 count: 1,
116 }
117 }
118
119 pub fn detail(mut self, key: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
120 self.details.push((key.into(), value.into()));
121 self
122 }
123
124 pub fn details(mut self, rows: impl IntoIterator<Item = (SharedString, SharedString)>) -> Self {
125 self.details.extend(rows);
126 self
127 }
128
129 pub fn source(mut self, source: SourceRef) -> Self {
130 self.source = Some(source);
131 self
132 }
133
134 fn same_as(&self, other: &LogRecord) -> bool {
136 self.level == other.level && self.message == other.message && self.details == other.details
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
143pub enum ResourceKind {
144 Document,
145 Stylesheet,
146 Image,
147 Font,
148 Script,
149 Fetch,
151 WebSocket,
152 Media,
153 #[default]
154 Other,
155}
156
157impl ResourceKind {
158 pub fn label(self) -> &'static str {
159 match self {
160 ResourceKind::Document => "Document",
161 ResourceKind::Stylesheet => "Stylesheet",
162 ResourceKind::Image => "Image",
163 ResourceKind::Font => "Font",
164 ResourceKind::Script => "Script",
165 ResourceKind::Fetch => "Fetch",
166 ResourceKind::WebSocket => "Socket",
167 ResourceKind::Media => "Media",
168 ResourceKind::Other => "Other",
169 }
170 }
171
172 pub const ALL: [ResourceKind; 9] = [
174 ResourceKind::Document,
175 ResourceKind::Stylesheet,
176 ResourceKind::Image,
177 ResourceKind::Font,
178 ResourceKind::Script,
179 ResourceKind::Fetch,
180 ResourceKind::WebSocket,
181 ResourceKind::Media,
182 ResourceKind::Other,
183 ];
184}
185
186#[derive(Debug, Clone, Copy, Default, PartialEq)]
190pub struct Timings {
191 pub stalled: Duration,
192 pub dns: Duration,
193 pub connect: Duration,
194 pub tls: Duration,
195 pub request: Duration,
196 pub response: Duration,
197}
198
199impl Timings {
200 pub fn total(&self) -> Duration {
201 self.stalled + self.dns + self.connect + self.tls + self.request + self.response
202 }
203
204 pub fn phases(&self) -> Vec<(&'static str, Duration)> {
206 [
207 ("Stalled", self.stalled),
208 ("DNS", self.dns),
209 ("Connect", self.connect),
210 ("Secure", self.tls),
211 ("Request", self.request),
212 ("Response", self.response),
213 ]
214 .into_iter()
215 .filter(|(_, d)| !d.is_zero())
216 .collect()
217 }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
223pub enum RequestState {
224 #[default]
225 Pending,
226 Finished,
227 Failed,
228 Canceled,
229}
230
231#[derive(Debug, Clone)]
233pub struct NetworkRecord {
234 pub id: u64,
235 pub method: SharedString,
236 pub url: SharedString,
237 pub kind: ResourceKind,
238 pub state: RequestState,
239 pub status: Option<u16>,
240 pub status_text: SharedString,
241 pub protocol: SharedString,
242 pub remote_address: SharedString,
243 pub priority: SharedString,
244 pub transfer_size: u64,
246 pub resource_size: u64,
248 pub cached: bool,
249 pub request_headers: Vec<(SharedString, SharedString)>,
250 pub response_headers: Vec<(SharedString, SharedString)>,
251 pub request_body: Option<SharedString>,
252 pub response_body: Option<SharedString>,
253 pub initiator: Option<SourceRef>,
254 pub error: Option<SharedString>,
255 pub start: Duration,
258 pub timings: Timings,
259}
260
261impl NetworkRecord {
262 pub fn new(method: impl Into<SharedString>, url: impl Into<SharedString>) -> Self {
263 NetworkRecord {
264 id: 0,
265 method: method.into(),
266 url: url.into(),
267 kind: ResourceKind::default(),
268 state: RequestState::Pending,
269 status: None,
270 status_text: SharedString::default(),
271 protocol: SharedString::default(),
272 remote_address: SharedString::default(),
273 priority: SharedString::default(),
274 transfer_size: 0,
275 resource_size: 0,
276 cached: false,
277 request_headers: Vec::new(),
278 response_headers: Vec::new(),
279 request_body: None,
280 response_body: None,
281 initiator: None,
282 error: None,
283 start: Duration::ZERO,
284 timings: Timings::default(),
285 }
286 }
287
288 pub fn kind(mut self, kind: ResourceKind) -> Self {
289 self.kind = kind;
290 self
291 }
292
293 pub fn status(mut self, status: u16, text: impl Into<SharedString>) -> Self {
294 self.status = Some(status);
295 self.status_text = text.into();
296 self
297 }
298
299 pub fn sizes(mut self, transfer: u64, resource: u64) -> Self {
300 self.transfer_size = transfer;
301 self.resource_size = resource;
302 self
303 }
304
305 pub fn timings(mut self, timings: Timings) -> Self {
306 self.timings = timings;
307 self
308 }
309
310 pub fn request_header(
311 mut self,
312 name: impl Into<SharedString>,
313 value: impl Into<SharedString>,
314 ) -> Self {
315 self.request_headers.push((name.into(), value.into()));
316 self
317 }
318
319 pub fn response_header(
320 mut self,
321 name: impl Into<SharedString>,
322 value: impl Into<SharedString>,
323 ) -> Self {
324 self.response_headers.push((name.into(), value.into()));
325 self
326 }
327
328 pub fn request_body(mut self, body: impl Into<SharedString>) -> Self {
329 self.request_body = Some(body.into());
330 self
331 }
332
333 pub fn response_body(mut self, body: impl Into<SharedString>) -> Self {
334 self.response_body = Some(body.into());
335 self
336 }
337
338 pub fn initiator(mut self, source: SourceRef) -> Self {
339 self.initiator = Some(source);
340 self
341 }
342
343 pub fn finished(mut self) -> Self {
344 self.state = RequestState::Finished;
345 self
346 }
347
348 pub fn failed(mut self, error: impl Into<SharedString>) -> Self {
349 self.state = RequestState::Failed;
350 self.error = Some(error.into());
351 self
352 }
353
354 pub fn name(&self) -> &str {
356 let url = self.url.as_ref();
357 let path = url
358 .split_once("://")
359 .map(|(_, rest)| rest)
360 .unwrap_or(url)
361 .split(['?', '#'])
362 .next()
363 .unwrap_or("");
364 match path.rsplit_once('/') {
365 Some((_, last)) if !last.is_empty() => last,
366 _ => path.split('/').next().unwrap_or(url),
367 }
368 }
369
370 pub fn domain(&self) -> &str {
372 let url = self.url.as_ref();
373 let rest = url.split_once("://").map(|(_, r)| r).unwrap_or(url);
374 rest.split(['/', '?', '#']).next().unwrap_or(rest)
375 }
376
377 pub fn scheme(&self) -> &str {
379 self
380 .url
381 .as_ref()
382 .split_once("://")
383 .map(|(s, _)| s)
384 .unwrap_or("")
385 }
386
387 pub fn is_error(&self) -> bool {
390 self.state == RequestState::Failed || self.status.is_some_and(|s| s >= 400)
391 }
392
393 pub fn duration(&self) -> Duration {
394 self.timings.total()
395 }
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
400pub enum StorageKind {
401 #[default]
403 Local,
404 Session,
406 Cookies,
407 Database,
409 Cache,
410}
411
412impl StorageKind {
413 pub fn label(self) -> &'static str {
414 match self {
415 StorageKind::Local => "Local Storage",
416 StorageKind::Session => "Session Storage",
417 StorageKind::Cookies => "Cookies",
418 StorageKind::Database => "Databases",
419 StorageKind::Cache => "Caches",
420 }
421 }
422}
423
424#[derive(Debug, Clone, PartialEq, Eq)]
426pub struct StorageEntry {
427 pub key: SharedString,
428 pub value: SharedString,
429 pub extra: Vec<(SharedString, SharedString)>,
431}
432
433impl StorageEntry {
434 pub fn new(key: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
435 StorageEntry {
436 key: key.into(),
437 value: value.into(),
438 extra: Vec::new(),
439 }
440 }
441
442 pub fn extra(mut self, name: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
443 self.extra.push((name.into(), value.into()));
444 self
445 }
446}
447
448#[derive(Debug, Clone)]
450pub struct StorageDomain {
451 pub id: SharedString,
452 pub name: SharedString,
453 pub kind: StorageKind,
454 pub entries: Vec<StorageEntry>,
455 pub columns: Vec<SharedString>,
457}
458
459impl StorageDomain {
460 pub fn new(id: impl Into<SharedString>, name: impl Into<SharedString>) -> Self {
461 StorageDomain {
462 id: id.into(),
463 name: name.into(),
464 kind: StorageKind::default(),
465 entries: Vec::new(),
466 columns: Vec::new(),
467 }
468 }
469
470 pub fn kind(mut self, kind: StorageKind) -> Self {
471 self.kind = kind;
472 self
473 }
474
475 pub fn columns(mut self, columns: impl IntoIterator<Item = SharedString>) -> Self {
476 self.columns = columns.into_iter().collect();
477 self
478 }
479
480 pub fn entry(mut self, entry: StorageEntry) -> Self {
481 self.entries.push(entry);
482 self
483 }
484
485 pub fn entries(mut self, entries: impl IntoIterator<Item = StorageEntry>) -> Self {
486 self.entries.extend(entries);
487 self
488 }
489
490 pub fn size(&self) -> u64 {
492 self
493 .entries
494 .iter()
495 .map(|e| (e.key.len() + e.value.len()) as u64)
496 .sum()
497 }
498}
499
500#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
502pub enum TimelineKind {
503 #[default]
505 Frame,
506 Layout,
507 Paint,
508 Script,
510 Network,
511}
512
513impl TimelineKind {
514 pub fn label(self) -> &'static str {
515 match self {
516 TimelineKind::Frame => "Frames",
517 TimelineKind::Layout => "Layout",
518 TimelineKind::Paint => "Rendering",
519 TimelineKind::Script => "JavaScript & Events",
520 TimelineKind::Network => "Network Requests",
521 }
522 }
523
524 pub const ALL: [TimelineKind; 5] = [
525 TimelineKind::Frame,
526 TimelineKind::Layout,
527 TimelineKind::Paint,
528 TimelineKind::Script,
529 TimelineKind::Network,
530 ];
531}
532
533#[derive(Debug, Clone)]
535pub struct TimelineEvent {
536 pub id: u64,
537 pub kind: TimelineKind,
538 pub label: SharedString,
539 pub start: Duration,
540 pub duration: Duration,
541 pub source: Option<SourceRef>,
542}
543
544impl TimelineEvent {
545 pub fn new(kind: TimelineKind, label: impl Into<SharedString>, duration: Duration) -> Self {
546 TimelineEvent {
547 id: 0,
548 kind,
549 label: label.into(),
550 start: Duration::ZERO,
551 duration,
552 source: None,
553 }
554 }
555
556 pub fn end(&self) -> Duration {
557 self.start + self.duration
558 }
559}
560
561#[derive(Debug, Clone, Copy)]
563pub struct Limits {
564 pub logs: usize,
565 pub network: usize,
566 pub timeline: usize,
567}
568
569impl Default for Limits {
570 fn default() -> Self {
571 Limits {
572 logs: 1000,
573 network: 1000,
574 timeline: 4000,
575 }
576 }
577}
578
579pub struct DevToolsState {
586 epoch: Instant,
587 next_id: u64,
588 generation: u64,
589 limits: Limits,
590 logs: VecDeque<LogRecord>,
591 network: VecDeque<NetworkRecord>,
592 timeline: VecDeque<TimelineEvent>,
593 storage: Vec<StorageDomain>,
594 frames: VecDeque<Duration>,
596 last_frame: Option<Instant>,
597}
598
599impl Global for DevToolsState {}
600
601impl Default for DevToolsState {
602 fn default() -> Self {
603 DevToolsState::new()
604 }
605}
606
607impl DevToolsState {
608 pub fn new() -> Self {
609 DevToolsState {
610 epoch: Instant::now(),
611 next_id: 1,
612 generation: 0,
613 limits: Limits::default(),
614 logs: VecDeque::new(),
615 network: VecDeque::new(),
616 timeline: VecDeque::new(),
617 storage: Vec::new(),
618 frames: VecDeque::new(),
619 last_frame: None,
620 }
621 }
622
623 pub fn limits(mut self, limits: Limits) -> Self {
624 self.limits = limits;
625 self
626 }
627
628 pub fn init(self, cx: &mut App) {
631 cx.set_global(self);
632 }
633
634 pub fn generation(&self) -> u64 {
636 self.generation
637 }
638
639 pub fn now(&self) -> Duration {
641 self.epoch.elapsed()
642 }
643
644 fn tick(&mut self) -> u64 {
645 self.generation += 1;
646 let id = self.next_id;
647 self.next_id += 1;
648 id
649 }
650
651 pub fn push_log(&mut self, mut record: LogRecord) {
655 if let Some(last) = self.logs.back_mut() {
656 if last.same_as(&record) {
657 last.count += 1;
658 self.generation += 1;
659 return;
660 }
661 }
662 record.id = self.tick();
663 record.at = self.now();
664 self.logs.push_back(record);
665 while self.logs.len() > self.limits.logs {
666 self.logs.pop_front();
667 }
668 }
669
670 pub fn logs(&self) -> &VecDeque<LogRecord> {
671 &self.logs
672 }
673
674 pub fn clear_logs(&mut self) {
675 self.logs.clear();
676 self.generation += 1;
677 }
678
679 pub fn log_issues(&self) -> (usize, usize) {
681 let mut warnings = 0;
682 let mut errors = 0;
683 for record in &self.logs {
684 match record.level {
685 LogLevel::Warning => warnings += record.count,
686 LogLevel::Error => errors += record.count,
687 _ => {}
688 }
689 }
690 (warnings, errors)
691 }
692
693 pub fn push_network(&mut self, mut record: NetworkRecord) -> u64 {
698 let id = self.tick();
699 record.id = id;
700 if record.start.is_zero() {
701 record.start = self.now();
702 }
703 self.network.push_back(record);
704 while self.network.len() > self.limits.network {
705 self.network.pop_front();
706 }
707 id
708 }
709
710 pub fn update_network(&mut self, id: u64, f: impl FnOnce(&mut NetworkRecord)) {
713 if let Some(record) = self.network.iter_mut().find(|r| r.id == id) {
714 f(record);
715 self.generation += 1;
716 }
717 }
718
719 pub fn network(&self) -> &VecDeque<NetworkRecord> {
720 &self.network
721 }
722
723 pub fn clear_network(&mut self) {
724 self.network.clear();
725 self.generation += 1;
726 }
727
728 pub fn network_span(&self) -> (Duration, Duration) {
730 let start = self
731 .network
732 .iter()
733 .map(|r| r.start)
734 .min()
735 .unwrap_or(Duration::ZERO);
736 let end = self
737 .network
738 .iter()
739 .map(|r| r.start + r.duration())
740 .max()
741 .unwrap_or(Duration::ZERO);
742 (start, end.max(start))
743 }
744
745 pub fn network_totals(&self) -> (usize, u64, u64) {
747 let transfer = self.network.iter().map(|r| r.transfer_size).sum();
748 let resource = self.network.iter().map(|r| r.resource_size).sum();
749 (self.network.len(), transfer, resource)
750 }
751
752 pub fn set_storage(&mut self, domain: StorageDomain) {
758 self.generation += 1;
759 match self.storage.iter_mut().find(|d| d.id == domain.id) {
760 Some(existing) => *existing = domain,
761 None => self.storage.push(domain),
762 }
763 }
764
765 pub fn remove_storage(&mut self, id: &str) {
766 self.storage.retain(|d| d.id.as_ref() != id);
767 self.generation += 1;
768 }
769
770 pub fn storage(&self) -> &[StorageDomain] {
771 &self.storage
772 }
773
774 pub fn push_timeline(&mut self, mut event: TimelineEvent) {
777 event.id = self.tick();
778 if event.start.is_zero() {
779 event.start = self.now().saturating_sub(event.duration);
780 }
781 self.timeline.push_back(event);
782 while self.timeline.len() > self.limits.timeline {
783 self.timeline.pop_front();
784 }
785 }
786
787 pub fn timeline(&self) -> &VecDeque<TimelineEvent> {
788 &self.timeline
789 }
790
791 pub fn clear_timeline(&mut self) {
792 self.timeline.clear();
793 self.frames.clear();
794 self.last_frame = None;
795 self.generation += 1;
796 }
797
798 pub fn record_frame(&mut self) {
802 let now = Instant::now();
803 if let Some(previous) = self.last_frame.replace(now) {
804 let delta = now.duration_since(previous);
805 if delta < Duration::from_secs(1) {
808 self.frames.push_back(delta);
809 while self.frames.len() > 240 {
810 self.frames.pop_front();
811 }
812 }
813 }
814 }
815
816 pub fn stop_frames(&mut self) {
819 self.last_frame = None;
820 }
821
822 pub fn frames(&self) -> &VecDeque<Duration> {
823 &self.frames
824 }
825
826 pub fn fps(&self) -> Option<f32> {
829 if self.frames.is_empty() {
830 return None;
831 }
832 let total: Duration = self.frames.iter().sum();
833 if total.is_zero() {
834 return None;
835 }
836 Some(self.frames.len() as f32 / total.as_secs_f32())
837 }
838
839 pub fn clear_all(&mut self) {
842 self.logs.clear();
843 self.network.clear();
844 self.timeline.clear();
845 self.frames.clear();
846 self.last_frame = None;
847 self.generation += 1;
848 }
849}
850
851pub fn format_bytes(bytes: u64) -> String {
853 const KB: f64 = 1024.0;
854 let bytes = bytes as f64;
855 if bytes < KB {
856 format!("{} B", bytes as u64)
857 } else if bytes < KB * KB {
858 format!("{:.1} KB", bytes / KB)
859 } else if bytes < KB * KB * KB {
860 format!("{:.2} MB", bytes / (KB * KB))
861 } else {
862 format!("{:.2} GB", bytes / (KB * KB * KB))
863 }
864}
865
866pub fn format_duration(duration: Duration) -> String {
869 let ms = duration.as_secs_f64() * 1000.0;
870 if ms < 1.0 {
871 format!("{:.2} ms", ms)
872 } else if ms < 1000.0 {
873 format!("{:.0} ms", ms)
874 } else {
875 format!("{:.2} s", ms / 1000.0)
876 }
877}
878
879pub fn format_timestamp(at: Duration) -> String {
881 let total = at.as_secs();
882 let minutes = total / 60;
883 let seconds = total % 60;
884 format!("{:02}:{:02}.{:03}", minutes, seconds, at.subsec_millis())
885}
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890
891 #[test]
892 fn identical_log_lines_coalesce() {
893 let mut state = DevToolsState::new();
894 state.push_log(LogRecord::new(LogLevel::Warning, "slow frame"));
895 state.push_log(LogRecord::new(LogLevel::Warning, "slow frame"));
896 state.push_log(LogRecord::new(LogLevel::Warning, "slow frame"));
897
898 assert_eq!(state.logs().len(), 1);
899 assert_eq!(state.logs()[0].count, 3);
900 assert_eq!(state.log_issues(), (3, 0));
901 }
902
903 #[test]
904 fn a_different_line_breaks_the_run() {
905 let mut state = DevToolsState::new();
906 state.push_log(LogRecord::new(LogLevel::Log, "a"));
907 state.push_log(LogRecord::new(LogLevel::Log, "b"));
908 state.push_log(LogRecord::new(LogLevel::Log, "a"));
909
910 assert_eq!(state.logs().len(), 3);
911 assert!(state.logs().iter().all(|r| r.count == 1));
912 }
913
914 #[test]
915 fn logs_evict_oldest_past_the_limit() {
916 let mut state = DevToolsState::new().limits(Limits {
917 logs: 3,
918 ..Limits::default()
919 });
920 for i in 0..6 {
921 state.push_log(LogRecord::new(LogLevel::Log, format!("line {i}")));
922 }
923
924 assert_eq!(state.logs().len(), 3);
925 assert_eq!(state.logs()[0].message.as_ref(), "line 3");
926 assert_eq!(state.logs()[2].message.as_ref(), "line 5");
927 }
928
929 #[test]
930 fn a_request_settles_by_id() {
931 let mut state = DevToolsState::new();
932 let id = state.push_network(NetworkRecord::new(
933 "GET",
934 "https://api.example.com/v1/users",
935 ));
936 assert_eq!(state.network()[0].state, RequestState::Pending);
937
938 state.update_network(id, |record| {
939 record.state = RequestState::Finished;
940 record.status = Some(200);
941 });
942
943 assert_eq!(state.network()[0].state, RequestState::Finished);
944 assert_eq!(state.network()[0].status, Some(200));
945 }
946
947 #[test]
948 fn settling_an_evicted_request_is_a_no_op() {
949 let mut state = DevToolsState::new().limits(Limits {
950 network: 1,
951 ..Limits::default()
952 });
953 let first = state.push_network(NetworkRecord::new("GET", "https://example.com/a"));
954 state.push_network(NetworkRecord::new("GET", "https://example.com/b"));
955
956 state.update_network(first, |record| record.status = Some(500));
957
958 assert_eq!(state.network().len(), 1);
959 assert_eq!(state.network()[0].status, None);
960 }
961
962 #[test]
963 fn url_splits_into_name_domain_and_scheme() {
964 let record = NetworkRecord::new("GET", "https://api.example.com/v1/users?page=2");
965 assert_eq!(record.name(), "users");
966 assert_eq!(record.domain(), "api.example.com");
967 assert_eq!(record.scheme(), "https");
968
969 let root = NetworkRecord::new("GET", "https://example.com/");
970 assert_eq!(root.domain(), "example.com");
971
972 let bare = NetworkRecord::new("GET", "https://example.com");
973 assert_eq!(bare.name(), "example.com");
974 }
975
976 #[test]
977 fn errors_are_status_or_transport() {
978 assert!(NetworkRecord::new("GET", "/a")
979 .status(404, "Not Found")
980 .is_error());
981 assert!(NetworkRecord::new("GET", "/a")
982 .status(500, "Server Error")
983 .is_error());
984 assert!(!NetworkRecord::new("GET", "/a")
985 .status(304, "Not Modified")
986 .is_error());
987 assert!(NetworkRecord::new("GET", "/a").failed("offline").is_error());
988 }
989
990 #[test]
991 fn timings_sum_and_drop_empty_phases() {
992 let timings = Timings {
993 dns: Duration::from_millis(4),
994 connect: Duration::from_millis(11),
995 response: Duration::from_millis(35),
996 ..Timings::default()
997 };
998
999 assert_eq!(timings.total(), Duration::from_millis(50));
1000 assert_eq!(
1001 timings.phases().iter().map(|(n, _)| *n).collect::<Vec<_>>(),
1002 vec!["DNS", "Connect", "Response"]
1003 );
1004 }
1005
1006 #[test]
1007 fn registering_a_storage_domain_twice_replaces_it() {
1008 let mut state = DevToolsState::new();
1009 state
1010 .set_storage(StorageDomain::new("prefs", "Preferences").entry(StorageEntry::new("a", "1")));
1011 state.set_storage(
1012 StorageDomain::new("prefs", "Preferences")
1013 .entry(StorageEntry::new("a", "2"))
1014 .entry(StorageEntry::new("b", "3")),
1015 );
1016
1017 assert_eq!(state.storage().len(), 1);
1018 assert_eq!(state.storage()[0].entries.len(), 2);
1019 assert_eq!(state.storage()[0].entries[0].value.as_ref(), "2");
1020 }
1021
1022 #[test]
1023 fn network_span_covers_every_row() {
1024 let mut state = DevToolsState::new();
1025 let mut first = NetworkRecord::new("GET", "/a");
1026 first.start = Duration::from_millis(100);
1027 first.timings.response = Duration::from_millis(50);
1028 let mut second = NetworkRecord::new("GET", "/b");
1029 second.start = Duration::from_millis(20);
1030 second.timings.response = Duration::from_millis(10);
1031 state.push_network(first);
1032 state.push_network(second);
1033
1034 let (start, end) = state.network_span();
1035 assert_eq!(start, Duration::from_millis(20));
1036 assert_eq!(end, Duration::from_millis(150));
1037 }
1038
1039 #[test]
1040 fn byte_and_duration_formats_match_the_columns() {
1041 assert_eq!(format_bytes(512), "512 B");
1042 assert_eq!(format_bytes(2048), "2.0 KB");
1043 assert_eq!(format_bytes(5 * 1024 * 1024), "5.00 MB");
1044
1045 assert_eq!(format_duration(Duration::from_micros(250)), "0.25 ms");
1046 assert_eq!(format_duration(Duration::from_millis(42)), "42 ms");
1047 assert_eq!(format_duration(Duration::from_millis(1500)), "1.50 s");
1048
1049 assert_eq!(format_timestamp(Duration::from_millis(63_042)), "01:03.042");
1050 }
1051
1052 #[test]
1053 fn source_refs_shorten_to_basename() {
1054 let source = SourceRef::new("crates/guise/src/button.rs", 42, 9);
1055 assert_eq!(source.basename(), "button.rs");
1056 assert_eq!(source.short(), "button.rs:42:9");
1057 }
1058}