1use parking_lot::{Mutex, RwLock};
15
16mod security_rules;
17
18pub use security_rules::{
19 basic_qot_uses_us_pre_after_detail, has_merged_lv2_order_subs_for_security, is_crypto_market,
20 merged_lv2_order_subs_for_security, order_book_max_depth_for_security,
21 order_book_read_uses_requested_count, order_book_requires_accepted_lv2_push,
22 order_book_requires_backend_full_depth, order_book_uses_backend_side_count,
23 snapshot_masks_hk_bmp_bid_ask,
24};
25
26const QOT_RIGHT_UNKNOWN: i32 = 0;
27const QOT_RIGHT_BMP: i32 = 1;
28const QOT_RIGHT_LEVEL1: i32 = 2;
29const QOT_RIGHT_LEVEL2: i32 = 3;
30pub const QOT_RIGHT_SF: i32 = 4;
31const QOT_RIGHT_NO: i32 = 5;
32const QOT_RIGHT_LEVEL3: i32 = 6;
33const SECURITY_TYPE_INDEX: i32 = 6;
34const SECURITY_TYPE_DRVT: i32 = 8;
35const SECURITY_TYPE_FUTURE: i32 = 10;
36const SECURITY_TYPE_CRYPTO: i32 = 12;
37const QOT_MARKET_CC_SECURITY: i32 = 91;
38pub const US_LV2_ORDER_ARCA: u32 = 1;
39pub const US_LV2_ORDER_NASDAQ_TV: u32 = 4;
40pub const US_LV2_ORDER_OVERNIGHT: u32 = 128;
41pub const LV2_ORDER_US_FUTURE: u32 = 1;
42pub const SG_LV2_ORDER_STOCK: u32 = 1;
43pub const SG_LV2_ORDER_STOCK_ODD_LOT: u32 = 2;
44
45fn hk_clt_to_api(clt: u32) -> i32 {
46 match clt {
47 1 => 3,
48 2 => 1,
49 3 => 2,
50 4 => 4,
51 _ => 0,
52 }
53}
54
55fn us_clt_to_api(clt: u32) -> i32 {
56 match clt {
57 0 => 0,
58 1 => 2,
59 2 => 5,
60 _ => 5,
61 }
62}
63
64fn us_backend_status_to_api(got: Option<u32>, flags: Option<UsLv2Flags>) -> Option<i32> {
65 let mut right = got.map(us_clt_to_api);
66 let Some((arca, _nyse, nasdaq_tv, _edg, _bzx)) = flags else {
67 return right;
68 };
69
70 if right != Some(QOT_RIGHT_NO) {
77 if arca == Some(1) && nasdaq_tv == Some(1) {
78 right = Some(QOT_RIGHT_LEVEL3);
79 } else if nasdaq_tv == Some(1) {
80 right = Some(QOT_RIGHT_LEVEL2);
81 }
82 }
83
84 right
85}
86
87fn cn_clt_to_api(clt: u32) -> i32 {
88 match clt {
89 1 => 2,
90 2 => 3,
91 3 => 5,
92 _ => 5,
93 }
94}
95
96fn other_clt_to_api(clt: u32) -> i32 {
97 match clt {
98 1 => 3,
99 2 => 2,
100 _ => 5,
101 }
102}
103
104fn my_stock_common_clt_to_api(clt: u32) -> i32 {
105 match clt {
106 4 => QOT_RIGHT_LEVEL3,
107 3 => QOT_RIGHT_LEVEL2,
108 2 => QOT_RIGHT_LEVEL1,
109 _ => QOT_RIGHT_NO,
110 }
111}
112
113fn jp_stock_clt_to_api(clt: u32) -> i32 {
114 match clt {
115 4 => QOT_RIGHT_LEVEL3,
116 1 => QOT_RIGHT_LEVEL2,
117 2 => QOT_RIGHT_LEVEL1,
118 _ => QOT_RIGHT_NO,
119 }
120}
121
122fn us_future_clt_to_api(clt: u32) -> i32 {
123 match clt {
124 0 => 5,
125 1 => 3,
126 2 => 1,
127 3 => 3,
128 4 => 2,
129 _ => 5,
130 }
131}
132
133fn us_option_clt_to_api(clt: u32) -> i32 {
134 match clt {
135 0 => 1,
136 1 => 2,
137 _ => 1,
138 }
139}
140
141fn us_otc_comm_auth_to_api(deal_data_auth: Option<u32>, order_book_auth: Option<u32>) -> i32 {
142 const COMM_AUTH_RT: u32 = 2;
146 const QOT_RIGHT_LEVEL1: i32 = 2;
147 const QOT_RIGHT_NO: i32 = 5;
148 let deal = deal_data_auth.unwrap_or(COMM_AUTH_RT);
149 let order_book = order_book_auth.unwrap_or(COMM_AUTH_RT);
150 if deal == COMM_AUTH_RT && order_book == COMM_AUTH_RT {
151 QOT_RIGHT_LEVEL1
152 } else {
153 QOT_RIGHT_NO
154 }
155}
156
157fn crypto_auth_to_api(auth: u32) -> i32 {
158 if auth == 1 {
159 QOT_RIGHT_LEVEL1
160 } else {
161 QOT_RIGHT_NO
162 }
163}
164
165fn us_index_flags_to_api(
166 dow_jones: Option<u32>,
167 nasdaq: Option<u32>,
168 standard_poor: Option<u32>,
169) -> Option<i32> {
170 const QOT_RIGHT_LEVEL1: i32 = 2;
174 const QOT_RIGHT_NO: i32 = 5;
175 if dow_jones.is_none() && nasdaq.is_none() && standard_poor.is_none() {
176 return None;
177 }
178 let has_any = dow_jones == Some(1) || nasdaq == Some(1) || standard_poor == Some(1);
179 Some(if has_any {
180 QOT_RIGHT_LEVEL1
181 } else {
182 QOT_RIGHT_NO
183 })
184}
185
186fn has_any_us_lv2_flag(flags: UsLv2Flags) -> bool {
187 let (arca, nyse, nasdaq_tv, edg, bzx) = flags;
188 arca == Some(1) || nyse == Some(1) || nasdaq_tv == Some(1) || edg == Some(1) || bzx == Some(1)
189}
190
191pub type UsLv2Flags = (
192 Option<u32>,
193 Option<u32>,
194 Option<u32>,
195 Option<u32>,
196 Option<u32>,
197);
198pub type UsIndexFlags = (Option<u32>, Option<u32>, Option<u32>);
199pub type UsOtcAuths = (Option<u32>, Option<u32>);
200pub type UsFutureDetailAuths = (Option<u32>, Option<u32>, Option<u32>, Option<u32>);
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub struct Lv2OrderSubDescriptor {
204 pub lv2_type: u32,
205 pub level: u32,
206 pub prob2_v2: bool,
207}
208
209#[derive(Debug, Clone, Copy, Default)]
210pub struct QotRightBackendExtras {
211 pub us_lv2_flags: Option<UsLv2Flags>,
212 pub us_index_flags: Option<UsIndexFlags>,
213 pub us_otc_auths: Option<UsOtcAuths>,
214}
215
216impl QotRightBackendExtras {
217 pub const fn none() -> Self {
218 Self {
219 us_lv2_flags: None,
220 us_index_flags: None,
221 us_otc_auths: None,
222 }
223 }
224}
225
226#[derive(Debug, Clone, Copy, Default)]
227pub struct QotRightBackendUpdate {
228 pub hk_got: Option<u32>,
229 pub us_got: Option<u32>,
230 pub cn_got: Option<u32>,
231 pub sh_auth: Option<u32>,
232 pub sz_auth: Option<u32>,
233 pub hk_option: Option<u32>,
234 pub hk_future: Option<u32>,
235 pub us_option: Option<u32>,
236 pub us_future_cme_cboe: Option<u32>,
237 pub us_future_detail: Option<UsFutureDetailAuths>,
238 pub sg_future: Option<u32>,
239 pub jp_future: Option<u32>,
240 pub sg_stock: Option<u32>,
241 pub my_stock: Option<u32>,
242 pub jp_stock: Option<u32>,
243 pub digital_currency_auth: Option<u32>,
244 pub digital_pt_orderbook_auth: Option<u32>,
245 pub sub_limit: Option<u32>,
246 pub kl_limit: Option<u32>,
247 pub extras: QotRightBackendExtras,
248}
249
250#[derive(Debug, Clone)]
251pub struct QotRightData {
252 pub hk_qot_right: i32,
253 pub us_qot_right: i32,
254 pub api_us_qot_right: i32,
256 pub sh_qot_right: i32,
257 pub sz_qot_right: i32,
258 pub hk_option_qot_right: i32,
259 pub hk_future_qot_right: i32,
260 pub has_us_option_qot_right: bool,
261 pub us_option_qot_right: i32,
262 pub us_index_qot_right: i32,
263 pub us_otc_qot_right: i32,
264 pub us_cme_future_qot_right: i32,
265 pub us_cbot_future_qot_right: i32,
266 pub us_nymex_future_qot_right: i32,
267 pub us_comex_future_qot_right: i32,
268 pub us_cboe_future_qot_right: i32,
269 pub sg_future_qot_right: i32,
270 pub jp_future_qot_right: i32,
271 pub sg_stock_qot_right: i32,
272 pub my_stock_qot_right: i32,
273 pub jp_stock_qot_right: i32,
274 pub cc_qot_right: i32,
275 pub cc_pt_orderbook_qot_right: i32,
276 pub us_lv2_arca_qot_right: bool,
277 pub us_lv2_nyse_qot_right: bool,
278 pub us_lv2_nasdaq_totalview_qot_right: bool,
279 pub sub_quota: i32,
280 pub history_kl_quota: i32,
281 pub hk_option_orderbook_depth: Option<u32>,
283 pub hk_future_orderbook_depth: Option<u32>,
284}
285
286impl Default for QotRightData {
287 fn default() -> Self {
288 const UNKNOWN: i32 = 0;
289 Self {
290 hk_qot_right: UNKNOWN,
291 us_qot_right: UNKNOWN,
292 api_us_qot_right: UNKNOWN,
293 sh_qot_right: UNKNOWN,
294 sz_qot_right: UNKNOWN,
295 hk_option_qot_right: UNKNOWN,
296 hk_future_qot_right: UNKNOWN,
297 has_us_option_qot_right: false,
300 us_option_qot_right: UNKNOWN,
301 us_index_qot_right: UNKNOWN,
302 us_otc_qot_right: UNKNOWN,
303 us_cme_future_qot_right: UNKNOWN,
304 us_cbot_future_qot_right: UNKNOWN,
305 us_nymex_future_qot_right: UNKNOWN,
306 us_comex_future_qot_right: UNKNOWN,
307 us_cboe_future_qot_right: UNKNOWN,
308 sg_future_qot_right: UNKNOWN,
309 jp_future_qot_right: UNKNOWN,
310 sg_stock_qot_right: UNKNOWN,
311 my_stock_qot_right: UNKNOWN,
312 jp_stock_qot_right: UNKNOWN,
313 cc_qot_right: UNKNOWN,
314 cc_pt_orderbook_qot_right: UNKNOWN,
315 us_lv2_arca_qot_right: false,
316 us_lv2_nyse_qot_right: false,
317 us_lv2_nasdaq_totalview_qot_right: false,
318 sub_quota: 4000,
319 history_kl_quota: 100,
320 hk_option_orderbook_depth: None,
321 hk_future_orderbook_depth: None,
322 }
323 }
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
329pub enum QotRightFreshness {
330 Unknown,
331 Pending,
332 Fresh,
333 Stale,
334 Failed { error: String, since_ms: i64 },
335}
336
337impl QotRightFreshness {
338 pub fn is_fresh(&self) -> bool {
339 matches!(self, QotRightFreshness::Fresh)
340 }
341 pub fn is_unconfirmed(&self) -> bool {
342 !self.is_fresh()
343 }
344}
345
346#[derive(Debug, Clone, Default)]
347pub struct PushedQuoteChangeNotify {
348 pub change_items: Vec<(i32, i32, i32)>,
349}
350
351#[derive(Debug, Clone)]
352pub struct QotRightRefreshReport {
353 pub changed: bool,
354 pub fresh_at_ms: i64,
355 pub decoded_fields: Vec<&'static str>,
356 pub freshness_after: QotRightFreshness,
357 pub login_epoch: u64,
358}
359
360#[derive(Debug, Clone)]
361pub struct QotRightStateMeta {
362 pub freshness: QotRightFreshness,
363 pub user_id: Option<u64>,
364 pub login_epoch: u64,
365 pub backend_generation: u64,
366 pub last_refresh_at_ms: i64,
367 pub last_pushed_quote_change_notify: Option<PushedQuoteChangeNotify>,
368}
369
370impl Default for QotRightStateMeta {
371 fn default() -> Self {
372 Self {
373 freshness: QotRightFreshness::Unknown,
374 user_id: None,
375 login_epoch: 0,
376 backend_generation: 0,
377 last_refresh_at_ms: 0,
378 last_pushed_quote_change_notify: None,
379 }
380 }
381}
382
383fn now_ms() -> i64 {
384 use std::time::{SystemTime, UNIX_EPOCH};
385 SystemTime::now()
386 .duration_since(UNIX_EPOCH)
387 .map(|d| d.as_millis() as i64)
388 .unwrap_or(0)
389}
390
391pub struct QotRightCache {
392 data: RwLock<QotRightData>,
393 meta: Mutex<QotRightStateMeta>,
394}
395
396impl Default for QotRightCache {
397 fn default() -> Self {
398 Self::new()
399 }
400}
401
402impl QotRightCache {
403 pub fn new() -> Self {
404 Self {
405 data: RwLock::new(QotRightData::default()),
406 meta: Mutex::new(QotRightStateMeta::default()),
407 }
408 }
409
410 pub fn get(&self) -> QotRightData {
411 self.data.read().clone()
412 }
413
414 pub fn freshness(&self) -> QotRightFreshness {
415 self.meta.lock().freshness.clone()
416 }
417
418 pub fn is_fresh(&self) -> bool {
419 self.meta.lock().freshness.is_fresh()
420 }
421
422 pub fn meta_snapshot(&self) -> QotRightStateMeta {
423 self.meta.lock().clone()
424 }
425
426 pub fn set_orderbook_depths(&self, hk_option_depth: Option<u32>, hk_future_depth: Option<u32>) {
427 let mut d = self.data.write();
428 if let Some(v) = hk_option_depth {
429 d.hk_option_orderbook_depth = Some(v);
430 }
431 if let Some(v) = hk_future_depth {
432 d.hk_future_orderbook_depth = Some(v);
433 }
434 }
435
436 pub fn mark_stale(&self, reason: &str) {
437 let mut m = self.meta.lock();
438 let old = m.freshness.clone();
439 m.freshness = QotRightFreshness::Stale;
440 tracing::info!(
441 old_freshness = ?old,
442 reason,
443 login_epoch = m.login_epoch,
444 backend_generation = m.backend_generation,
445 "QotRightCache: marked stale"
446 );
447 }
448
449 pub fn advance_login_epoch(&self, new_epoch: u64, user_id: Option<u64>) {
450 let mut m = self.meta.lock();
451 let old_epoch = m.login_epoch;
452 m.login_epoch = new_epoch;
453 m.user_id = user_id;
454 if matches!(m.freshness, QotRightFreshness::Fresh) {
455 m.freshness = QotRightFreshness::Stale;
456 }
457 tracing::info!(
458 old_epoch,
459 new_epoch,
460 user_id = ?user_id,
461 "QotRightCache: login epoch advanced"
462 );
463 }
464
465 pub fn advance_backend_generation(&self, new_generation: u64) {
466 let mut m = self.meta.lock();
467 let old_gen = m.backend_generation;
468 m.backend_generation = new_generation;
469 if matches!(m.freshness, QotRightFreshness::Fresh) {
470 m.freshness = QotRightFreshness::Stale;
471 }
472 tracing::info!(
473 old_generation = old_gen,
474 new_generation,
475 "QotRightCache: backend generation advanced"
476 );
477 }
478
479 pub fn mark_pending(&self) {
480 self.meta.lock().freshness = QotRightFreshness::Pending;
481 }
482
483 pub fn mark_fresh(&self) {
484 let mut m = self.meta.lock();
485 m.freshness = QotRightFreshness::Fresh;
486 m.last_refresh_at_ms = now_ms();
487 }
488
489 pub fn mark_failed(&self, error: impl Into<String>) {
490 self.meta.lock().freshness = QotRightFreshness::Failed {
491 error: error.into(),
492 since_ms: now_ms(),
493 };
494 }
495
496 pub fn last_refresh_at_ms(&self) -> i64 {
497 self.meta.lock().last_refresh_at_ms
498 }
499
500 pub fn set_pushed_quote_change_notify(&self, notify: PushedQuoteChangeNotify) {
501 self.meta.lock().last_pushed_quote_change_notify = Some(notify);
502 }
503
504 pub fn pushed_quote_change_notify(&self) -> Option<PushedQuoteChangeNotify> {
505 self.meta.lock().last_pushed_quote_change_notify.clone()
506 }
507
508 pub fn apply_6651_changes(&self, items: &[(i32, i32, i32)]) -> Vec<i32> {
509 let mut d = self.data.write();
510 let mut changed_types = Vec::new();
511
512 for &(quote_type, before, after) in items {
513 if before == 0 || before == after {
516 continue;
517 }
518 let after_u = after as u32;
519 if apply_qot_right_after_change(&mut d, quote_type, after_u) {
520 changed_types.push(quote_type);
521 }
522 }
523 if !changed_types.is_empty() {
524 drop(d);
525 let mut m = self.meta.lock();
526 m.freshness = QotRightFreshness::Fresh;
527 m.last_refresh_at_ms = now_ms();
528 }
529 changed_types
530 }
531
532 pub fn apply_direct_auth_changes(&self, items: &[(i32, u32)]) -> Vec<i32> {
533 let mut d = self.data.write();
534 let mut changed_types = Vec::new();
535
536 for &(quote_type, after) in items {
537 if apply_qot_right_after_change(&mut d, quote_type, after) {
538 changed_types.push(quote_type);
539 }
540 }
541 if !changed_types.is_empty() {
542 drop(d);
543 let mut m = self.meta.lock();
544 m.freshness = QotRightFreshness::Fresh;
545 m.last_refresh_at_ms = now_ms();
546 }
547 changed_types
548 }
549
550 pub fn update_from_backend(&self, update: QotRightBackendUpdate) {
559 let QotRightBackendUpdate {
560 hk_got,
561 us_got,
562 cn_got,
563 sh_auth,
564 sz_auth,
565 hk_option,
566 hk_future,
567 us_option,
568 us_future_cme_cboe,
569 us_future_detail,
570 sg_future,
571 jp_future,
572 sg_stock,
573 my_stock,
574 jp_stock,
575 digital_currency_auth,
576 digital_pt_orderbook_auth,
577 sub_limit,
578 kl_limit,
579 extras,
580 } = update;
581 let mut d = self.data.write();
582
583 if let Some(v) = hk_got {
584 d.hk_qot_right = hk_clt_to_api(v);
585 }
586 if let Some(v) = us_got {
587 let right = us_clt_to_api(v);
588 d.us_qot_right = right;
589 }
590 if let Some(right) = us_backend_status_to_api(us_got, extras.us_lv2_flags) {
591 d.api_us_qot_right = right;
592 }
593 if let Some(flags) = extras.us_lv2_flags {
594 let (arca, nyse, nasdaq_tv, _edg, _bzx) = flags;
595 d.us_lv2_arca_qot_right = arca == Some(1);
596 d.us_lv2_nyse_qot_right = nyse == Some(1);
597 d.us_lv2_nasdaq_totalview_qot_right = nasdaq_tv == Some(1);
598 if has_any_us_lv2_flag(flags) && matches!(d.us_qot_right, 2 | 3) {
599 d.us_qot_right = 3;
603 }
604 }
605 if let Some((dow_jones, nasdaq, standard_poor)) = extras.us_index_flags
606 && let Some(api_right) = us_index_flags_to_api(dow_jones, nasdaq, standard_poor)
607 {
608 d.us_index_qot_right = api_right;
609 }
610 if let Some((deal_data_auth, order_book_auth)) = extras.us_otc_auths {
611 d.us_otc_qot_right = us_otc_comm_auth_to_api(deal_data_auth, order_book_auth);
612 }
613 if let Some(v) = sh_auth {
614 d.sh_qot_right = cn_clt_to_api(v);
615 } else if let Some(v) = cn_got {
616 d.sh_qot_right = cn_clt_to_api(v);
617 }
618 if let Some(v) = sz_auth {
619 d.sz_qot_right = cn_clt_to_api(v);
620 } else if let Some(v) = cn_got {
621 d.sz_qot_right = cn_clt_to_api(v);
622 }
623 if let Some(v) = hk_option {
624 d.hk_option_qot_right = hk_clt_to_api(v);
625 }
626 if let Some(v) = hk_future {
627 d.hk_future_qot_right = hk_clt_to_api(v);
628 }
629 if let Some(v) = us_option {
630 d.us_option_qot_right = us_option_clt_to_api(v);
631 d.has_us_option_qot_right = d.us_option_qot_right != 1;
632 }
633 if let Some(v) = us_future_cme_cboe {
634 d.us_cme_future_qot_right = us_future_clt_to_api(v);
635 d.us_cboe_future_qot_right = 5;
640 }
641
642 if let Some((cme, cbot, nymex, comex)) = us_future_detail {
645 if let Some(cme) = cme {
646 d.us_cme_future_qot_right = us_future_clt_to_api(cme);
647 }
648 if let Some(cbot) = cbot {
649 d.us_cbot_future_qot_right = us_future_clt_to_api(cbot);
650 }
651 if let Some(nymex) = nymex {
652 d.us_nymex_future_qot_right = us_future_clt_to_api(nymex);
653 }
654 if let Some(comex) = comex {
655 d.us_comex_future_qot_right = us_future_clt_to_api(comex);
656 }
657 d.us_cboe_future_qot_right = 5;
659 }
660
661 if let Some(v) = sg_future {
662 d.sg_future_qot_right = other_clt_to_api(v);
663 }
664 if let Some(v) = jp_future {
665 d.jp_future_qot_right = other_clt_to_api(v);
666 }
667 if let Some(v) = sg_stock {
668 d.sg_stock_qot_right = other_clt_to_api(v);
669 }
670 if let Some(v) = my_stock {
671 d.my_stock_qot_right = my_stock_common_clt_to_api(v);
672 }
673 if let Some(v) = jp_stock {
674 d.jp_stock_qot_right = jp_stock_clt_to_api(v);
675 }
676 if let Some(v) = digital_currency_auth {
677 d.cc_qot_right = crypto_auth_to_api(v);
678 }
679 if let Some(v) = digital_pt_orderbook_auth {
680 d.cc_pt_orderbook_qot_right = crypto_auth_to_api(v);
681 }
682
683 if let Some(v) = sub_limit
684 && v > 0
685 {
686 d.sub_quota = v as i32;
687 }
688 if let Some(v) = kl_limit
689 && v > 0
690 {
691 d.history_kl_quota = v as i32;
692 }
693
694 drop(d);
696 let mut m = self.meta.lock();
697 m.freshness = QotRightFreshness::Fresh;
698 m.last_refresh_at_ms = now_ms();
699 }
700}
701
702fn apply_qot_right_after_change(data: &mut QotRightData, quote_type: i32, after: u32) -> bool {
703 match quote_type {
704 1 => data.hk_qot_right = hk_clt_to_api(after),
705 3 | 17 => {
706 let right = us_clt_to_api(after);
707 data.us_qot_right = right;
708 data.api_us_qot_right = right;
709 }
710 4 => data.hk_future_qot_right = hk_clt_to_api(after),
711 5 => data.hk_option_qot_right = hk_clt_to_api(after),
712 36 => data.sg_future_qot_right = other_clt_to_api(after),
713 39 => data.jp_future_qot_right = other_clt_to_api(after),
714 20 => data.sg_stock_qot_right = other_clt_to_api(after),
715 29 => data.jp_stock_qot_right = jp_stock_clt_to_api(after),
716 30 => data.us_cme_future_qot_right = us_future_clt_to_api(after),
717 31 => data.us_cbot_future_qot_right = us_future_clt_to_api(after),
718 32 => data.us_nymex_future_qot_right = us_future_clt_to_api(after),
719 33 => data.us_comex_future_qot_right = us_future_clt_to_api(after),
720 37 => data.us_otc_qot_right = if after == 2 { 2 } else { 5 },
721 38 => data.us_index_qot_right = us_clt_to_api(after),
722 40 => data.sh_qot_right = cn_clt_to_api(after),
723 41 => data.sz_qot_right = cn_clt_to_api(after),
724 _ => return false,
725 }
726 true
727}
728
729#[cfg(test)]
730mod tests;