1use crate::sync::SyncType;
2use crate::{
3 config::Config,
4 evaluate::{EvalDetail, Repository},
5};
6use crate::{sync::Synchronizer, FPConfig};
7use crate::{sync::UpdateCallback, user::FPUser};
8use crate::{FPDetail, SdkAuthorization, Toggle};
9use event::event::AccessEvent;
10use event::event::CustomEvent;
11use event::event::DebugEvent;
12use event::event::Event;
13use event::recorder::unix_timestamp;
14use event::recorder::EventRecorder;
15use feature_probe_event as event;
16#[cfg(feature = "realtime")]
17use futures_util::FutureExt;
18use parking_lot::RwLock;
19use serde_json::Value;
20#[cfg(feature = "realtime")]
21use socketio_rs::Client;
22use std::collections::HashMap;
23use std::fmt::Debug;
24use std::sync::Arc;
25use tracing::{trace, warn};
26
27#[cfg(feature = "realtime")]
28type SocketCallback = std::pin::Pin<Box<dyn futures_util::Future<Output = ()> + Send>>;
29
30#[derive(Default, Clone)]
31pub struct FeatureProbe {
32 repo: Arc<RwLock<Repository>>,
33 syncer: Option<Synchronizer>,
34 event_recorder: Option<EventRecorder>,
35 config: Config,
36 should_stop: Arc<RwLock<bool>>,
37 #[cfg(feature = "realtime")]
38 socket: Option<Client>,
39}
40
41impl Debug for FeatureProbe {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.debug_tuple("FeatureProbe")
44 .field(&self.repo)
45 .field(&self.syncer)
46 .field(&self.config)
47 .field(&self.should_stop)
48 .finish()
49 }
50}
51
52impl FeatureProbe {
53 pub fn new(config: FPConfig) -> Self {
54 let config = config.build();
55 let mut slf = Self {
56 config,
57 ..Default::default()
58 };
59
60 slf.start();
61 slf
62 }
63
64 pub fn new_for_test(toggle: &str, value: Value) -> Self {
65 let mut toggles = HashMap::new();
66 toggles.insert(toggle.to_owned(), value);
67 FeatureProbe::new_for_tests(toggles)
68 }
69
70 pub fn new_for_tests(toggles: HashMap<String, Value>) -> Self {
71 let mut repo = Repository::default();
72 for (key, val) in toggles {
73 repo.toggles
74 .insert(key.clone(), Toggle::new_for_test(key, val));
75 }
76
77 Self {
78 repo: Arc::new(RwLock::new(repo)),
79 ..Default::default()
80 }
81 }
82
83 pub fn bool_value(&self, toggle: &str, user: &FPUser, default: bool) -> bool {
84 self.generic_eval(toggle, user, default, false, |v| v.as_bool())
85 .value
86 }
87
88 pub fn string_value(&self, toggle: &str, user: &FPUser, default: String) -> String {
89 self.generic_eval(toggle, user, default, false, |v| {
90 v.as_str().map(|s| s.to_owned())
91 })
92 .value
93 }
94
95 pub fn number_value(&self, toggle: &str, user: &FPUser, default: f64) -> f64 {
96 self.generic_eval(toggle, user, default, false, |v| v.as_f64())
97 .value
98 }
99
100 pub fn json_value(&self, toggle: &str, user: &FPUser, default: Value) -> Value {
101 self.generic_eval(toggle, user, default, false, Some).value
102 }
103
104 pub fn bool_detail(&self, toggle: &str, user: &FPUser, default: bool) -> FPDetail<bool> {
105 self.generic_eval(toggle, user, default, true, |v| v.as_bool())
106 }
107
108 pub fn string_detail(&self, toggle: &str, user: &FPUser, default: String) -> FPDetail<String> {
109 self.generic_eval(toggle, user, default, true, |v| {
110 v.as_str().map(|x| x.to_owned())
111 })
112 }
113
114 pub fn number_detail(&self, toggle: &str, user: &FPUser, default: f64) -> FPDetail<f64> {
115 self.generic_eval(toggle, user, default, true, |v| v.as_f64())
116 }
117
118 pub fn json_detail(&self, toggle: &str, user: &FPUser, default: Value) -> FPDetail<Value> {
119 self.generic_eval(toggle, user, default, true, Some)
120 }
121
122 pub fn track(&self, event_name: &str, user: &FPUser, value: Option<f64>) {
123 let recorder = match self.event_recorder.as_ref() {
124 None => {
125 warn!("Event Recorder no ready.");
126 return;
127 }
128 Some(recorder) => recorder,
129 };
130 let event = CustomEvent {
131 kind: "custom".to_string(),
132 time: unix_timestamp(),
133 user: user.key(),
134 name: event_name.to_string(),
135 value,
136 };
137 recorder.record_event(Event::CustomEvent(event));
138 }
139
140 pub fn new_with(server_key: String, repo: Repository) -> Self {
141 Self {
142 config: Config {
143 server_sdk_key: server_key,
144 ..Default::default()
145 },
146 repo: Arc::new(RwLock::new(repo)),
147 syncer: None,
148 event_recorder: None,
149 should_stop: Arc::new(RwLock::new(false)),
150 #[cfg(feature = "realtime")]
151 socket: None,
152 }
153 }
154
155 pub fn close(&self) {
156 trace!("closing featureprobe client");
157 if let Some(recorder) = &self.event_recorder {
158 recorder.flush();
159 }
160 let mut should_stop = self.should_stop.write();
161 *should_stop = true;
162 }
163
164 pub fn initialized(&self) -> bool {
165 match &self.syncer {
166 Some(s) => s.initialized(),
167 None => false,
168 }
169 }
170
171 pub fn set_update_callback(&mut self, update_callback: UpdateCallback) {
172 if let Some(syncer) = &mut self.syncer {
173 syncer.set_update_callback(update_callback)
174 }
175 }
176
177 pub fn version(&self) -> Option<u128> {
178 self.syncer.as_ref().map(|s| s.version()).flatten()
179 }
180
181 fn generic_eval<T: Default + Debug>(
182 &self,
183 toggle: &str,
184 user: &FPUser,
185 default: T,
186 is_detail: bool,
187 transform: fn(Value) -> Option<T>,
188 ) -> FPDetail<T> {
189 let (value, reason, detail) = match self.eval(toggle, user, is_detail) {
190 None => (
191 default,
192 Some(format!("Toggle:[{toggle}] not exist")),
193 Default::default(),
194 ),
195 Some(mut d) => match d.value.take() {
196 None => (default, None, d), Some(v) => match transform(v) {
198 None => (default, Some("Value type mismatch.".to_string()), d), Some(typed_v) => (typed_v, None, d),
200 },
201 },
202 };
203
204 FPDetail {
205 value,
206 reason: reason.unwrap_or(detail.reason),
207 rule_index: detail.rule_index,
208 variation_index: detail.variation_index,
209 version: detail.version,
210 }
211 }
212
213 fn eval(&self, toggle: &str, user: &FPUser, is_detail: bool) -> Option<EvalDetail<Value>> {
214 let repo = self.repo.read();
215 let debug_until_time = repo.debug_until_time;
216 let detail = repo.toggles.get(toggle).map(|toggle| {
217 toggle.eval(
218 user,
219 &repo.segments,
220 &repo.toggles,
221 is_detail,
222 self.config.max_prerequisites_deep,
223 debug_until_time,
224 )
225 });
226
227 if let Some(recorder) = &self.event_recorder {
228 let track_access_events = repo
229 .toggles
230 .get(toggle)
231 .map(|t| t.track_access_events())
232 .unwrap_or(false);
233 record_event(
234 recorder.clone(),
235 track_access_events,
236 toggle,
237 user,
238 detail.clone(),
239 debug_until_time,
240 )
241 }
242
243 detail.map(|mut d| {
244 d.debug_until_time = debug_until_time;
245 d
246 })
247 }
248
249 fn start(&mut self) {
250 self.sync();
251
252 #[cfg(feature = "realtime")]
253 self.connect_socket();
254
255 if self.config.track_events {
256 self.flush_events();
257 }
258 }
259
260 fn sync(&mut self) {
261 trace!("sync url {}", &self.config.toggles_url);
262 let toggles_url = self.config.toggles_url.clone();
263 let refresh_interval = self.config.refresh_interval;
264 let auth = SdkAuthorization(self.config.server_sdk_key.clone()).encode();
265 let repo = self.repo.clone();
266 let syncer = Synchronizer::new(
267 toggles_url,
268 refresh_interval,
269 auth,
270 self.config.http_client.clone().unwrap_or_default(),
271 repo,
272 );
273 self.syncer = Some(syncer.clone());
274 syncer.start_sync(self.config.start_wait, self.should_stop.clone());
275 }
276
277 pub fn sync_now(&self, t: SyncType) {
278 trace!("sync now url {}", &self.config.toggles_url);
279 let syncer = match &self.syncer {
280 Some(syncer) => syncer.clone(),
281 None => return,
282 };
283 syncer.sync_now(t);
284 }
285
286 #[cfg(feature = "realtime")]
287 fn connect_socket(&mut self) {
288 let mut slf = self.clone();
289 let slf2 = self.clone();
290 let nsp = self.config.realtime_path.clone();
291 tokio::spawn(async move {
292 let url = slf.config.realtime_url;
293 let server_sdk_key = slf.config.server_sdk_key.clone();
294 trace!("connect_socket {}", url);
295 let client = socketio_rs::ClientBuilder::new(url.clone())
296 .namespace(&nsp)
297 .on(socketio_rs::Event::Connect, move |_, socket, _| {
298 Self::socket_on_connect(socket, server_sdk_key.clone())
299 })
300 .on(
301 "update",
302 move |payload: Option<socketio_rs::Payload>, _, _| {
303 Self::socket_on_update(slf2.clone(), payload)
304 },
305 )
306 .on("error", |err, _, _| {
307 async move { tracing::error!("socket on error: {:#?}", err) }.boxed()
308 })
309 .connect()
310 .await;
311 match client {
312 Err(e) => tracing::error!("connect_socket error: {:?}", e),
313 Ok(client) => slf.socket = Some(client),
314 };
315 });
316 }
317
318 #[cfg(feature = "realtime")]
319 fn socket_on_connect(socket: socketio_rs::Socket, server_sdk_key: String) -> SocketCallback {
320 let sdk_key = server_sdk_key;
321 trace!("socket_on_connect: {:?}", sdk_key);
322 async move {
323 if let Err(e) = socket
324 .emit("register", serde_json::json!({ "key": sdk_key }))
325 .await
326 {
327 tracing::error!("register error: {:?}", e);
328 }
329 }
330 .boxed()
331 }
332
333 #[cfg(feature = "realtime")]
334 fn socket_on_update(slf: Self, payload: Option<socketio_rs::Payload>) -> SocketCallback {
335 trace!("socket_on_update: {:?}", payload);
336 async move {
337 if let Some(syncer) = &slf.syncer {
338 syncer.sync_now(SyncType::Realtime);
339 } else {
340 warn!("socket receive update event, but no synchronizer");
341 }
342 }
343 .boxed()
344 }
345
346 fn flush_events(&mut self) {
347 trace!("flush_events");
348 let events_url = self.config.events_url.clone();
349 let flush_interval = self.config.refresh_interval;
350 let auth = SdkAuthorization(self.config.server_sdk_key.clone()).encode();
351 let should_stop = self.should_stop.clone();
352 let event_recorder = EventRecorder::new(
353 events_url,
354 auth,
355 (*crate::USER_AGENT).clone(),
356 flush_interval,
357 100,
358 should_stop,
359 );
360 self.event_recorder = Some(event_recorder);
361 }
362
363 #[cfg(feature = "internal")]
364 pub fn repo(&self) -> Arc<RwLock<Repository>> {
365 self.repo.clone()
366 }
367}
368
369fn record_event(
370 recorder: EventRecorder,
371 track_access_events: bool,
372 toggle: &str,
373 user: &FPUser,
374 detail: Option<EvalDetail<Value>>,
375 debug_until_time: Option<u64>,
376) {
377 let toggle = toggle.to_owned();
378 let user = user.key();
379 let user_detail = serde_json::to_value(user.clone()).unwrap_or_default();
380
381 tokio::spawn(async move {
382 let ts = unix_timestamp();
383 record_access(
384 &recorder,
385 &toggle,
386 user.clone(),
387 track_access_events,
388 &detail,
389 ts,
390 );
391 record_debug(
392 &recorder,
393 &toggle,
394 user,
395 user_detail,
396 debug_until_time,
397 &detail,
398 ts,
399 );
400 });
401}
402
403fn record_access(
404 recorder: &EventRecorder,
405 toggle: &str,
406 user: String,
407 track_access_events: bool,
408 detail: &Option<EvalDetail<Value>>,
409 ts: u128,
410) -> Option<()> {
411 let detail = detail.as_ref()?;
412 let value = detail.value.as_ref()?;
413 let event = AccessEvent {
414 kind: "access".to_string(),
415 time: ts,
416 key: toggle.to_owned(),
417 user,
418 value: value.clone(),
419 variation_index: detail.variation_index?,
420 version: detail.version,
421 rule_index: detail.rule_index,
422 track_access_events,
423 };
424 recorder.record_event(Event::AccessEvent(event));
425 None
426}
427
428#[allow(clippy::too_many_arguments)]
429fn record_debug(
430 recorder: &EventRecorder,
431 toggle: &str,
432 user: String,
433 user_detail: Value,
434 debug_until_time: Option<u64>,
435 detail: &Option<EvalDetail<Value>>,
436 ts: u128,
437) -> Option<()> {
438 let detail = detail.as_ref()?;
439 let value = detail.value.as_ref()?;
440 if let Some(debug_until_time) = debug_until_time {
441 if debug_until_time as u128 >= ts {
442 let debug = DebugEvent {
443 kind: "debug".to_string(),
444 time: ts,
445 key: toggle.to_owned(),
446 user,
447 user_detail,
448 value: value.clone(),
449 variation_index: detail.variation_index?,
450 version: detail.version,
451 rule_index: detail.rule_index,
452 reason: Some(detail.reason.to_string()),
453 };
454 recorder.record_event(Event::DebugEvent(debug));
455 }
456 }
457 None
458}
459
460#[cfg(test)]
461mod tests {
462 use serde_json::json;
463
464 use super::*;
465 use crate::FPError;
466 use std::fs;
467 use std::path::PathBuf;
468
469 #[test]
470 fn test_feature_probe_bool() {
471 let json = load_local_json("resources/fixtures/repo.json");
472 let fp = FeatureProbe::new_with("secret key".to_string(), json.unwrap());
473 let u = FPUser::new().with("name", "bob").with("city", "1");
474
475 assert!(fp.bool_value("bool_toggle", &u, false));
476 assert!(fp.bool_detail("bool_toggle", &u, false).value);
477 }
478
479 #[test]
480 fn test_feature_probe_number() {
481 let json = load_local_json("resources/fixtures/repo.json");
482 let fp = FeatureProbe::new_with("secret key".to_string(), json.unwrap());
483 let u = FPUser::new().with("name", "bob").with("city", "1");
484
485 assert_eq!(fp.number_value("number_toggle", &u, 0.0), 1.0);
486 assert_eq!(fp.number_detail("number_toggle", &u, 0.0).value, 1.0);
487 }
488
489 #[test]
490 fn test_feature_probe_string() {
491 let json = load_local_json("resources/fixtures/repo.json");
492 let fp = FeatureProbe::new_with("secret key".to_string(), json.unwrap());
493 let u = FPUser::new().with("name", "bob").with("city", "1");
494
495 assert_eq!(
496 fp.string_value("string_toggle", &u, "".to_string()),
497 "1".to_owned()
498 );
499 assert_eq!(
500 fp.string_detail("string_toggle", &u, "".to_owned()).value,
501 "1".to_owned()
502 );
503 }
504
505 #[test]
506 fn test_feature_probe_json() {
507 let json = load_local_json("resources/fixtures/repo.json");
508 let fp = FeatureProbe::new_with("secret key".to_string(), json.unwrap());
509 let u = FPUser::new().with("name", "bob").with("city", "1");
510
511 assert!(fp
512 .json_value("json_toggle", &u, json!(""))
513 .get("variation_0")
514 .is_some());
515 assert!(fp
516 .json_detail("json_toggle", &u, json!(""))
517 .value
518 .get("variation_0")
519 .is_some());
520 }
521
522 #[test]
523 fn test_feature_probe_none_exist_toggle() {
524 let json = load_local_json("resources/fixtures/repo.json");
525 let fp = FeatureProbe::new_with("secret key".to_string(), json.unwrap());
526 let u = FPUser::new();
527
528 assert!(fp.bool_value("none_exist_toggle", &u, true));
529 let d = fp.bool_detail("none_exist_toggle", &u, true);
530 assert!(d.value);
531 assert_eq!(d.rule_index, None);
532 }
533
534 #[test]
535 fn test_for_ut() {
536 let fp = FeatureProbe::new_for_test("toggle_1", Value::Bool(false));
537 let u = FPUser::new();
538 assert!(!fp.bool_value("toggle_1", &u, true));
539
540 let mut toggles: HashMap<String, Value> = HashMap::new();
541 toggles.insert("toggle_2".to_owned(), json!(12.5));
542 toggles.insert("toggle_3".to_owned(), json!("value"));
543 let fp = FeatureProbe::new_for_tests(toggles);
544 assert_eq!(fp.number_value("toggle_2", &u, 20.0), 12.5);
545 assert_eq!(fp.string_value("toggle_3", &u, "val".to_owned()), "value");
546 }
547
548 #[test]
549 fn test_feature_probe_record_debug() {
550 let json = load_local_json("resources/fixtures/repo.json");
551 let mut repo = json.unwrap();
552 repo.debug_until_time = Some(unix_timestamp() as u64 + 60 * 1000);
553 let fp = FeatureProbe::new_with("secret key".to_string(), repo);
554 let u = FPUser::new().with("name", "bob").with("city", "1");
555 fp.bool_value("bool_toggle", &u, false);
556 }
557
558 fn load_local_json(file: &str) -> Result<Repository, FPError> {
559 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
560 path.push(file);
561 let json_str = fs::read_to_string(path).unwrap();
562 let repo = crate::evaluate::load_json(&json_str);
563 assert!(repo.is_ok(), "err is {:?}", repo);
564 repo
565 }
566}
567
568#[cfg(test)]
569mod server_sdk_contract_tests {
570 use crate::{FPDetail, FPError, FPUser, FeatureProbe, Repository};
571 use serde::{Deserialize, Serialize};
572 use serde_json::Value;
573 use std::fmt::Debug;
574 use std::fs;
575 use std::path::PathBuf;
576 use std::string::String;
577
578 #[allow(dead_code)]
579 pub(crate) fn load_tests_json(json_str: &str) -> Result<Tests, FPError> {
580 serde_json::from_str::<Tests>(json_str)
581 .map_err(|e| FPError::JsonError(json_str.to_owned(), e))
582 }
583
584 #[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
585 #[serde(rename_all = "camelCase")]
586 pub struct Tests {
587 pub(crate) tests: Vec<Scenario>,
588 }
589
590 #[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
591 pub struct Scenario {
592 pub(crate) scenario: String,
593 pub(crate) cases: Vec<Case>,
594 pub(crate) fixture: Repository,
595 }
596
597 #[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
598 #[serde(rename_all = "camelCase")]
599 pub struct Case {
600 pub(crate) name: String,
601 pub(crate) user: User,
602 pub(crate) function: Function,
603 pub(crate) expect_result: ExpectResult,
604 }
605
606 #[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
607 #[serde(rename_all = "camelCase")]
608 pub struct User {
609 pub(crate) key: String,
610 pub(crate) custom_values: Vec<KeyValue>,
611 }
612
613 #[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
614 pub struct KeyValue {
615 pub(crate) key: String,
616 pub(crate) value: String,
617 }
618
619 #[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
620 pub struct Function {
621 pub(crate) name: String,
622 pub(crate) toggle: String,
623 pub(crate) default: Value,
624 }
625
626 #[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
627 #[serde(rename_all = "camelCase")]
628 pub struct ExpectResult {
629 pub(crate) value: Value,
630 pub(crate) reason: Option<String>,
631 pub(crate) rule_index: Option<usize>,
632 pub(crate) no_rule_index: Option<bool>,
633 pub(crate) version: Option<u64>,
634 }
635
636 #[test]
637 fn test_contract() {
638 let root = load_test_json("resources/fixtures/spec/spec/toggle_simple_spec.json");
639 assert!(root.is_ok());
640
641 for scenario in root.unwrap().tests {
642 println!("scenario: {}", scenario.scenario);
643 assert!(!scenario.cases.is_empty());
644
645 let fp = FeatureProbe::new_with("secret key".to_string(), scenario.fixture);
646
647 for case in scenario.cases {
648 println!(" case: {}", case.name);
649
650 let mut user = FPUser::new().stable_rollout(case.user.key.clone());
651 for custom_value in &case.user.custom_values {
652 user = user.with(custom_value.key.clone(), custom_value.value.clone());
653 }
654
655 macro_rules! validate_value {
656 ( $fun:ident, $default:expr, $expect:expr) => {
657 let ret = fp.$fun(case.function.toggle.as_str(), &user, $default);
658 assert_eq!(ret, $expect);
659 };
660 }
661
662 macro_rules! validate_detail {
663 ( $fun:ident, $default:expr, $expect:expr) => {
664 let ret = fp.$fun(case.function.toggle.as_str(), &user, $default);
665 assert_eq!(ret.value, $expect);
666 assert_detail(&case, ret);
667 };
668 }
669
670 match case.function.name.as_str() {
671 "bool_value" => {
672 validate_value!(
673 bool_value,
674 case.function.default.as_bool().unwrap(),
675 case.expect_result.value.as_bool().unwrap()
676 );
677 }
678 "string_value" => {
679 validate_value!(
680 string_value,
681 case.function.default.as_str().unwrap().to_string(),
682 case.expect_result.value.as_str().unwrap().to_string()
683 );
684 }
685 "number_value" => {
686 validate_value!(
687 number_value,
688 case.function.default.as_f64().unwrap(),
689 case.expect_result.value.as_f64().unwrap()
690 );
691 }
692 "json_value" => {
693 validate_value!(
694 json_value,
695 case.function.default,
696 case.expect_result.value
697 );
698 }
699 "bool_detail" => {
700 validate_detail!(
701 bool_detail,
702 case.function.default.as_bool().unwrap(),
703 case.expect_result.value
704 );
705 }
706 "string_detail" => {
707 validate_detail!(
708 string_detail,
709 case.function.default.as_str().unwrap().to_string(),
710 case.expect_result.value
711 );
712 }
713 "number_detail" => {
714 validate_detail!(
715 number_detail,
716 case.function.default.as_f64().unwrap(),
717 case.expect_result.value
718 );
719 }
720 "json_detail" => {
721 validate_detail!(
722 json_detail,
723 case.function.default.clone(),
724 case.expect_result.value
725 );
726 }
727 _ => panic!("function name {} not found.", case.function.name),
728 }
729 }
730 }
731 }
732
733 fn assert_detail<T: Default + Debug>(case: &Case, ret: FPDetail<T>) {
734 match &case.expect_result.reason {
735 None => (),
736 Some(r) => {
737 assert!(
738 ret.reason.contains(r.as_str()),
739 "reason: \"{}\" does not contains \"{}\"",
740 ret.reason.as_str(),
741 r.as_str()
742 );
743 }
744 };
745
746 if case.expect_result.rule_index.is_some() {
747 assert_eq!(
748 case.expect_result.rule_index, ret.rule_index,
749 "rule index not match"
750 );
751 }
752
753 if case.expect_result.no_rule_index.is_some() {
754 assert!(
755 case.expect_result.rule_index.is_none(),
756 "should not have rule index."
757 );
758 }
759
760 if case.expect_result.version.is_some() {
761 assert_eq!(case.expect_result.version, ret.version, "version not match");
762 }
763 }
764
765 fn load_test_json(file: &str) -> Result<Tests, FPError> {
766 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
767 path.push(file);
768 let mut json_str = fs::read_to_string(path.clone());
769 if json_str.is_err() {
770 use std::process::Command;
771 Command::new("git")
772 .args(["submodule", "init"])
773 .status()
774 .expect("init");
775 Command::new("git")
776 .args(["submodule", "update"])
777 .status()
778 .expect("update");
779 json_str = fs::read_to_string(path);
780 }
781 assert!(json_str.is_ok(),
782 "contract test resource not found, run `git submodule init && git submodule update` to fetch");
783 let tests = load_tests_json(&json_str.unwrap());
784 assert!(tests.is_ok(), "err is {:?}", tests);
785 tests
786 }
787}