Skip to main content

cargo_lambda_metadata/cargo/
watch.rs

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    /// Ignore any code changes, and don't reload the function automatically
37    #[arg(long, visible_alias = "no-reload")]
38    #[serde(default)]
39    pub ignore_changes: bool,
40
41    /// Start the Lambda runtime APIs without starting the function.
42    /// This is useful if you start (and debug) your function in your IDE.
43    #[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    /// Address where users send invoke requests
50    pub invoke_address: String,
51
52    /// Address port where users send invoke requests
53    #[arg(short = 'P', long, default_value_t = DEFAULT_INVOKE_PORT)]
54    #[serde(default = "default_invoke_port")]
55    pub invoke_port: u16,
56
57    /// Print OpenTelemetry traces after each function invocation
58    #[arg(long)]
59    #[serde(default)]
60    pub print_traces: bool,
61
62    /// Wait for the first invocation to compile the function
63    #[arg(long, short)]
64    #[serde(default)]
65    pub wait: bool,
66
67    /// Disable the default CORS configuration
68    #[arg(long)]
69    #[serde(default)]
70    pub disable_cors: bool,
71
72    /// How long the invoke request waits for a response
73    #[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    /// Maximum number of concurrent instances per function (default: 1).
90    #[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    /// Returns the package name if there is only one package in the list of `packages`,
108    /// otherwise None.
109    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        // Count non-empty fields
139        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        // Only serialize bool fields that are true
164        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        // Only serialize Some values for Options
187        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        // Flatten the fields from cargo_opts and env_options
198        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}