1use super::Chart;
4use crate::data::FeedError;
5use crate::data::inbox::Inbox;
6use crate::drawings::persistence::{DrawingStorage, LoadError, StorageError, StoredPane};
7use crate::localization as l10n;
8use crate::services::document::{Received, StoredDocument, copy_number};
9use crate::services::{ConflictChoice, Revision, Storage, StoreKind};
10use std::collections::{HashMap, VecDeque};
11use std::sync::Arc;
12
13fn message(ctx: Option<&egui::Context>, english: &str, values: &[(&str, &str)]) -> String {
15 match ctx {
16 Some(ctx) => l10n::format_text(ctx, english, values),
17 None => l10n::substitute(english, values),
18 }
19}
20
21#[derive(Default)]
23struct LocalCopies {
24 inbox: Arc<Inbox>,
25 listed: Option<(String, Vec<String>)>,
27 sent: Option<CopyRequest>,
29 queued: VecDeque<CopyRequest>,
31 read: Option<(String, String, String)>,
33}
34
35impl LocalCopies {
36 fn forget(&mut self, symbol: &str, name: &str) {
38 if let Some((listed, names)) = &mut self.listed
39 && listed == symbol
40 {
41 names.retain(|listed| listed != name);
42 }
43 }
44}
45
46enum CopyRequest {
48 List { symbol: String },
50 Read {
52 symbol: String,
53 name: String,
54 discard: bool,
55 },
56 Delete { symbol: String, name: String },
58}
59
60impl CopyRequest {
61 fn symbol(&self) -> &str {
62 match self {
63 Self::List { symbol } | Self::Read { symbol, .. } | Self::Delete { symbol, .. } => {
64 symbol
65 }
66 }
67 }
68}
69
70pub(super) struct DrawingStore {
71 sync: fn(&mut Chart, Option<&egui::Context>),
73 save: fn(&mut Chart),
74 storage: Arc<dyn Storage>,
75 documents: HashMap<String, StoredDocument>,
77 loaded: HashMap<String, String>,
79 captured: u64,
81 pending: HashMap<String, (DrawingStorage, bool)>,
83 error: Option<StorageError>,
84 ctx: Option<egui::Context>,
86 changed: HashMap<String, Option<Revision>>,
90 unshown_panes: HashMap<String, HashMap<String, (i64, StoredPane)>>,
93 copies: LocalCopies,
95}
96
97impl DrawingStore {
98 pub(super) fn forget(&mut self, symbol: &str) -> bool {
99 if self.pending.contains_key(symbol)
100 || self
101 .documents
102 .get(symbol)
103 .is_some_and(StoredDocument::saving)
104 {
105 return false;
106 }
107 self.documents.remove(symbol);
108 self.loaded.remove(symbol);
109 self.unshown_panes.remove(symbol);
110 self.documents.shrink_to_fit();
111 self.loaded.shrink_to_fit();
112 true
113 }
114
115 fn receive_copies(&mut self) {
119 loop {
120 if self.copies.sent.is_none()
121 && self.copies.read.is_none()
122 && let Some(request) = self.copies.queued.front()
123 && !self
124 .documents
125 .get(request.symbol())
126 .is_some_and(StoredDocument::copying)
127 {
128 let request = self.copies.queued.pop_front().expect("queued request");
129 let inbox = &self.copies.inbox;
130 match &request {
131 CopyRequest::List { .. } => {
132 self.storage.list(StoreKind::Drawings, inbox.reply(0));
133 }
134 CopyRequest::Read { name, .. } => {
135 self.storage.load(StoreKind::Drawings, name, inbox.reply(0));
136 }
137 CopyRequest::Delete { name, .. } => {
138 self.storage
139 .delete(StoreKind::Drawings, name, inbox.reply(0));
140 }
141 }
142 self.copies.sent = Some(request);
143 }
144 let answers = self.copies.inbox.drain().answers;
145 if answers.is_empty() {
146 break;
147 }
148 for (_, answer) in answers {
149 if let Some(request) = self.copies.sent.take() {
150 self.copy_answered(request, answer);
151 }
152 }
153 }
154 }
155
156 fn copy_answered(&mut self, request: CopyRequest, answer: Box<dyn std::any::Any + Send>) {
158 match request {
159 CopyRequest::List { symbol } => {
160 let answer = *answer
161 .downcast::<Result<Vec<String>, FeedError>>()
162 .expect("copy list");
163 let mut copies: Vec<_> = match answer {
164 Ok(names) => names
165 .into_iter()
166 .filter_map(|name| Some((copy_number(&symbol, &name)?, name)))
167 .collect(),
168 Err(error) => {
169 self.error = Some(StorageError::Storage(message(
170 self.ctx.as_ref(),
171 "Could not list the kept copies of {symbol} drawings: {error}",
172 &[
173 ("symbol", symbol.as_str()),
174 ("error", error.to_string().as_str()),
175 ],
176 )));
177 Vec::new()
178 }
179 };
180 copies.sort_unstable();
181 let names = copies.into_iter().map(|(_, name)| name).collect();
182 self.copies.listed = Some((symbol, names));
183 }
184 CopyRequest::Read {
185 symbol,
186 name,
187 discard,
188 } => {
189 let answer = *answer
190 .downcast::<Result<Option<String>, FeedError>>()
191 .expect("copy read");
192 match answer {
193 Ok(None) => {
194 self.copies.forget(&symbol, &name);
195 if !discard {
196 self.error = Some(StorageError::Storage(message(
197 self.ctx.as_ref(),
198 "{name} no longer exists",
199 &[("name", name.as_str())],
200 )));
201 }
202 }
203 Ok(Some(json)) if !discard => self.copies.read = Some((symbol, name, json)),
204 Ok(Some(json)) if holds_drawings_of(&json, &symbol) => {
205 self.copies
206 .queued
207 .push_front(CopyRequest::Delete { symbol, name });
208 }
209 Ok(Some(_)) => {
210 self.error = Some(StorageError::Storage(message(
211 self.ctx.as_ref(),
212 "{name} was not discarded: it does not hold drawings of {symbol}",
213 &[("name", name.as_str()), ("symbol", symbol.as_str())],
214 )));
215 }
216 Err(error) if discard => {
217 self.error = Some(StorageError::Storage(message(
218 self.ctx.as_ref(),
219 "Could not discard {name}: {error}",
220 &[
221 ("name", name.as_str()),
222 ("error", error.to_string().as_str()),
223 ],
224 )));
225 }
226 Err(error) => {
227 self.error = Some(StorageError::Storage(message(
228 self.ctx.as_ref(),
229 "Could not read {name}: {error}",
230 &[
231 ("name", name.as_str()),
232 ("error", error.to_string().as_str()),
233 ],
234 )))
235 }
236 }
237 }
238 CopyRequest::Delete { symbol, name } => {
239 let answer = *answer
240 .downcast::<Result<(), FeedError>>()
241 .expect("copy delete");
242 match answer {
243 Ok(()) => self.copies.forget(&symbol, &name),
244 Err(error) => {
245 self.error = Some(StorageError::Storage(message(
246 self.ctx.as_ref(),
247 "Could not discard {name}: {error}",
248 &[
249 ("name", name.as_str()),
250 ("error", error.to_string().as_str()),
251 ],
252 )))
253 }
254 }
255 }
256 }
257 }
258}
259
260#[cfg(feature = "ui")]
261impl DrawingStore {
262 pub(super) fn ready(&self, symbol: &str) -> bool {
263 self.pending.get(symbol).is_some_and(|(_, merge)| !merge)
264 || self
265 .documents
266 .get(symbol)
267 .is_some_and(|document| !document.reading())
268 }
269}
270
271fn open_drawings(storage: &dyn Storage, symbol: &str) -> StoredDocument {
273 let mut document = StoredDocument::open(storage, StoreKind::Drawings, symbol);
274 document.follow_changes(storage);
275 document
276}
277
278fn holds_drawings_of(json: &str, symbol: &str) -> bool {
281 serde_json::from_str::<serde_json::Value>(json).is_ok_and(|document| {
282 document.get("symbol").and_then(serde_json::Value::as_str) == Some(symbol)
283 })
284}
285
286impl Chart {
287 pub fn set_storage(&mut self, storage: Arc<dyn Storage>) {
314 self.drawings.set_sync_enabled(false);
315 for (manager, _) in self.symbol_drawings.values_mut() {
316 manager.set_sync_enabled(false);
317 }
318 self.drawing_store = Some(DrawingStore {
319 sync: Self::poll_stored_drawings,
320 save: Self::write_stored_drawings,
321 storage,
322 documents: HashMap::new(),
323 loaded: HashMap::new(),
324 captured: self.history.drawing_edits,
325 pending: HashMap::new(),
326 error: None,
327 ctx: None,
328 changed: HashMap::new(),
329 unshown_panes: HashMap::new(),
330 copies: LocalCopies::default(),
331 });
332 }
333
334 pub fn flush_stored_drawings(&mut self) -> bool {
341 if let Some(sync) = self.drawing_store.as_ref().map(|store| store.sync) {
342 sync(self, None);
343 sync(self, None);
345 }
346 self.drawing_store.as_ref().is_none_or(|store| {
347 store.pending.is_empty()
348 && store.captured == self.history.drawing_edits
349 && store.documents.values().all(StoredDocument::flushed)
350 })
351 }
352
353 pub fn take_storage_error(&mut self) -> Option<StorageError> {
359 self.drawing_store.as_mut()?.error.take()
360 }
361
362 pub fn storage_conflict(&self) -> Option<&str> {
368 let store = self.drawing_store.as_ref()?;
369 let waiting = |document: &StoredDocument| document.conflicted() && !document.copying();
370 if store.documents.get(&self.symbol).is_some_and(waiting) {
371 return Some(self.symbol.as_str());
372 }
373 store
374 .documents
375 .iter()
376 .filter(|(_, document)| waiting(document))
377 .map(|(name, _)| name.as_str())
378 .min()
379 }
380
381 pub fn resolve_storage_conflict(&mut self, symbol: &str, choice: ConflictChoice) {
388 if let Some(store) = &mut self.drawing_store
389 && let Some(document) = store.documents.get_mut(symbol)
390 {
391 document.resolve(&*store.storage, choice);
392 }
393 }
394
395 pub fn list_local_copies(&mut self) {
402 if let Some(store) = &mut self.drawing_store
403 && !self.symbol.is_empty()
404 {
405 let symbol = self.symbol.clone();
406 store.copies.queued.push_back(CopyRequest::List { symbol });
407 }
408 }
409
410 pub fn local_copies(&self) -> Option<&[String]> {
415 let (symbol, names) = self.drawing_store.as_ref()?.copies.listed.as_ref()?;
416 (*symbol == self.symbol).then_some(names.as_slice())
417 }
418
419 pub fn load_local_copy(&mut self, name: &str) {
428 self.request_local_copy(name, false);
429 }
430
431 pub fn discard_local_copy(&mut self, name: &str) {
437 self.request_local_copy(name, true);
438 }
439
440 fn request_local_copy(&mut self, name: &str, discard: bool) {
441 if !self
442 .local_copies()
443 .is_some_and(|names| names.iter().any(|listed| listed == name))
444 {
445 return;
446 }
447 let symbol = self.symbol.clone();
448 let store = self.drawing_store.as_mut().expect("listed copies");
449 let name = name.to_owned();
450 store.copies.queued.push_back(CopyRequest::Read {
451 symbol,
452 name,
453 discard,
454 });
455 }
456
457 fn poll_local_copies(&mut self, ctx: Option<&egui::Context>, busy: bool) {
460 let Some(store) = &mut self.drawing_store else {
461 return;
462 };
463 if let Some(ctx) = ctx {
464 let repaint = ctx.clone();
465 store
466 .copies
467 .inbox
468 .set_waker(move || repaint.request_repaint());
469 }
470 store.receive_copies();
471 let ready = !busy
472 && store
473 .documents
474 .get(&self.symbol)
475 .is_some_and(|document| !document.reading());
476 let Some((symbol, name, json)) = store
477 .copies
478 .read
479 .take_if(|(symbol, ..)| *symbol != self.symbol || ready)
480 else {
481 return;
482 };
483 let loaded = if symbol == self.symbol {
484 self.apply_local_copy(&name, &json)
485 } else {
486 Err(StorageError::Storage(message(
487 store.ctx.as_ref(),
488 "{name} was not loaded because {symbol} is no longer shown",
489 &[("name", name.as_str()), ("symbol", symbol.as_str())],
490 )))
491 };
492 if let Err(error) = loaded {
493 self.drawing_store.as_mut().expect("drawing storage").error = Some(error);
494 }
495 }
496
497 fn apply_local_copy(&mut self, name: &str, json: &str) -> Result<(), StorageError> {
500 let copy = DrawingStorage::from_json(json)
501 .and_then(|(copy, skipped)| {
502 if copy.symbol != self.symbol {
503 Err(LoadError::OtherSymbol)
504 } else if !skipped.is_empty() {
505 Err(LoadError::Skipped(skipped))
506 } else {
507 Ok(copy)
508 }
509 })
510 .map_err(|error| StorageError::Read {
511 symbol: name.into(),
512 error,
513 })?;
514 let mut unshown = HashMap::new();
516 let drawings: Vec<_> = copy
517 .drawings()
518 .iter()
519 .map(|record| {
520 let drawing = self.restored_drawing(record, true);
521 if drawing.pane.is_none()
522 && let Some(pane) = &record.pane
523 {
524 unshown.insert(record.id.clone(), (record.created_at, pane.clone()));
525 }
526 drawing
527 })
528 .collect();
529 if !unshown.is_empty() {
530 let store = self.drawing_store.as_mut().expect("drawing storage");
531 let panes = store.unshown_panes.entry(self.symbol.clone()).or_default();
532 panes.extend(unshown);
533 }
534 self.user_action(|chart| chart.drawings.replace_saved_drawings(drawings));
535 Ok(())
536 }
537
538 pub(super) fn shares_stored_drawings(&self, other: &Chart) -> bool {
540 match (&self.drawing_store, &other.drawing_store) {
541 (Some(own), Some(other)) => Arc::ptr_eq(&own.storage, &other.storage),
542 _ => false,
543 }
544 }
545
546 pub(super) fn keeps_stored_drawings(&self) -> bool {
548 self.drawing_store.is_some()
549 }
550
551 pub(super) fn share_stored_drawings(charts: &mut [&mut Chart]) -> bool {
555 let mut shared = false;
556 for at in 0..charts.len() {
557 let Some(store) = &mut charts[at].drawing_store else {
558 continue;
559 };
560 if store.changed.is_empty() {
561 continue;
562 }
563 let changed = std::mem::take(&mut store.changed);
564 let storage = Arc::clone(&store.storage);
565 for (other, chart) in charts.iter_mut().enumerate() {
566 let Some(store) = chart
567 .drawing_store
568 .as_mut()
569 .filter(|store| other != at && Arc::ptr_eq(&store.storage, &storage))
570 else {
571 continue;
572 };
573 for (name, revision) in &changed {
574 if let Some(document) = store.documents.get_mut(name) {
575 document.changed_elsewhere(revision.clone());
576 shared = true;
577 }
578 }
579 }
580 }
581 shared
582 }
583
584 pub(super) fn sync_stored_drawings(&mut self, ctx: &egui::Context) {
586 if let Some(store) = &self.drawing_store {
587 (store.sync)(self, Some(ctx));
588 }
589 }
590
591 fn poll_stored_drawings(&mut self, ctx: Option<&egui::Context>) {
592 if let Some(ctx) = ctx
593 && let Some(store) = &mut self.drawing_store
594 {
595 store.ctx = Some(ctx.clone());
596 }
597 self.save_stored_drawings();
598 let symbol = self.symbol.clone();
599 let busy = ctx.is_some_and(|ctx| ctx.input(|input| input.pointer.any_down()))
601 || self.drawings.curr_drawing.is_some()
602 || self.drawings.measurement.is_some()
603 || self.drawings.text_editor.is_some();
604 let Some(store) = &mut self.drawing_store else {
605 return;
606 };
607 if !symbol.is_empty() && !store.documents.contains_key(&symbol) {
608 let document = open_drawings(&*store.storage, &symbol);
609 store.documents.insert(symbol.clone(), document);
610 }
611 for (name, document) in &mut store.documents {
612 document.hold(busy && name == &symbol);
613 let mut first = document.reading();
614 let received = match ctx {
615 Some(ctx) => document.poll(&*store.storage, ctx),
616 None => document.flush(&*store.storage),
617 };
618 for received in received {
619 match received {
620 Received::Loaded(json) => {
621 let later = !std::mem::replace(&mut first, false);
622 if later && let Some(revision) = document.revision() {
623 store.changed.insert(name.clone(), Some(revision.clone()));
624 }
625 let json = json.or_else(|| {
627 later
628 .then(|| DrawingStorage::new(name.clone()).to_json().ok())
629 .flatten()
630 });
631 let read = json.as_deref().map(DrawingStorage::from_json).transpose();
632 match read {
633 Ok(Some((drawings, _))) if drawings.symbol != *name => {
634 document.refuse_writes();
635 store.error = Some(StorageError::Read {
636 symbol: name.clone(),
637 error: LoadError::OtherSymbol,
638 });
639 }
640 Ok(Some((_, skipped))) if !skipped.is_empty() => {
641 document.refuse_writes();
642 store.error = Some(StorageError::Read {
643 symbol: name.clone(),
644 error: LoadError::Skipped(skipped),
645 });
646 }
647 Err(error) => {
648 document.refuse_writes();
649 store.error = Some(StorageError::Read {
650 symbol: name.clone(),
651 error,
652 });
653 }
654 Ok(loaded) => {
655 if let Some((mut pending, merge)) = store.pending.remove(name) {
656 let merged = merge
657 && loaded.as_ref().is_some_and(|(drawings, _)| {
658 !drawings.drawings().is_empty()
659 });
660 if merge && let Some((mut drawings, _)) = loaded {
661 for mut drawing in pending.drawings().iter().cloned() {
662 while drawings.get_drawing(&drawing.id).is_some() {
664 drawing.id.push_str("-local");
665 }
666 drawings.add_drawing(drawing);
667 }
668 pending = drawings;
669 }
670 match pending.to_json() {
671 Ok(json) => {
672 document.save(&*store.storage, json.clone());
673 if merged {
675 store.loaded.insert(name.clone(), json);
676 }
677 }
678 Err(error) => {
679 store.error = Some(StorageError::Save {
680 symbol: name.clone(),
681 error,
682 })
683 }
684 }
685 } else if let Some(json) = json
686 && (name == &symbol
687 || self.symbol_drawings.contains_key(name)
688 || loaded.is_some_and(|(drawings, _)| {
689 !drawings.drawings().is_empty()
690 }))
691 {
692 store.loaded.insert(name.clone(), json);
693 }
694 }
695 }
696 }
697 Received::Failed(error) => store.error = Some(StorageError::Storage(error)),
698 Received::Conflict => {}
700 Received::Saved(_) => {
701 store
702 .changed
703 .insert(name.clone(), document.revision().cloned());
704 }
705 }
706 }
707 }
708 store.documents.retain(|name, document| {
709 name == &symbol
710 || self.symbol_drawings.contains_key(name)
711 || store.loaded.contains_key(name)
712 || store.pending.contains_key(name)
713 || document.saving()
714 });
715 if let Some(json) = store.loaded.remove(&symbol) {
716 self.restore_stored_drawings(&json);
717 }
718 self.poll_local_copies(ctx, busy);
719 self.save_stored_drawings();
720 }
721
722 fn restore_stored_drawings(&mut self, json: &str) {
724 let previous = self.drawings.before_read();
725 let loaded = self.load_drawings_remapped(json, true);
726 if loaded.is_ok() {
727 self.drawings.keep_across_read(previous);
728 }
729 let refused = match loaded {
730 Ok(skipped) if skipped.is_empty() => None,
731 Ok(skipped) => Some(LoadError::Skipped(skipped)),
732 Err(error) => Some(error),
733 };
734 let unshown: HashMap<_, _> = DrawingStorage::from_json(json)
736 .map(|(stored, _)| {
737 self.drawings
738 .drawings
739 .iter()
740 .filter(|drawing| drawing.pane.is_none())
741 .filter_map(|drawing| {
742 let id = drawing.stored_id.as_ref()?;
743 let record = stored.get_drawing(id)?;
744 Some((id.clone(), (record.created_at, record.pane.clone()?)))
745 })
746 .collect()
747 })
748 .unwrap_or_default();
749 let store = self.drawing_store.as_mut().expect("drawing storage");
750 if unshown.is_empty() {
751 store.unshown_panes.remove(&self.symbol);
752 } else {
753 store.unshown_panes.insert(self.symbol.clone(), unshown);
754 }
755 if let Some(error) = refused {
756 if let Some(document) = store.documents.get_mut(&self.symbol) {
757 document.refuse_writes();
758 }
759 store.error = Some(StorageError::Read {
760 symbol: self.symbol.clone(),
761 error,
762 });
763 }
764 }
765
766 pub(super) fn save_stored_drawings(&mut self) {
768 if let Some(store) = &self.drawing_store {
769 (store.save)(self);
770 }
771 }
772
773 fn write_stored_drawings(&mut self) {
774 let edits = self.history.drawing_edits;
775 if self.symbol.is_empty()
776 || self
777 .drawing_store
778 .as_ref()
779 .is_none_or(|store| store.captured == edits)
780 {
781 return;
782 }
783 let mut drawings = self.save_drawings();
784 let store = self.drawing_store.as_mut().expect("drawing storage");
785 if let (Ok(drawings), Some(panes)) = (&mut drawings, store.unshown_panes.get(&self.symbol))
788 {
789 for record in &mut drawings.drawings {
790 if record.pane.is_none()
791 && let Some((created_at, pane)) = panes.get(&record.id)
792 && *created_at == record.created_at
793 {
794 record.pane = Some(pane.clone());
795 }
796 }
797 }
798 let document = store
799 .documents
800 .entry(self.symbol.clone())
801 .or_insert_with(|| open_drawings(&*store.storage, &self.symbol));
802 match drawings {
803 Ok(drawings) if document.reading() => {
804 let merge = store
805 .pending
806 .get(&self.symbol)
807 .is_none_or(|(_, merge)| *merge);
808 store.pending.insert(self.symbol.clone(), (drawings, merge));
809 store.captured = edits;
810 }
811 Ok(drawings) => match drawings.to_json() {
812 Ok(json) => {
813 document.save(&*store.storage, json);
814 store.captured = edits;
815 }
816 Err(error) => {
817 store.error = Some(StorageError::Save {
818 symbol: self.symbol.clone(),
819 error,
820 })
821 }
822 },
823 Err(error) => {
824 store.error = Some(StorageError::Save {
825 symbol: self.symbol.clone(),
826 error,
827 })
828 }
829 }
830 }
831
832 #[cfg(feature = "ui")]
833 pub(super) fn save_restored_drawings(&mut self) {
834 self.save_stored_drawings();
835 if let Some(store) = &mut self.drawing_store {
836 store.loaded.remove(&self.symbol);
837 if let Some((_, merge)) = store.pending.get_mut(&self.symbol) {
838 *merge = false;
839 }
840 }
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847 use crate::data::{FeedError, Reply};
848 use crate::drawings::{ChartPoint, Drawing, DrawingToolType};
849 use crate::model::{Bar, BarData};
850 use crate::widget::{ChartLayout, ChartLayoutMode};
851 use chrono::{TimeZone, Utc};
852 use egui::{Pos2, RawInput, Rect, vec2};
853 use std::sync::Mutex;
854 use std::sync::atomic::{AtomicBool, Ordering};
855
856 #[derive(Default)]
857 struct Host {
858 documents: Mutex<HashMap<String, String>>,
859 fail_writes: AtomicBool,
860 delay_loads: AtomicBool,
861 fail_loads: AtomicBool,
862 loads: Mutex<Vec<(String, Reply<Option<String>>)>>,
863 delay_writes: AtomicBool,
864 writes: Mutex<Vec<Reply<()>>>,
865 }
866
867 impl Storage for Host {
868 fn list(&self, _: StoreKind, reply: Reply<Vec<String>>) {
869 reply.send(Ok(Vec::new()));
870 }
871 fn load(&self, kind: StoreKind, name: &str, reply: Reply<Option<String>>) {
872 assert_eq!(kind, StoreKind::Drawings);
873 if self.fail_loads.load(Ordering::Relaxed) {
874 return reply.send(Err(FeedError::Network("offline".into())));
875 }
876 if self.delay_loads.load(Ordering::Relaxed) {
877 self.loads.lock().unwrap().push((name.into(), reply));
878 } else {
879 reply.send(Ok(self.documents.lock().unwrap().get(name).cloned()));
880 }
881 }
882 fn save(&self, _: StoreKind, name: &str, document: String, reply: Reply<()>) {
883 if self.fail_writes.load(Ordering::Relaxed) {
884 return reply.send(Err(FeedError::NotSupported));
885 }
886 self.documents.lock().unwrap().insert(name.into(), document);
887 if self.delay_writes.load(Ordering::Relaxed) {
888 self.writes.lock().unwrap().push(reply);
889 } else {
890 reply.send(Ok(()));
891 }
892 }
893 fn delete(&self, _: StoreKind, _: &str, reply: Reply<()>) {
894 reply.send(Ok(()));
895 }
896 }
897
898 #[test]
899 fn automatic_storage_does_not_fill_the_notification_queue() {
900 let host = Arc::new(Host::default());
901 let mut chart = Chart::new(BarData::from_bars(vec![Bar::new(
902 Utc.timestamp_opt(0, 0).unwrap(),
903 100.0,
904 101.0,
905 99.0,
906 100.0,
907 1.0,
908 )]));
909 chart.set_symbol("A");
910 chart.set_storage(host.clone());
911 let ctx = egui::Context::default();
912 chart.sync_stored_drawings(&ctx);
913 let mut line = Drawing::new(0, DrawingToolType::HorizontalLine);
914 line.chart_points = vec![ChartPoint::new(0.0, 100.0)];
915 let id = chart.user_action(|chart| chart.drawings_mut().add_drawing(line).unwrap());
916 for i in 0..10_000 {
917 chart.user_action(|chart| {
918 chart.drawings_mut().edit_drawing(id, |drawing| {
919 drawing.chart_points[0].price = 100.0 + (i % 2) as f64;
920 })
921 });
922 assert!(chart.drawings().pending_sync.lock().unwrap().is_empty());
923 if i % 100 == 0 {
924 chart.sync_stored_drawings(&ctx);
925 }
926 }
927 assert!(chart.flush_stored_drawings());
928 let json = host.documents.lock().unwrap()["A"].clone();
929 assert_eq!(
930 DrawingStorage::from_json(&json).unwrap().0.drawings()[0].points[0].price,
931 101.0
932 );
933 for symbol in ["B", "A"] {
934 chart.set_symbol(symbol);
935 assert!(!chart.drawings.sync_enabled);
936 chart.sync_stored_drawings(&ctx);
937 assert!(
938 !chart.drawings.sync_enabled,
939 "loading keeps notifications disabled"
940 );
941 }
942 chart.drawings_mut().set_sync_enabled(true);
943 for symbol in ["B", "A"] {
944 chart.set_symbol(symbol);
945 assert!(chart.drawings.sync_enabled);
946 chart.sync_stored_drawings(&ctx);
947 assert!(
948 chart.drawings.sync_enabled,
949 "loading preserves the host's opt-in"
950 );
951 }
952 }
953
954 #[test]
955 fn symbol_drawings_come_back_and_storage_failures_lose_nothing() {
956 let ctx = egui::Context::default();
957 let frame = |chart: &mut Chart| {
958 ctx.run_ui(Default::default(), |ui| {
959 chart.show(ui);
960 })
961 .drop_without_applying_deltas();
962 };
963 let new_chart = |host: &Arc<Host>| {
964 let bars = (0..20)
965 .map(|i| {
966 Bar::new(
967 Utc.timestamp_opt(i * 60, 0).unwrap(),
968 10.0,
969 11.0,
970 9.0,
971 10.0,
972 1.0,
973 )
974 })
975 .collect();
976 let mut chart = Chart::new(BarData::from_bars(bars));
977 chart.set_symbol("A");
978 chart.set_storage(host.clone());
979 chart
980 };
981 let draw = |chart: &mut Chart| {
982 let mut drawing = Drawing::new(0, DrawingToolType::HorizontalLine);
983 drawing.chart_points = vec![ChartPoint::new(4.0, 10.5)];
984 drawing.completed = true;
985 chart.user_action(|chart| chart.drawings_mut().add_synced_drawing(drawing).unwrap());
986 };
987 let host = Arc::new(Host::default());
988 host.documents
989 .lock()
990 .unwrap()
991 .insert("B".into(), "{\"version\": 99}".into());
992 let mut scanner = new_chart(&host);
993 for i in 0..16 {
994 let symbol = format!("EMPTY-{i}");
995 if i % 2 == 0 {
996 host.documents.lock().unwrap().insert(
997 symbol.clone(),
998 DrawingStorage::new(&symbol).to_json().unwrap(),
999 );
1000 }
1001 scanner.set_symbol(symbol);
1002 frame(&mut scanner);
1003 }
1004 assert_eq!(
1005 scanner.drawing_store.as_ref().unwrap().documents.len(),
1006 1,
1007 "empty scans must retain only the active storage reader"
1008 );
1009 let mut chart = new_chart(&host);
1010 frame(&mut chart);
1011 draw(&mut chart);
1012 frame(&mut chart);
1013 assert!(host.documents.lock().unwrap().contains_key("A"));
1014
1015 let mut transient = Drawing::new(0, DrawingToolType::HorizontalLine);
1016 transient.chart_points = vec![ChartPoint::new(4.0, 10.75)];
1017 transient.saveable = false;
1018 chart.user_action(|chart| chart.drawings_mut().add_drawing(transient).unwrap());
1019 for replay in 0..3 {
1020 match replay {
1021 1 => assert!(chart.undo()),
1022 2 => assert!(chart.redo()),
1023 _ => {}
1024 }
1025 frame(&mut chart);
1026 let json = host.documents.lock().unwrap()["A"].clone();
1027 let (saved, skipped) = DrawingStorage::from_json(&json).unwrap();
1028 assert!(skipped.is_empty());
1029 assert_eq!(saved.drawings().len(), 1, "transient drawing was saved");
1030 }
1031
1032 chart.set_symbol("away");
1033 assert!(chart.forget_symbol("A"));
1034 assert!(
1035 !chart
1036 .drawing_store
1037 .as_ref()
1038 .unwrap()
1039 .documents
1040 .contains_key("A")
1041 );
1042 assert!(host.documents.lock().unwrap().contains_key("A"));
1043 chart.set_symbol("A");
1044 frame(&mut chart);
1045 assert_eq!(
1046 chart.drawings().drawings.len(),
1047 1,
1048 "forgotten state reloads storage"
1049 );
1050
1051 chart.set_symbol("SAVING");
1053 frame(&mut chart);
1054 host.delay_writes.store(true, Ordering::Relaxed);
1055 draw(&mut chart);
1056 frame(&mut chart);
1057 chart.user_action(|chart| chart.drawings_mut().clear_all());
1058 chart.set_symbol("away");
1059 assert!(
1060 !chart.flush_stored_drawings(),
1061 "a delayed write is not acknowledged"
1062 );
1063 assert!(!chart.forget_symbol("SAVING"));
1064 for _ in 0..2 {
1065 for reply in std::mem::take(&mut *host.writes.lock().unwrap()) {
1066 reply.send(Ok(()));
1067 }
1068 frame(&mut chart);
1069 }
1070 assert!(chart.flush_stored_drawings());
1071 assert!(chart.forget_symbol("SAVING"));
1072 let (saved, skipped) =
1073 DrawingStorage::from_json(&host.documents.lock().unwrap()["SAVING"]).unwrap();
1074 assert!(saved.drawings().is_empty());
1075 assert!(skipped.is_empty());
1076 host.delay_writes.store(false, Ordering::Relaxed);
1077
1078 let mut restarted = new_chart(&host);
1079 frame(&mut restarted);
1080 let restored = &restarted.drawings().drawings;
1081 assert_eq!(restored.len(), 1);
1082 assert_eq!(restored[0].chart_points[0].bar_idx, 4.0);
1083 assert_eq!(restored[0].chart_points[0].price, 10.5);
1084
1085 restarted.set_symbol("B");
1087 frame(&mut restarted);
1088 assert!(
1089 matches!(restarted.take_storage_error(), Some(StorageError::Read {
1090 symbol, error: LoadError::UnsupportedVersion { found: 99, .. },
1091 }) if symbol == "B")
1092 );
1093 draw(&mut restarted);
1094 frame(&mut restarted);
1095 assert_eq!(host.documents.lock().unwrap()["B"], "{\"version\": 99}");
1096
1097 host.fail_writes.store(true, Ordering::Relaxed);
1099 restarted.set_symbol("C");
1100 frame(&mut restarted);
1101 for _ in 0..2 {
1102 draw(&mut restarted);
1103 frame(&mut restarted);
1104 frame(&mut restarted);
1105 }
1106 assert!(matches!(
1107 restarted.take_storage_error(),
1108 Some(StorageError::Storage(_))
1109 ));
1110 draw(&mut restarted);
1111 frame(&mut restarted);
1112 frame(&mut restarted);
1113 assert!(restarted.take_storage_error().is_none());
1114 assert_eq!(restarted.drawings().drawings.len(), 3);
1115
1116 assert!(restarted.drawing_store.as_ref().unwrap().documents["C"].saving());
1118 host.fail_writes.store(false, Ordering::Relaxed);
1119 ctx.run_ui(
1120 egui::RawInput {
1121 time: Some(ctx.input(|input| input.time) + 2.0),
1122 ..Default::default()
1123 },
1124 |ui| {
1125 restarted.show(ui);
1126 },
1127 )
1128 .drop_without_applying_deltas();
1129 let json = host.documents.lock().unwrap()["C"].clone();
1130 let (saved, skipped) = DrawingStorage::from_json(&json).unwrap();
1131 assert!(skipped.is_empty());
1132 assert_eq!(saved.drawings().len(), 3);
1133 assert!(!restarted.drawing_store.as_ref().unwrap().documents["C"].saving());
1134
1135 host.fail_writes.store(true, Ordering::Relaxed);
1137 draw(&mut restarted);
1138 assert!(!restarted.flush_stored_drawings());
1139 host.fail_writes.store(false, Ordering::Relaxed);
1140 assert!(
1141 !restarted.flush_stored_drawings(),
1142 "unreadable B still prevents a complete flush"
1143 );
1144 assert!(!restarted.drawing_store.as_ref().unwrap().documents["C"].saving());
1145 let json = host.documents.lock().unwrap()["C"].clone();
1146 assert_eq!(
1147 DrawingStorage::from_json(&json).unwrap().0.drawings().len(),
1148 4
1149 );
1150
1151 host.fail_writes.store(false, Ordering::Relaxed);
1153 host.delay_loads.store(true, Ordering::Relaxed);
1154 let mut delayed = new_chart(&host);
1155 frame(&mut delayed);
1156 #[cfg(feature = "ui")]
1157 assert!(
1158 delayed.snapshot().is_err(),
1159 "a layout must not capture drawings before their read completes"
1160 );
1161 draw(&mut delayed);
1162 delayed.set_symbol("D");
1163 assert!(
1164 !delayed.forget_symbol("A"),
1165 "pending edits must finish loading"
1166 );
1167 frame(&mut delayed);
1168 for (symbol, reply) in std::mem::take(&mut *host.loads.lock().unwrap()) {
1169 reply.send(Ok(host.documents.lock().unwrap().get(&symbol).cloned()));
1170 }
1171 frame(&mut delayed);
1172 assert!(delayed.drawings().drawings.is_empty());
1173 let json = host.documents.lock().unwrap()["A"].clone();
1174 let (saved, skipped) = DrawingStorage::from_json(&json).unwrap();
1175 assert!(skipped.is_empty());
1176 assert_eq!(saved.drawings().len(), 2);
1177 delayed.set_symbol("A");
1178 frame(&mut delayed);
1179 assert_eq!(delayed.drawings().drawings.len(), 2);
1180 let bars = delayed.bars(super::super::SeriesId::MAIN)[2..].to_vec();
1182 delayed.set_data(super::super::SeriesId::MAIN, bars);
1183 for drawing in &delayed.drawings().drawings {
1184 assert_eq!(drawing.chart_points[0].bar_idx, 2.0);
1185 assert_eq!(drawing.chart_points[0].price, 10.5);
1186 }
1187 }
1188
1189 fn line_at(bar: f32, price: f64) -> Drawing {
1190 let mut line = Drawing::new(0, DrawingToolType::HorizontalLine);
1191 line.chart_points = vec![ChartPoint::new(bar, price)];
1192 line.completed = true;
1193 line
1194 }
1195
1196 fn draw_line(chart: &mut Chart, line: Drawing) -> usize {
1197 chart.user_action(|chart| chart.drawings_mut().add_drawing(line).unwrap())
1198 }
1199
1200 fn sorted(mut prices: Vec<f64>) -> Vec<f64> {
1201 prices.sort_by(f64::total_cmp);
1202 prices
1203 }
1204
1205 fn shown_prices(chart: &Chart) -> Vec<f64> {
1206 let drawings = &chart.drawings().drawings;
1207 sorted(drawings.iter().map(|d| d.chart_points[0].price).collect())
1208 }
1209
1210 fn chart_on(storage: Option<Arc<dyn Storage>>) -> Chart {
1212 let bars = (0..10)
1213 .map(|i| {
1214 Bar::new(
1215 Utc.timestamp_opt(i * 60, 0).unwrap(),
1216 10.0,
1217 11.0,
1218 9.0,
1219 10.0,
1220 1.0,
1221 )
1222 })
1223 .collect();
1224 let mut chart = Chart::new(BarData::from_bars(bars));
1225 chart.set_symbol("A");
1226 if let Some(storage) = storage {
1227 chart.set_storage(storage);
1228 }
1229 chart
1230 }
1231
1232 fn show_frames(
1235 ctx: &egui::Context,
1236 layout: &mut ChartLayout,
1237 charts: &mut [&mut Chart],
1238 seconds: f64,
1239 ) {
1240 for _ in 0..3 {
1241 let input = RawInput {
1242 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(900.0, 600.0))),
1243 time: Some(ctx.input(|input| input.time) + seconds / 3.0),
1244 ..Default::default()
1245 };
1246 ctx.run_ui(input, |ui| {
1247 layout.show(ui, charts);
1248 })
1249 .drop_without_applying_deltas();
1250 }
1251 }
1252
1253 fn stored_prices(host: &Host) -> Vec<f64> {
1255 prices_in(&host.documents.lock().unwrap()["A"])
1256 }
1257
1258 fn prices_in(document: &str) -> Vec<f64> {
1260 let (stored, _) = DrawingStorage::from_json(document).unwrap();
1261 sorted(
1262 stored
1263 .drawings()
1264 .iter()
1265 .map(|d| d.points[0].price)
1266 .collect(),
1267 )
1268 }
1269
1270 #[test]
1274 fn a_refused_drawings_write_waits_for_the_users_choice() {
1275 use crate::services::document::tests::Revised;
1276 let stored = |host: &Revised, name: &str| {
1277 host.get(name).map(|json| {
1278 let (stored, _) = DrawingStorage::from_json(&json).unwrap();
1279 sorted(
1280 stored
1281 .drawings()
1282 .iter()
1283 .map(|d| d.points[0].price)
1284 .collect(),
1285 )
1286 })
1287 };
1288 let new_chart = |host: &Arc<Revised>| {
1289 let bar = Bar::new(Utc.timestamp_opt(0, 0).unwrap(), 10.0, 11.0, 9.0, 10.0, 1.0);
1290 let mut chart = Chart::new(BarData::from_bars(vec![bar]));
1291 chart.set_symbol("A");
1292 chart.set_storage(host.clone());
1293 assert!(chart.flush_stored_drawings());
1294 chart
1295 };
1296 for (choice, deleted, expected, copy, shown) in [
1299 (
1300 ConflictChoice::ReloadRemote,
1301 false,
1302 Some(vec![10.0, 10.5]),
1303 None,
1304 vec![10.0, 10.5],
1305 ),
1306 (
1307 ConflictChoice::KeepLocalCopy,
1308 false,
1309 Some(vec![10.0, 10.5]),
1310 Some(vec![10.0, 11.0]),
1311 vec![10.0, 10.5],
1312 ),
1313 (
1314 ConflictChoice::Retry,
1315 false,
1316 Some(vec![10.0, 11.0]),
1317 None,
1318 vec![10.0, 11.0],
1319 ),
1320 (ConflictChoice::ReloadRemote, true, None, None, vec![]),
1321 ] {
1322 let host = Arc::new(Revised::default());
1323 let mut theirs = new_chart(&host);
1324 draw_line(&mut theirs, line_at(0.0, 10.0));
1325 assert!(theirs.flush_stored_drawings());
1326 let mut mine = new_chart(&host);
1327 assert_eq!(shown_prices(&mine), [10.0]);
1328 draw_line(&mut mine, line_at(0.0, 11.0));
1329 if deleted {
1330 host.remove("A");
1331 } else {
1332 draw_line(&mut theirs, line_at(0.0, 10.5));
1333 assert!(theirs.flush_stored_drawings());
1334 }
1335 assert!(!mine.flush_stored_drawings(), "{choice:?}");
1336 assert_eq!(mine.storage_conflict(), Some("A"));
1337 assert_eq!(shown_prices(&mine), [10.0, 11.0], "kept until the choice");
1338 mine.resolve_storage_conflict("A", choice);
1339 assert!(mine.flush_stored_drawings(), "{choice:?}");
1340 assert_eq!(mine.storage_conflict(), None);
1341 assert_eq!(stored(&host, "A"), expected, "{choice:?}");
1342 assert_eq!(stored(&host, "A (local copy)"), copy, "{choice:?}");
1343 assert_eq!(shown_prices(&mine), shown, "{choice:?}");
1344 if choice == ConflictChoice::Retry {
1346 theirs.drawings_mut().curr_drawing = Some(line_at(0.0, 12.0));
1347 theirs.flush_stored_drawings();
1348 assert_eq!(shown_prices(&theirs), [10.0, 10.5]);
1349 theirs.drawings_mut().curr_drawing = None;
1350 theirs.flush_stored_drawings();
1351 assert_eq!(shown_prices(&theirs), [10.0, 11.0]);
1352 }
1353 if choice == ConflictChoice::KeepLocalCopy {
1354 host.put("A (local copy 2)", &host.get("A").unwrap());
1356 let other = DrawingStorage::new("B").to_json().unwrap();
1357 for name in [
1358 "A (local copy 3)",
1359 "A (local copy 02)",
1360 "AB (local copy)",
1361 "A (local copy) (local copy)",
1362 ] {
1363 host.put(name, &other);
1364 }
1365 assert_eq!(mine.local_copies(), None);
1366 mine.list_local_copies();
1367 assert!(mine.flush_stored_drawings());
1368 let listed = |chart: &Chart| chart.local_copies().map(<[String]>::to_vec);
1369 assert_eq!(
1370 listed(&mine),
1371 Some(
1372 ["A (local copy)", "A (local copy 2)", "A (local copy 3)"]
1373 .map(String::from)
1374 .to_vec()
1375 )
1376 );
1377 assert!(mine.take_storage_error().is_none());
1378 mine.load_local_copy("A (local copy 3)");
1380 mine.discard_local_copy("A (local copy 3)");
1381 assert!(mine.flush_stored_drawings());
1382 assert!(mine.take_storage_error().is_some());
1383 assert!(host.get("A (local copy 3)").is_some());
1384 assert_eq!(shown_prices(&mine), [10.0, 10.5]);
1385 let mut overlay = line_at(0.0, 9.0);
1387 overlay.saveable = false;
1388 draw_line(&mut mine, overlay);
1389 mine.load_local_copy("A (local copy)");
1390 assert!(mine.flush_stored_drawings());
1391 assert_eq!(shown_prices(&mine), [9.0, 10.0, 11.0]);
1392 assert_eq!(stored(&host, "A"), Some(vec![10.0, 11.0]));
1393 assert!(mine.undo());
1394 assert!(mine.flush_stored_drawings());
1395 assert_eq!(shown_prices(&mine), [9.0, 10.0, 10.5]);
1396 assert_eq!(stored(&host, "A"), Some(vec![10.0, 10.5]));
1397 mine.discard_local_copy("A (local copy 02)");
1399 mine.discard_local_copy("A (local copy)");
1400 assert!(mine.flush_stored_drawings());
1401 assert_eq!(host.get("A (local copy)"), None);
1402 assert!(host.get("A (local copy 02)").is_some());
1403 assert_eq!(
1404 listed(&mine),
1405 Some(
1406 ["A (local copy 2)", "A (local copy 3)"]
1407 .map(String::from)
1408 .to_vec()
1409 )
1410 );
1411 }
1412 }
1413 }
1414
1415 #[cfg(feature = "studies")]
1419 #[test]
1420 fn charts_of_one_layout_follow_each_others_stored_drawings() {
1421 use crate::services::document::tests::Revised;
1422 use crate::studies::{CustomIndicator, IndicatorValue};
1423
1424 let unwatched = Revised::default();
1425 unwatched.2.store(true, Ordering::Relaxed);
1426 let hosts: [Arc<dyn Storage>; 2] = [Arc::new(Host::default()), Arc::new(unwatched)];
1427 for host in hosts {
1428 let new_chart = || {
1429 let bars = (0..10)
1430 .map(|i| {
1431 let time = Utc.timestamp_opt(i * 60, 0).unwrap();
1432 Bar::new(time, 10.0, 11.0, 9.0, 10.0, 1.0)
1433 })
1434 .collect();
1435 let mut chart = Chart::new(BarData::from_bars(bars));
1436 chart.set_symbol("A");
1437 chart.set_storage(host.clone());
1438 chart
1439 };
1440 let (mut a, mut b) = (new_chart(), new_chart());
1441 let gamma = a.add_indicator(Box::new(
1442 CustomIndicator::new("Gamma", |bars: &[Bar]| {
1443 vec![IndicatorValue::Single(40.0); bars.len()]
1444 })
1445 .with_overlay(false),
1446 ));
1447 let mut layout = ChartLayout::default();
1448 layout.mode = ChartLayoutMode::TwoHorizontal;
1449 layout.sync.sync_drawings = true;
1450 let ctx = egui::Context::default();
1451 let mut frames = |a: &mut Chart, b: &mut Chart| {
1453 for _ in 0..3 {
1454 let input = RawInput {
1455 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))),
1456 ..Default::default()
1457 };
1458 ctx.run_ui(input, |ui| {
1459 layout.show(ui, &mut [&mut *a, &mut *b]);
1460 })
1461 .drop_without_applying_deltas();
1462 }
1463 };
1464 let find = |chart: &Chart, price: f64| {
1465 let drawings = &chart.drawings().drawings;
1466 drawings
1467 .iter()
1468 .find(|d| d.chart_points[0].price == price)
1469 .cloned()
1470 .unwrap()
1471 };
1472 frames(&mut a, &mut b);
1473 let mut study_line = line_at(4.0, 40.0);
1474 study_line.pane = Some(gamma.0);
1475 draw_line(&mut a, study_line);
1476 frames(&mut a, &mut b);
1477 assert_eq!(shown_prices(&a), [40.0]);
1478 assert_eq!(shown_prices(&b), [40.0]);
1479
1480 draw_line(&mut b, line_at(4.0, 10.5));
1481 frames(&mut a, &mut b);
1482 assert_eq!(shown_prices(&a), [10.5, 40.0]);
1483 assert_eq!(shown_prices(&b), [10.5, 40.0]);
1484 assert_eq!(find(&a, 40.0).pane, Some(gamma.0));
1485 assert_eq!(find(&a, 10.5).pane, None);
1486
1487 #[cfg(feature = "ui")]
1489 {
1490 use crate::ui::dialogs::{DrawingId, DrawingProps};
1491 let open = |chart: &Chart, price: f64| {
1492 let drawing = find(chart, price);
1493 let props = DrawingProps {
1494 line_width: 3.5,
1495 ..DrawingProps::from_drawing(&drawing, chart)
1496 };
1497 (DrawingId(drawing.id), props)
1498 };
1499 let (removed_id, removed) = open(&a, 40.0);
1500 let (kept_id, kept) = open(&a, 10.5);
1501 let gone = find(&b, 40.0).id;
1502 b.user_action(|chart| {
1503 chart.drawings_mut().delete_drawing(gone);
1504 });
1505 draw_line(&mut b, line_at(4.0, 9.5));
1506 frames(&mut a, &mut b);
1507 assert_eq!(shown_prices(&a), [9.5, 10.5]);
1508 assert_eq!(find(&a, 10.5).id, kept_id.0);
1509 assert!(removed.apply_to_chart(removed_id, &mut a).is_err());
1510 let width = find(&a, 9.5).stroke_width;
1511 assert_eq!(find(&a, 10.5).stroke_width, width);
1512 kept.apply_to_chart(kept_id, &mut a).unwrap();
1513 assert_eq!(find(&a, 10.5).stroke_width, 3.5);
1514 assert_eq!(find(&a, 9.5).stroke_width, width);
1515 }
1516
1517 let mut volatile = line_at(4.0, 77.0);
1520 volatile.saveable = false;
1521 a.drawings_mut().add_synced_drawing(volatile).unwrap();
1522 frames(&mut a, &mut b);
1523 draw_line(&mut b, line_at(4.0, 8.5));
1524 frames(&mut a, &mut b);
1525 let count = |chart: &Chart, price: f64| {
1526 shown_prices(chart).iter().filter(|p| **p == price).count()
1527 };
1528 for chart in [&a, &b] {
1529 assert_eq!(count(chart, 8.5), 1);
1530 assert_eq!(count(chart, 77.0), 1);
1531 }
1532 }
1533 }
1534
1535 #[test]
1538 fn a_drawing_read_again_never_takes_the_id_of_one_that_is_not_saved() {
1539 let host = Arc::new(Host::default());
1540 let storage: Arc<dyn Storage> = host.clone();
1541 let (mut a, mut b) = (chart_on(Some(storage.clone())), chart_on(Some(storage)));
1542 let mut layout = ChartLayout::default();
1543 layout.mode = ChartLayoutMode::TwoHorizontal;
1544 let ctx = egui::Context::default();
1545 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1546 let mut marker = line_at(4.0, 77.0);
1548 marker.saveable = false;
1549 let marker = b.drawings_mut().add_drawing(marker).unwrap();
1550 draw_line(&mut a, line_at(4.0, 10.5));
1552 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1553 assert_eq!(shown_prices(&b), [10.5, 77.0]);
1554 b.drawings_mut().delete_drawing(marker);
1555 assert_eq!(shown_prices(&b), [10.5]);
1556 draw_line(&mut b, line_at(4.0, 9.0));
1557 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1558 assert_eq!(
1559 [shown_prices(&a), shown_prices(&b)],
1560 [[9.0, 10.5], [9.0, 10.5]]
1561 );
1562 assert_eq!(stored_prices(&host), [9.0, 10.5]);
1563 }
1564
1565 #[test]
1568 fn an_edit_made_while_the_drawings_are_read_again_waits_for_the_read() {
1569 let host = Arc::new(Host::default());
1570 let storage: Arc<dyn Storage> = host.clone();
1571 let (mut a, mut b) = (chart_on(Some(storage.clone())), chart_on(Some(storage)));
1572 let mut layout = ChartLayout::default();
1573 layout.mode = ChartLayoutMode::TwoHorizontal;
1574 let ctx = egui::Context::default();
1575 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1576 draw_line(&mut a, line_at(4.0, 10.0));
1577 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1578 assert_eq!(shown_prices(&b), [10.0]);
1579 host.delay_loads.store(true, Ordering::Relaxed);
1582 draw_line(&mut a, line_at(4.0, 12.0));
1583 assert!(a.undo());
1584 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1585 let (_, read) = host.loads.lock().unwrap().pop().expect("B reads again");
1586 let before = host.documents.lock().unwrap()["A"].clone();
1587 draw_line(&mut b, line_at(4.0, 11.0));
1588 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1589 assert_eq!(stored_prices(&host), [10.0], "written during the read");
1590 host.delay_loads.store(false, Ordering::Relaxed);
1591 read.send(Ok(Some(before)));
1592 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1593 assert_eq!(shown_prices(&b), [10.0, 11.0]);
1594 assert_eq!(stored_prices(&host), [10.0, 11.0]);
1595 assert!(b.can_undo(), "B read its own write again");
1596 }
1597
1598 #[test]
1601 fn a_failed_read_of_drawings_another_chart_wrote_is_reported_once_and_tried_again() {
1602 let host = Arc::new(Host::default());
1603 let storage: Arc<dyn Storage> = host.clone();
1604 let (mut a, mut b) = (chart_on(Some(storage.clone())), chart_on(Some(storage)));
1605 let mut layout = ChartLayout::default();
1606 layout.mode = ChartLayoutMode::TwoHorizontal;
1607 let ctx = egui::Context::default();
1608 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1609 host.fail_loads.store(true, Ordering::Relaxed);
1610 draw_line(&mut a, line_at(4.0, 10.0));
1611 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 3.0);
1613 assert_eq!(
1614 b.take_storage_error()
1615 .map(|error| error.to_string())
1616 .as_deref(),
1617 Some("Could not read A: network error: offline")
1618 );
1619 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 3.0);
1620 assert!(
1621 b.take_storage_error().is_none(),
1622 "a run of failed reads is reported once"
1623 );
1624 host.fail_loads.store(false, Ordering::Relaxed);
1625 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 3.0);
1626 assert_eq!(shown_prices(&b), [10.0]);
1627 draw_line(&mut b, line_at(4.0, 11.0));
1628 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1629 assert_eq!(stored_prices(&host), [10.0, 11.0]);
1630 host.fail_loads.store(true, Ordering::Relaxed);
1632 draw_line(&mut a, line_at(4.0, 12.0));
1633 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1634 assert!(b.take_storage_error().is_some());
1635 draw_line(&mut b, line_at(4.0, 13.0));
1636 draw_line(&mut a, line_at(4.0, 14.0));
1637 show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1638 assert!(b.take_storage_error().is_some(), "a new run is reported");
1639 }
1640
1641 #[test]
1646 fn a_chart_outside_the_storage_shows_each_drawing_once_and_keeps_its_own() {
1647 use crate::services::document::tests::Revised;
1648
1649 for revised in [false, true] {
1650 let host = Arc::new(Host::default());
1651 let watched = Arc::new(Revised::default());
1652 let storage: Arc<dyn Storage> = if revised {
1653 watched.clone()
1654 } else {
1655 host.clone()
1656 };
1657 let (mut a, mut b) = (chart_on(Some(storage.clone())), chart_on(Some(storage)));
1658 let mut c = chart_on(None);
1659 let mut layout = ChartLayout::default();
1660 layout.mode = ChartLayoutMode::Three;
1661 layout.sync.sync_drawings = true;
1662 let ctx = egui::Context::default();
1663 let mut show = |a: &mut Chart, b: &mut Chart, c: &mut Chart| {
1664 show_frames(&ctx, &mut layout, &mut [a, b, c], 0.1);
1665 };
1666 show(&mut a, &mut b, &mut c);
1667 draw_line(&mut a, line_at(4.0, 10.5));
1668 show(&mut a, &mut b, &mut c);
1669 assert_eq!([shown_prices(&b), shown_prices(&c)], [[10.5], [10.5]]);
1670 let own = draw_line(&mut c, line_at(4.0, 9.0));
1671 show(&mut a, &mut b, &mut c);
1672 draw_line(&mut a, line_at(4.0, 8.0));
1673 show(&mut a, &mut b, &mut c);
1674 for chart in [&a, &b, &c] {
1675 assert_eq!(shown_prices(chart), [8.0, 9.0, 10.5]);
1676 }
1677 let own = c.drawings().drawings.iter().find(|d| d.id == own).unwrap();
1678 assert_eq!(own.chart_points[0].price, 9.0);
1679 assert_eq!(
1680 if revised {
1681 prices_in(&watched.get("A").unwrap())
1682 } else {
1683 stored_prices(&host)
1684 },
1685 [8.0, 10.5],
1686 "a mirrored drawing is not stored"
1687 );
1688 }
1689 }
1690}