1use cargo_options::Run;
2use clap::Args;
3use matchit::{InsertError, MatchError, Router};
4use serde::{
5 Deserialize, Serialize,
6 de::{Error, Visitor},
7 ser::SerializeSeq,
8};
9use serde_json::{Value, json};
10use std::{collections::HashMap, path::PathBuf};
11
12use crate::{
13 cargo::{count_common_options, serialize_common_options},
14 env::{EnvOptions, Environment},
15 error::MetadataError,
16 lambda::Timeout,
17};
18
19use cargo_lambda_remote::tls::TlsOptions;
20
21#[cfg(windows)]
22const DEFAULT_INVOKE_ADDRESS: &str = "127.0.0.1";
23
24#[cfg(not(windows))]
25const DEFAULT_INVOKE_ADDRESS: &str = "::";
26
27const DEFAULT_INVOKE_PORT: u16 = 9000;
28
29#[derive(Args, Clone, Debug, Default, Deserialize)]
30#[command(
31 name = "watch",
32 visible_alias = "start",
33 after_help = "Full command documentation: https://www.cargo-lambda.info/commands/watch.html"
34)]
35pub struct Watch {
36 #[arg(long, visible_alias = "no-reload")]
38 #[serde(default)]
39 pub ignore_changes: bool,
40
41 #[arg(long)]
44 #[serde(default)]
45 pub only_lambda_apis: bool,
46
47 #[arg(short = 'A', long, default_value = DEFAULT_INVOKE_ADDRESS)]
48 #[serde(default = "default_invoke_address")]
49 pub invoke_address: String,
51
52 #[arg(short = 'P', long, default_value_t = DEFAULT_INVOKE_PORT)]
54 #[serde(default = "default_invoke_port")]
55 pub invoke_port: u16,
56
57 #[arg(long)]
59 #[serde(default)]
60 pub print_traces: bool,
61
62 #[arg(long, short)]
64 #[serde(default)]
65 pub wait: bool,
66
67 #[arg(long)]
69 #[serde(default)]
70 pub disable_cors: bool,
71
72 #[arg(long)]
74 #[serde(default)]
75 pub timeout: Option<Timeout>,
76
77 #[command(flatten)]
78 #[serde(flatten)]
79 pub cargo_opts: Run,
80
81 #[command(flatten)]
82 #[serde(flatten)]
83 pub env_options: EnvOptions,
84
85 #[command(flatten)]
86 #[serde(flatten)]
87 pub tls_options: TlsOptions,
88
89 #[arg(long, default_value = "1")]
91 #[serde(default = "default_concurrency")]
92 pub concurrency: usize,
93
94 #[arg(skip)]
95 #[serde(default, skip_serializing_if = "is_empty_router")]
96 pub router: Option<FunctionRouter>,
97}
98
99impl Watch {
100 pub fn manifest_path(&self) -> PathBuf {
101 self.cargo_opts
102 .manifest_path
103 .clone()
104 .unwrap_or_else(|| "Cargo.toml".into())
105 }
106
107 pub fn pkg_name(&self) -> Option<String> {
110 if self.cargo_opts.packages.len() > 1 {
111 return None;
112 }
113 self.cargo_opts.packages.first().map(|s| s.to_string())
114 }
115
116 pub fn bin_name(&self) -> Option<String> {
117 if self.cargo_opts.bin.len() > 1 {
118 return None;
119 }
120 self.cargo_opts.bin.first().map(|s| s.to_string())
121 }
122
123 pub fn lambda_environment(
124 &self,
125 base: &HashMap<String, String>,
126 ) -> Result<Environment, MetadataError> {
127 self.env_options.lambda_environment(base)
128 }
129}
130
131impl Serialize for Watch {
132 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
133 where
134 S: serde::Serializer,
135 {
136 use serde::ser::SerializeStruct;
137
138 let field_count = self.ignore_changes as usize
140 + self.only_lambda_apis as usize
141 + !self.invoke_address.is_empty() as usize
142 + (self.invoke_port != 0) as usize
143 + self.print_traces as usize
144 + self.wait as usize
145 + self.disable_cors as usize
146 + self.timeout.is_some() as usize
147 + (self.concurrency != 1) as usize
148 + self.router.is_some() as usize
149 + self.cargo_opts.manifest_path.is_some() as usize
150 + self.cargo_opts.release as usize
151 + self.cargo_opts.ignore_rust_version as usize
152 + self.cargo_opts.unit_graph as usize
153 + !self.cargo_opts.packages.is_empty() as usize
154 + !self.cargo_opts.bin.is_empty() as usize
155 + !self.cargo_opts.example.is_empty() as usize
156 + !self.cargo_opts.args.is_empty() as usize
157 + count_common_options(&self.cargo_opts.common)
158 + self.env_options.count_fields()
159 + self.tls_options.count_fields();
160
161 let mut state = serializer.serialize_struct("Watch", field_count)?;
162
163 if self.ignore_changes {
165 state.serialize_field("ignore_changes", &true)?;
166 }
167 if self.only_lambda_apis {
168 state.serialize_field("only_lambda_apis", &true)?;
169 }
170 if !self.invoke_address.is_empty() {
171 state.serialize_field("invoke_address", &self.invoke_address)?;
172 }
173 if self.invoke_port != 0 {
174 state.serialize_field("invoke_port", &self.invoke_port)?;
175 }
176 if self.print_traces {
177 state.serialize_field("print_traces", &true)?;
178 }
179 if self.wait {
180 state.serialize_field("wait", &true)?;
181 }
182 if self.disable_cors {
183 state.serialize_field("disable_cors", &true)?;
184 }
185
186 if let Some(timeout) = &self.timeout {
188 state.serialize_field("timeout", timeout)?;
189 }
190 if self.concurrency != 1 {
191 state.serialize_field("concurrency", &self.concurrency)?;
192 }
193 if let Some(router) = &self.router {
194 state.serialize_field("router", router)?;
195 }
196
197 self.env_options.serialize_fields::<S>(&mut state)?;
199 self.tls_options.serialize_fields::<S>(&mut state)?;
200
201 if let Some(manifest_path) = &self.cargo_opts.manifest_path {
202 state.serialize_field("manifest_path", manifest_path)?;
203 }
204 if self.cargo_opts.release {
205 state.serialize_field("release", &true)?;
206 }
207 if self.cargo_opts.ignore_rust_version {
208 state.serialize_field("ignore_rust_version", &true)?;
209 }
210 if self.cargo_opts.unit_graph {
211 state.serialize_field("unit_graph", &true)?;
212 }
213 if !self.cargo_opts.packages.is_empty() {
214 state.serialize_field("packages", &self.cargo_opts.packages)?;
215 }
216 if !self.cargo_opts.bin.is_empty() {
217 state.serialize_field("bin", &self.cargo_opts.bin)?;
218 }
219 if !self.cargo_opts.example.is_empty() {
220 state.serialize_field("example", &self.cargo_opts.example)?;
221 }
222 if !self.cargo_opts.args.is_empty() {
223 state.serialize_field("args", &self.cargo_opts.args)?;
224 }
225 serialize_common_options::<S>(&mut state, &self.cargo_opts.common)?;
226
227 state.end()
228 }
229}
230
231fn default_invoke_address() -> String {
232 DEFAULT_INVOKE_ADDRESS.to_string()
233}
234
235fn default_invoke_port() -> u16 {
236 DEFAULT_INVOKE_PORT
237}
238
239fn default_concurrency() -> usize {
240 1
241}
242
243#[derive(Clone, Debug, Default, Deserialize, Serialize)]
244pub struct WatchConfig {
245 pub router: Option<FunctionRouter>,
246}
247
248#[derive(Clone, Debug, Default)]
249pub struct FunctionRouter {
250 inner: Router<FunctionRoutes>,
251 pub(crate) raw: Vec<Route>,
252}
253
254impl FunctionRouter {
255 pub fn at(
256 &self,
257 path: &str,
258 method: &str,
259 ) -> Result<(String, HashMap<String, String>), MatchError> {
260 let matched = self.inner.at(path)?;
261 let function = matched.value.at(method).ok_or(MatchError::NotFound)?;
262
263 let params = matched
264 .params
265 .iter()
266 .map(|(k, v)| (k.to_string(), v.to_string()))
267 .collect();
268
269 Ok((function.to_string(), params))
270 }
271
272 pub fn insert(&mut self, path: &str, routes: FunctionRoutes) -> Result<(), InsertError> {
273 self.inner.insert(path, routes)
274 }
275
276 pub fn is_empty(&self) -> bool {
277 self.raw.is_empty()
278 }
279}
280
281#[allow(dead_code)]
282fn is_empty_router(router: &Option<FunctionRouter>) -> bool {
283 router.is_none() || router.as_ref().is_some_and(|r| r.is_empty())
284}
285
286#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
287pub struct Route {
288 path: String,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 methods: Option<Vec<String>>,
291 function: String,
292}
293
294#[derive(Clone, Debug, PartialEq)]
295pub enum FunctionRoutes {
296 Single(String),
297 Multiple(HashMap<String, String>),
298}
299
300impl FunctionRoutes {
301 pub fn at(&self, method: &str) -> Option<&str> {
302 match self {
303 FunctionRoutes::Single(function) => Some(function),
304 FunctionRoutes::Multiple(routes) => routes.get(method).map(|s| s.as_str()),
305 }
306 }
307}
308
309struct FunctionRouterVisitor;
310
311impl<'de> Visitor<'de> for FunctionRouterVisitor {
312 type Value = FunctionRouter;
313
314 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
315 formatter.write_str("a map or sequence of function routes")
316 }
317
318 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
319 where
320 A: serde::de::MapAccess<'de>,
321 {
322 let routes: HashMap<String, FunctionRoutes> =
323 Deserialize::deserialize(serde::de::value::MapAccessDeserializer::new(map))?;
324
325 let mut inner = Router::new();
326 let mut raw = Vec::new();
327
328 let mut inverse = HashMap::new();
329
330 for (path, route) in &routes {
331 inner.insert(path, route.clone()).map_err(|e| {
332 serde::de::Error::custom(format!("Failed to insert route {path}: {e}"))
333 })?;
334
335 match route {
336 FunctionRoutes::Single(function) => {
337 raw.push(Route {
338 path: path.clone(),
339 methods: None,
340 function: function.clone(),
341 });
342 }
343 FunctionRoutes::Multiple(routes) => {
344 for (method, function) in routes {
345 inverse
346 .entry((path.clone(), function.clone()))
347 .and_modify(|route: &mut Route| {
348 let mut methods = route.methods.clone().unwrap_or_default();
349 methods.push(method.clone());
350 route.methods = Some(methods);
351 })
352 .or_insert_with(|| Route {
353 path: path.clone(),
354 methods: Some(vec![method.clone()]),
355 function: function.clone(),
356 });
357 }
358 }
359 }
360 }
361
362 for (_, route) in inverse {
363 raw.push(route);
364 }
365
366 Ok(FunctionRouter { inner, raw })
367 }
368
369 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
370 where
371 A: serde::de::SeqAccess<'de>,
372 {
373 let routes: Vec<Route> =
374 Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))?;
375
376 let mut inner = Router::new();
377 let mut raw = Vec::new();
378
379 let mut routes_by_path = HashMap::new();
380
381 for route in &routes {
382 routes_by_path
383 .entry(route.path.clone())
384 .and_modify(|routes| merge_routes(routes, route))
385 .or_insert_with(|| decode_route(route));
386
387 raw.push(route.clone());
388 }
389
390 for (path, route) in &routes_by_path {
391 inner.insert(path, route.clone()).map_err(|e| {
392 serde::de::Error::custom(format!("Failed to insert route {path}: {e}"))
393 })?;
394 }
395
396 Ok(FunctionRouter { inner, raw })
397 }
398}
399
400fn merge_routes(routes: &mut FunctionRoutes, route: &Route) {
401 let methods = route.methods.clone().unwrap_or_default();
402 match routes {
403 FunctionRoutes::Single(function) if !methods.is_empty() => {
404 let mut tmp = HashMap::new();
405 for method in methods {
406 tmp.insert(method.clone(), function.clone());
407 }
408 *routes = FunctionRoutes::Multiple(tmp);
409 }
410 FunctionRoutes::Multiple(_) if methods.is_empty() => {
411 *routes = FunctionRoutes::Single(route.function.clone());
412 }
413 FunctionRoutes::Multiple(routes) => {
414 for method in methods {
415 routes.insert(method.clone(), route.function.clone());
416 }
417 }
418 FunctionRoutes::Single(_) => {
419 *routes = FunctionRoutes::Single(route.function.clone());
420 }
421 }
422}
423
424fn decode_route(route: &Route) -> FunctionRoutes {
425 match &route.methods {
426 Some(methods) => {
427 let mut routes = HashMap::new();
428 for method in methods {
429 routes.insert(method.clone(), route.function.clone());
430 }
431 FunctionRoutes::Multiple(routes)
432 }
433 None => FunctionRoutes::Single(route.function.clone()),
434 }
435}
436
437impl<'de> Deserialize<'de> for FunctionRouter {
438 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
439 where
440 D: serde::Deserializer<'de>,
441 {
442 deserializer.deserialize_any(FunctionRouterVisitor)
443 }
444}
445
446impl Serialize for FunctionRouter {
447 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
448 where
449 S: serde::Serializer,
450 {
451 self.raw.serialize(serializer)
452 }
453}
454
455impl<'de> Deserialize<'de> for FunctionRoutes {
456 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
457 where
458 D: serde::Deserializer<'de>,
459 {
460 let value = Value::deserialize(deserializer)?;
461 match value {
462 Value::String(s) => Ok(FunctionRoutes::Single(s)),
463 Value::Array(arr) => {
464 let mut routes = HashMap::new();
465 for item in arr {
466 let obj = item.as_object().ok_or_else(|| {
467 Error::custom("Array items must be objects with method and function fields")
468 })?;
469
470 let method = obj
471 .get("method")
472 .and_then(|m| m.as_str())
473 .ok_or_else(|| Error::custom("Missing or invalid method field"))?;
474
475 let function = obj
476 .get("function")
477 .and_then(|f| f.as_str())
478 .ok_or_else(|| Error::custom("Missing or invalid function field"))?;
479
480 routes.insert(method.to_string(), function.to_string());
481 }
482 Ok(FunctionRoutes::Multiple(routes))
483 }
484 _ => Err(Error::custom(
485 "Function routes must be either a string or an array of objects with method and function fields",
486 )),
487 }
488 }
489}
490
491impl Serialize for FunctionRoutes {
492 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
493 where
494 S: serde::Serializer,
495 {
496 match self {
497 FunctionRoutes::Single(function) => function.serialize(serializer),
498 FunctionRoutes::Multiple(routes) => {
499 let mut seq = serializer.serialize_seq(Some(routes.len()))?;
500 for (method, function) in routes {
501 let mut map = serde_json::Map::new();
502 map.insert("method".to_string(), json!(method));
503 map.insert("function".to_string(), json!(function));
504 seq.serialize_element(&Value::Object(map))?;
505 }
506 seq.end()
507 }
508 }
509 }
510}
511
512#[cfg(test)]
513mod tests {
514
515 use cargo_options::CommonOptions;
516 use serde_json::{Value, json};
517 use std::path::PathBuf;
518
519 use super::*;
520
521 #[test]
522 fn test_router_deserialize() {
523 let router: FunctionRouter = toml::from_str(
524 r#"
525 "/api/v1/users" = [
526 { function = "get_user", method = "GET" },
527 { function = "create_user", method = "POST" }
528 ]
529 "/api/v1/all_methods" = "all_methods"
530 "#,
531 )
532 .unwrap();
533
534 assert_eq!(
535 router.inner.at("/api/v1/users").unwrap().value,
536 &FunctionRoutes::Multiple(HashMap::from([
537 ("GET".to_string(), "get_user".to_string()),
538 ("POST".to_string(), "create_user".to_string()),
539 ]))
540 );
541
542 assert_eq!(
543 router.inner.at("/api/v1/all_methods").unwrap().value,
544 &FunctionRoutes::Single("all_methods".to_string())
545 );
546 }
547
548 #[test]
549 fn test_router_get() {
550 let router = FunctionRouter::default();
551 assert_eq!(router.at("/api/v1/users", "GET"), Err(MatchError::NotFound));
552
553 let mut inner = Router::new();
554 inner
555 .insert(
556 "/api/v1/users",
557 FunctionRoutes::Single("user_handler".to_string()),
558 )
559 .unwrap();
560 let router = FunctionRouter {
561 inner,
562 ..Default::default()
563 };
564 assert_eq!(
565 router.at("/api/v1/users", "GET"),
566 Ok(("user_handler".to_string(), HashMap::new()))
567 );
568 assert_eq!(
569 router.at("/api/v1/users", "POST"),
570 Ok(("user_handler".to_string(), HashMap::new()))
571 );
572
573 let mut inner = Router::new();
574 inner
575 .insert(
576 "/api/v1/users",
577 FunctionRoutes::Multiple(HashMap::from([
578 ("GET".to_string(), "get_user".to_string()),
579 ("POST".to_string(), "create_user".to_string()),
580 ])),
581 )
582 .unwrap();
583 let router = FunctionRouter {
584 inner,
585 ..Default::default()
586 };
587 assert_eq!(
588 router.at("/api/v1/users", "GET"),
589 Ok(("get_user".to_string(), HashMap::new()))
590 );
591 assert_eq!(
592 router.at("/api/v1/users", "POST"),
593 Ok(("create_user".to_string(), HashMap::new()))
594 );
595 assert_eq!(router.at("/api/v1/users", "PUT"), Err(MatchError::NotFound));
596
597 let mut inner = Router::new();
598 inner
599 .insert(
600 "/api/v1/users/{id}",
601 FunctionRoutes::Single("user_handler".to_string()),
602 )
603 .unwrap();
604 let router = FunctionRouter {
605 inner,
606 ..Default::default()
607 };
608
609 let (function, params) = router.at("/api/v1/users/1", "GET").unwrap();
610 assert_eq!(function, "user_handler");
611 assert_eq!(params, HashMap::from([("id".to_string(), "1".to_string())]));
612 }
613
614 #[test]
615 fn test_router_serialize() {
616 let config = r#"
617 "/api/v1/users" = [
618 { function = "get_user", method = "GET" },
619 { function = "create_user", method = "POST" }
620 ]
621 "/api/v1/all_methods" = "all_methods"
622 "#;
623 let router: FunctionRouter = toml::from_str(config).unwrap();
624
625 let json = serde_json::to_value(&router).unwrap();
626
627 let new_router: FunctionRouter = serde_json::from_value(json).unwrap();
628 assert_eq!(new_router.raw, router.raw);
629
630 assert_eq!(
631 new_router.inner.at("/api/v1/users").unwrap().value,
632 &FunctionRoutes::Multiple(HashMap::from([
633 ("GET".to_string(), "get_user".to_string()),
634 ("POST".to_string(), "create_user".to_string()),
635 ]))
636 );
637
638 assert_eq!(
639 new_router.inner.at("/api/v1/all_methods").unwrap().value,
640 &FunctionRoutes::Single("all_methods".to_string())
641 );
642 }
643
644 #[test]
645 fn test_watch_serialization() {
646 let watch = Watch {
647 invoke_address: "127.0.0.1".to_string(),
648 invoke_port: 9000,
649 env_options: EnvOptions {
650 env_file: Some(PathBuf::from("/tmp/env")),
651 env_var: Some(vec!["FOO=BAR".to_string()]),
652 },
653 tls_options: TlsOptions::new(
654 Some(PathBuf::from("/tmp/cert.pem")),
655 Some(PathBuf::from("/tmp/key.pem")),
656 Some(PathBuf::from("/tmp/ca.pem")),
657 ),
658 cargo_opts: Run {
659 common: CommonOptions {
660 quiet: false,
661 jobs: None,
662 keep_going: false,
663 profile: None,
664 features: vec!["feature1".to_string()],
665 all_features: false,
666 no_default_features: true,
667 target: vec!["x86_64-unknown-linux-gnu".to_string()],
668 target_dir: Some(PathBuf::from("/tmp/target")),
669 message_format: vec!["json".to_string()],
670 verbose: 1,
671 color: Some("auto".to_string()),
672 frozen: true,
673 locked: true,
674 offline: true,
675 config: vec!["config.toml".to_string()],
676 unstable_flags: vec!["flag1".to_string()],
677 timings: None,
678 },
679 manifest_path: None,
680 release: false,
681 ignore_rust_version: false,
682 unit_graph: false,
683 packages: vec![],
684 bin: vec![],
685 example: vec![],
686 args: vec![],
687 },
688 ..Default::default()
689 };
690
691 let json = serde_json::to_value(&watch).unwrap();
692 assert_eq!(json["invoke_address"], "127.0.0.1");
693 assert_eq!(json["invoke_port"], 9000);
694 assert_eq!(json["env_file"], "/tmp/env");
695 assert_eq!(json["env_var"], json!(["FOO=BAR"]));
696 assert_eq!(json["tls_cert"], "/tmp/cert.pem");
697 assert_eq!(json["tls_key"], "/tmp/key.pem");
698 assert_eq!(json["tls_ca"], "/tmp/ca.pem");
699 assert_eq!(json["features"], json!(["feature1"]));
700 assert_eq!(json["no_default_features"], true);
701 assert_eq!(json["target"], json!(["x86_64-unknown-linux-gnu"]));
702 assert_eq!(json["target_dir"], "/tmp/target");
703 assert_eq!(json["message_format"], json!(["json"]));
704 assert_eq!(json["verbose"], 1);
705 assert_eq!(json["color"], "auto");
706 assert_eq!(json["frozen"], true);
707 assert_eq!(json["locked"], true);
708 assert_eq!(json["offline"], true);
709 assert_eq!(json["config"], json!(["config.toml"]));
710 assert_eq!(json["unstable_flags"], json!(["flag1"]));
711 assert_eq!(json["timings"], Value::Null);
712
713 let deserialized: Watch = serde_json::from_value(json).unwrap();
714
715 assert_eq!(deserialized.invoke_address, watch.invoke_address);
716 assert_eq!(deserialized.invoke_port, watch.invoke_port);
717 assert_eq!(
718 deserialized.env_options.env_file,
719 watch.env_options.env_file
720 );
721 assert_eq!(deserialized.env_options.env_var, watch.env_options.env_var);
722 assert_eq!(
723 deserialized.tls_options.tls_cert,
724 watch.tls_options.tls_cert
725 );
726 assert_eq!(deserialized.tls_options.tls_key, watch.tls_options.tls_key);
727 assert_eq!(deserialized.tls_options.tls_ca, watch.tls_options.tls_ca);
728 assert_eq!(
729 deserialized.cargo_opts.common.features,
730 watch.cargo_opts.common.features
731 );
732 assert_eq!(
733 deserialized.cargo_opts.common.no_default_features,
734 watch.cargo_opts.common.no_default_features
735 );
736 assert_eq!(
737 deserialized.cargo_opts.common.target,
738 watch.cargo_opts.common.target
739 );
740 assert_eq!(
741 deserialized.cargo_opts.common.target_dir,
742 watch.cargo_opts.common.target_dir
743 );
744 assert_eq!(
745 deserialized.cargo_opts.common.message_format,
746 watch.cargo_opts.common.message_format
747 );
748 assert_eq!(
749 deserialized.cargo_opts.common.verbose,
750 watch.cargo_opts.common.verbose
751 );
752 assert_eq!(
753 deserialized.cargo_opts.common.color,
754 watch.cargo_opts.common.color
755 );
756 assert_eq!(
757 deserialized.cargo_opts.common.frozen,
758 watch.cargo_opts.common.frozen
759 );
760 assert_eq!(
761 deserialized.cargo_opts.common.locked,
762 watch.cargo_opts.common.locked
763 );
764 assert_eq!(
765 deserialized.cargo_opts.common.offline,
766 watch.cargo_opts.common.offline
767 );
768 assert_eq!(
769 deserialized.cargo_opts.common.config,
770 watch.cargo_opts.common.config
771 );
772 assert_eq!(
773 deserialized.cargo_opts.common.unstable_flags,
774 watch.cargo_opts.common.unstable_flags
775 );
776 assert_eq!(
777 deserialized.cargo_opts.common.timings,
778 watch.cargo_opts.common.timings
779 );
780 }
781}