1use core::panic;
6use std::cell::Cell;
7use std::collections::HashMap;
8use std::fs::{self, read_to_string};
9use std::path::{Path, PathBuf};
10use std::rc::Rc;
11use std::str::FromStr;
12#[cfg(any(target_os = "android", target_env = "ohos"))]
13use std::sync::OnceLock;
14use std::{env, fmt};
15
16use bpaf::*;
17use euclid::Size2D;
18use log::warn;
19use serde_json::Value;
20use servo::user_contents::UserStyleSheet;
21use servo::{
22 DeviceIndependentPixel, DiagnosticsLogging, DiagnosticsLoggingOption, Opts, OutputOptions,
23 PrefValue, Preferences,
24};
25use url::Url;
26
27use crate::VERSION;
28
29pub(crate) static EXPERIMENTAL_PREFS: &[&str] = &[
35 "dom_async_clipboard_enabled",
36 "dom_exec_command_enabled",
37 "dom_fontface_enabled",
38 "dom_indexeddb_enabled",
39 "dom_intersection_observer_enabled",
40 "dom_navigator_protocol_handlers_enabled",
41 "dom_notification_enabled",
42 "dom_offscreen_canvas_enabled",
43 "dom_permissions_enabled",
44 "dom_sanitizer_enabled",
45 "dom_storage_manager_api_enabled",
46 "dom_webgl2_enabled",
47 "dom_webgpu_enabled",
48 "layout_columns_enabled",
49 "layout_container_queries_enabled",
50 "layout_grid_enabled",
51 "layout_variable_fonts_enabled",
52];
53
54#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
55#[derive(Clone)]
56pub(crate) struct ServoShellPreferences {
57 pub url: Option<String>,
59 pub device_pixel_ratio_override: Option<f32>,
61 pub clean_shutdown: bool,
63 pub no_native_titlebar: bool,
65 pub homepage: String,
67 pub searchpage: String,
70 pub headless: bool,
73 pub tracing_filter: Option<String>,
78 pub initial_window_size: Size2D<u32, DeviceIndependentPixel>,
80 pub screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>,
83 pub simulate_touch_events: bool,
85 pub output_image_path: Option<String>,
88 pub exit_after_stable_image: bool,
90 pub userscripts_directory: Option<PathBuf>,
93 pub user_stylesheets: Vec<Rc<UserStyleSheet>>,
95 pub webdriver_port: Cell<Option<u16>>,
98 pub experimental_preferences_enabled: bool,
100 #[cfg(target_env = "ohos")]
103 pub log_filter: Option<String>,
104 #[cfg(target_env = "ohos")]
106 pub log_to_file: bool,
107}
108
109impl Default for ServoShellPreferences {
110 fn default() -> Self {
111 Self {
112 clean_shutdown: false,
113 device_pixel_ratio_override: None,
114 headless: false,
115 homepage: "https://servo.org".into(),
116 initial_window_size: Size2D::new(1024, 740),
117 no_native_titlebar: true,
118 screen_size_override: None,
119 simulate_touch_events: false,
120 searchpage: "https://duckduckgo.com/html/?q=%s".into(),
121 tracing_filter: None,
122 url: None,
123 output_image_path: None,
124 exit_after_stable_image: false,
125 userscripts_directory: None,
126 user_stylesheets: Default::default(),
127 webdriver_port: Cell::new(None),
128 #[cfg(target_env = "ohos")]
129 log_filter: None,
130 #[cfg(target_env = "ohos")]
131 log_to_file: false,
132 experimental_preferences_enabled: false,
133 }
134 }
135}
136
137#[cfg(all(
138 unix,
139 not(target_os = "macos"),
140 not(target_os = "ios"),
141 not(target_os = "android"),
142 not(target_env = "ohos")
143))]
144pub fn default_config_dir() -> Option<PathBuf> {
145 let mut config_dir = ::dirs::config_dir().unwrap();
146 config_dir.push("servo");
147 config_dir.push("default");
148 Some(config_dir)
149}
150
151#[cfg(any(target_os = "android", target_env = "ohos"))]
153pub(crate) static DEFAULT_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
154#[cfg(any(target_os = "android", target_env = "ohos"))]
155pub fn default_config_dir() -> Option<PathBuf> {
156 DEFAULT_CONFIG_DIR.get().cloned()
157}
158
159#[cfg(target_os = "macos")]
160pub fn default_config_dir() -> Option<PathBuf> {
161 let mut config_dir = ::dirs::data_dir().unwrap();
164 config_dir.push("Servo");
165 Some(config_dir)
166}
167
168#[cfg(target_os = "windows")]
169pub fn default_config_dir() -> Option<PathBuf> {
170 let mut config_dir = ::dirs::config_dir().unwrap();
171 config_dir.push("Servo");
172 Some(config_dir)
173}
174
175fn get_preferences(prefs_files: &[PathBuf], config_dir: &Option<PathBuf>) -> Preferences {
179 if cfg!(test) {
182 return Preferences::default();
183 }
184
185 let user_prefs_path = config_dir
186 .clone()
187 .map(|path| path.join("prefs.json"))
188 .filter(|path| path.exists());
189 let user_prefs_hash = user_prefs_path.map(read_prefs_file).unwrap_or_default();
190
191 let apply_preferences =
192 |preferences: &mut Preferences, preferences_hash: HashMap<String, PrefValue>| {
193 for (key, value) in preferences_hash.iter() {
194 preferences.set_value(key, value.clone());
195 }
196 };
197
198 let mut preferences = Preferences::default();
199 apply_preferences(&mut preferences, user_prefs_hash);
200 for pref_file_path in prefs_files.iter() {
201 apply_preferences(&mut preferences, read_prefs_file(pref_file_path))
202 }
203
204 preferences
205}
206
207fn read_prefs_file<P: AsRef<Path>>(path: P) -> HashMap<String, PrefValue> {
208 read_prefs_map(&read_to_string(path).expect("Error opening user prefs"))
209}
210
211pub fn read_prefs_map(txt: &str) -> HashMap<String, PrefValue> {
212 let prefs: HashMap<String, Value> = serde_json::from_str(txt)
213 .map_err(|_| panic!("Could not parse preferences JSON"))
214 .unwrap();
215 prefs
216 .into_iter()
217 .map(|(key, value)| {
218 let value = (&value)
219 .try_into()
220 .map_err(|error| panic!("{error}"))
221 .unwrap();
222 (key, value)
223 })
224 .collect()
225}
226
227#[expect(clippy::large_enum_variant)]
228#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
229pub(crate) enum ArgumentParsingResult {
230 ChromeProcess(Opts, Preferences, ServoShellPreferences),
231 ContentProcess(String),
232 Exit,
233 ErrorParsing,
234}
235
236enum ParseResolutionError {
237 InvalidFormat,
238 ZeroDimension,
239 ParseError(std::num::ParseIntError),
240}
241
242impl fmt::Display for ParseResolutionError {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 match self {
245 ParseResolutionError::InvalidFormat => write!(f, "invalid resolution format"),
246 ParseResolutionError::ZeroDimension => {
247 write!(f, "width and height must be greater than 0")
248 },
249 ParseResolutionError::ParseError(e) => write!(f, "{e}"),
250 }
251 }
252}
253
254fn parse_resolution_string(
256 string: String,
257) -> Result<Option<Size2D<u32, DeviceIndependentPixel>>, ParseResolutionError> {
258 if string.is_empty() {
259 Ok(None)
260 } else {
261 let (width, height) = string
262 .split_once(['x', 'X'])
263 .ok_or(ParseResolutionError::InvalidFormat)?;
264
265 let width = width.trim();
266 let height = height.trim();
267 if width.is_empty() || height.is_empty() {
268 return Err(ParseResolutionError::InvalidFormat);
269 }
270
271 let width = width.parse().map_err(ParseResolutionError::ParseError)?;
272 let height = height.parse().map_err(ParseResolutionError::ParseError)?;
273 if width == 0 || height == 0 {
274 return Err(ParseResolutionError::ZeroDimension);
275 }
276
277 Ok(Some(Size2D::new(width, height)))
278 }
279}
280
281fn parse_user_stylesheets(string: String) -> Result<Vec<Rc<UserStyleSheet>>, std::io::Error> {
284 let mut results = Vec::new();
285 for path_string in string.split([' ', ',']) {
286 let path = env::current_dir()?.join(path_string);
287 results.push(Rc::new(UserStyleSheet::new(
288 read_to_string(&path)?,
289 Url::from_file_path(&path).unwrap(),
290 )));
291 }
292 Ok(results)
293}
294
295fn flag_with_default_parser<S, T>(
301 short_cmd: Option<char>,
302 long_cmd: &'static str,
303 argument_help: &'static str,
304 help: &'static str,
305 default: T,
306 transform: fn(S) -> T,
307) -> impl Parser<Option<T>>
308where
309 S: FromStr + 'static,
310 <S as FromStr>::Err: fmt::Display,
311 T: Clone + 'static,
312{
313 let just_flag = if let Some(c) = short_cmd {
314 short(c).long(long_cmd)
315 } else {
316 long(long_cmd)
317 }
318 .req_flag(default)
319 .hide();
320
321 let arg = if let Some(c) = short_cmd {
322 short(c).long(long_cmd)
323 } else {
324 long(long_cmd)
325 }
326 .argument::<S>(argument_help)
327 .help(help)
328 .map(transform);
329
330 construct!([arg, just_flag]).optional()
331}
332
333fn profile() -> impl Parser<Option<OutputOptions>> {
334 flag_with_default_parser(
335 Some('p'),
336 "profile",
337 "",
338 "uses 5.0 output as standard if no argument supplied",
339 OutputOptions::Stdout(5.0),
340 |val: String| {
341 if let Ok(float) = val.parse::<f64>() {
342 OutputOptions::Stdout(float)
343 } else {
344 OutputOptions::FileName(val)
345 }
346 },
347 )
348}
349
350fn userscripts() -> impl Parser<Option<PathBuf>> {
351 let arg = long("userscripts")
352 .argument::<String>("your/directory")
353 .help("Uses userscripts in specified full path")
354 .map(PathBuf::from);
355
356 construct!([arg]).optional()
357}
358
359fn webdriver_port() -> impl Parser<Option<u16>> {
360 flag_with_default_parser(
361 None,
362 "webdriver",
363 "7000",
364 "Start remote WebDriver server on port",
365 7000,
366 |val| val,
367 )
368}
369
370fn map_debug_options(arg: String) -> Vec<String> {
371 arg.split(',').map(|s| s.to_owned()).collect()
372}
373
374#[derive(Bpaf, Clone, Debug)]
375#[bpaf(options, version(VERSION), usage("servoshell [OPTIONS] URL"))]
376struct CmdArgs {
378 #[bpaf(short('B'), long("bhm"))]
380 background_hang_monitor: bool,
381
382 #[bpaf(argument("/home/servo/resources/certs"))]
385 certificate_path: Option<PathBuf>,
386
387 #[bpaf(long)]
389 clean_shutdown: bool,
390
391 #[bpaf(argument("~/.config/servo"))]
394 config_dir: Option<PathBuf>,
395
396 #[bpaf(long)]
398 temporary_storage: bool,
399
400 #[bpaf(argument("servo-ipc-channel.abcdefg"))]
403 content_process: Option<String>,
404
405 #[bpaf(
408 short('Z'),
409 argument("layout_grid_enabled=true,dom_async_clipboard_enabled"),
410 long,
411 map(map_debug_options),
412 fallback(vec![])
413 )]
414 debug: Vec<String>,
415
416 #[bpaf(argument("1.0"))]
419 device_pixel_ratio: Option<f32>,
420
421 #[bpaf(argument("127.0.0.1:7000"))]
423 devtools: Option<String>,
424
425 #[bpaf(long)]
428 enable_experimental_web_platform_features: bool,
429
430 #[bpaf(short('x'), long)]
432 exit: bool,
433
434 #[bpaf(short('I'), long("force-ipc"))]
436 force_ipc: bool,
437
438 #[bpaf(short('f'), long)]
440 hard_fail: bool,
441
442 #[bpaf(short('z'), long)]
444 headless: bool,
445
446 #[bpaf(long("host-file"), argument("/path/to/hosts"))]
450 host_file: Option<PathBuf>,
451
452 #[bpaf(long)]
455 ignore_certificate_errors: bool,
456
457 #[bpaf(short('y'), long, argument("1"))]
459 layout_threads: Option<i64>,
460
461 #[bpaf(argument("~/.local/share/servo"))]
464 local_script_source: Option<PathBuf>,
465
466 #[cfg(target_env = "ohos")]
467 #[bpaf(argument("FILTER"))]
469 log_filter: Option<String>,
470
471 #[cfg(target_env = "ohos")]
472 #[bpaf(long)]
474 log_to_file: bool,
475
476 #[bpaf(short('M'), long)]
478 multiprocess: bool,
479
480 #[bpaf(short('b'), long)]
482 no_native_titlebar: bool,
483
484 #[bpaf(short('o'), argument("test.png"), long)]
487 output: Option<PathBuf>,
488
489 #[bpaf(external)]
492 profile: Option<OutputOptions>,
493
494 #[bpaf(argument("trace.html"), long)]
497 profiler_trace_path: Option<PathBuf>,
498
499 #[bpaf(argument("dom_bluetooth_enabled"), many)]
502 pref: Vec<String>,
503
504 #[bpaf(long, argument("/path/to/prefs.json"), many)]
507 prefs_file: Vec<PathBuf>,
508
509 #[bpaf(argument("0.25"))]
512 random_pipeline_closure_probability: Option<f32>,
513
514 random_pipeline_closure_seed: Option<usize>,
516
517 #[bpaf(short('S'), long)]
519 sandbox: bool,
520
521 shaders: Option<PathBuf>,
523
524 #[bpaf(long("screen-size"), argument::<String>("1024x768"),
527 parse(parse_resolution_string), fallback(None))]
528 screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>,
529
530 #[bpaf(long("simulate-touch-events"))]
533 simulate_touch_events: bool,
534
535 #[bpaf(long("tracing-filter"), argument("FILTER"))]
537 tracing_filter: Option<String>,
538
539 #[bpaf(long)]
541 unminify_js: bool,
542
543 #[bpaf(long)]
545 unminify_css: bool,
546
547 #[bpaf(short('u'),long,argument::<String>("NCSA mosaic/1.0 (X11;SunOS 4.1.4 sun4m"))]
550 user_agent: Option<String>,
551
552 #[bpaf(external)]
555 userscripts: Option<PathBuf>,
556
557 #[bpaf(argument::<String>("file.css"), parse(parse_user_stylesheets),
561 fallback(vec![]))]
562 user_stylesheet: Vec<Rc<UserStyleSheet>>,
563
564 #[bpaf(external)]
566 webdriver_port: Option<u16>,
567
568 #[bpaf(argument::<String>("1024x740"), parse(parse_resolution_string), fallback(None))]
571 window_size: Option<Size2D<u32, DeviceIndependentPixel>>,
572
573 #[bpaf(positional("URL"), fallback(String::from("https://www.servo.org")))]
575 url: String,
576}
577
578fn update_preferences_from_command_line_arguemnts(
579 preferences: &mut Preferences,
580 cmd_args: &CmdArgs,
581) {
582 if let Some(listen_address) = &cmd_args.devtools {
583 preferences.devtools_server_enabled = true;
584 preferences.devtools_server_listen_address = listen_address.clone();
585 }
586
587 if cmd_args.enable_experimental_web_platform_features {
588 for pref in EXPERIMENTAL_PREFS {
589 preferences.set_value(pref, PrefValue::Bool(true));
590 }
591 }
592
593 for pref in &cmd_args.pref {
594 let split: Vec<&str> = pref.splitn(2, '=').collect();
595 let pref_name = split[0];
596 let pref_value = PrefValue::from_booleanish_str(split.get(1).copied().unwrap_or("true"));
597 preferences.set_value(pref_name, pref_value);
598 }
599
600 if let Some(layout_threads) = cmd_args.layout_threads {
601 preferences.layout_threads = layout_threads;
602 }
603
604 if cmd_args.headless && preferences.media_glvideo_enabled {
605 warn!("GL video rendering is not supported on headless windows.");
606 preferences.media_glvideo_enabled = false;
607 }
608
609 if let Some(user_agent) = cmd_args.user_agent.clone() {
610 preferences.user_agent = user_agent;
611 }
612
613 if cmd_args.webdriver_port.is_some() {
614 preferences.dom_testing_html_input_element_select_files_enabled = true;
615 }
616}
617
618pub(crate) fn parse_command_line_arguments<'a>(
623 args_without_binary: impl Into<Args<'a>>,
624) -> ArgumentParsingResult {
625 parse_arguments_helper(args_without_binary.into())
626}
627fn parse_arguments_helper(args_without_binary: Args) -> ArgumentParsingResult {
628 let cmd_args = cmd_args().run_inner(args_without_binary);
629 let cmd_args = match cmd_args {
630 Ok(cmd_args) => cmd_args,
631 Err(error) => {
632 if cfg!(target_os = "android") || cfg!(target_env = "ohos") {
635 match &error {
636 ParseFailure::Stderr(doc) => log::error!("{doc}"),
637 ParseFailure::Stdout(doc, _) => log::error!("{doc}"),
639 ParseFailure::Completion(_) => log::error!("Not supported on these platforms"),
640 }
641 } else {
642 error.print_message(80);
643 }
644
645 return if error.exit_code() == 0 {
646 ArgumentParsingResult::Exit
647 } else {
648 ArgumentParsingResult::ErrorParsing
649 };
650 },
651 };
652
653 if let Some(content_process) = cmd_args.content_process {
655 return ArgumentParsingResult::ContentProcess(content_process);
656 }
657
658 let config_dir = cmd_args
659 .config_dir
660 .clone()
661 .or_else(default_config_dir)
662 .inspect(|config_dir| {
663 if !config_dir.exists() {
664 fs::create_dir_all(config_dir).expect("Could not create config_dir");
665 }
666 });
667 let temporary_storage = cmd_args.temporary_storage;
668 if let Some(ref time_profiler_trace_path) = cmd_args.profiler_trace_path {
669 let mut path = PathBuf::from(time_profiler_trace_path);
670 path.pop();
671 fs::create_dir_all(&path).expect("Error in creating profiler trace path");
672 }
673
674 let mut preferences = get_preferences(&cmd_args.prefs_file, &config_dir);
675
676 update_preferences_from_command_line_arguemnts(&mut preferences, &cmd_args);
677
678 if cfg!(target_os = "android") && cfg!(target_pointer_width = "32") {
680 preferences.js_baseline_interpreter_enabled = false;
681 preferences.js_baseline_jit_enabled = false;
682 preferences.js_ion_enabled = false;
683 }
684
685 let default_window_size = Size2D::new(1024, 740);
687 let default_window_size = cmd_args
688 .screen_size_override
689 .map_or(default_window_size, |screen_size_override| {
690 default_window_size.min(screen_size_override)
691 });
692
693 let servoshell_preferences = ServoShellPreferences {
694 url: Some(cmd_args.url),
695 no_native_titlebar: cmd_args.no_native_titlebar,
696 device_pixel_ratio_override: cmd_args.device_pixel_ratio,
697 clean_shutdown: cmd_args.clean_shutdown,
698 headless: cmd_args.headless,
699 tracing_filter: cmd_args.tracing_filter,
700 initial_window_size: cmd_args.window_size.unwrap_or(default_window_size),
701 screen_size_override: cmd_args.screen_size_override,
702 simulate_touch_events: cmd_args.simulate_touch_events,
703 webdriver_port: Cell::new(cmd_args.webdriver_port),
704 output_image_path: cmd_args.output.map(|p| p.to_string_lossy().into_owned()),
705 exit_after_stable_image: cmd_args.exit,
706 userscripts_directory: cmd_args.userscripts,
707 user_stylesheets: cmd_args.user_stylesheet,
708 experimental_preferences_enabled: cmd_args.enable_experimental_web_platform_features,
709 #[cfg(target_env = "ohos")]
710 log_filter: cmd_args.log_filter.or_else(|| {
711 (!preferences.log_filter.is_empty()).then(|| preferences.log_filter.clone())
712 }),
713 #[cfg(target_env = "ohos")]
714 log_to_file: cmd_args.log_to_file,
715 ..Default::default()
716 };
717
718 let Ok(debug_options) = parse_diagnostics_logging(cmd_args.debug) else {
719 return ArgumentParsingResult::ErrorParsing;
720 };
721
722 let opts = Opts {
723 debug: debug_options,
724 time_profiling: cmd_args.profile,
725 time_profiler_trace_path: cmd_args
726 .profiler_trace_path
727 .map(|p| p.to_string_lossy().into_owned()),
728 hard_fail: cmd_args.hard_fail,
729 multiprocess: cmd_args.multiprocess,
730 background_hang_monitor: cmd_args.background_hang_monitor,
731 sandbox: cmd_args.sandbox,
732 random_pipeline_closure_probability: cmd_args.random_pipeline_closure_probability,
733 random_pipeline_closure_seed: cmd_args.random_pipeline_closure_seed,
734 config_dir,
735 temporary_storage,
736 shaders_path: cmd_args.shaders,
737 certificate_path: cmd_args
738 .certificate_path
739 .map(|p| p.to_string_lossy().into_owned()),
740 host_file: cmd_args.host_file,
741 ignore_certificate_errors: cmd_args.ignore_certificate_errors,
742 unminify_js: cmd_args.unminify_js,
743 local_script_source: cmd_args
744 .local_script_source
745 .map(|p| p.to_string_lossy().into_owned()),
746 unminify_css: cmd_args.unminify_css,
747 force_ipc: cmd_args.force_ipc,
748 };
749
750 ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences)
751}
752
753fn parse_diagnostics_logging(cli_options: Vec<String>) -> Result<DiagnosticsLogging, ()> {
755 fn print_option(name: &str, description: &str) {
756 println!("\t{:<35} {}", name, description);
757 }
758
759 if cli_options.contains(&"help".into()) {
760 println!("Usage: servoshell -Z option,[option,...]\n\twhere options include:");
762 print_option("help", "Show this help message");
763 for option in DiagnosticsLoggingOption::iter() {
764 print_option(option.help_option(), option.help_message())
765 }
766
767 std::process::exit(0);
768 }
769
770 let mut diagnostics_logging = DiagnosticsLogging::new();
771 for cli_option in cli_options.iter() {
772 if let Err(error) = diagnostics_logging.extend_from_string(cli_option) {
773 eprintln!("Could not parse debug logging option: {error}");
774 return Err(());
775 }
776 }
777
778 Ok(diagnostics_logging)
779}
780
781#[cfg(test)]
782fn test_parse_pref(arg: &str) -> Preferences {
783 let args = ["--pref", arg];
784 match parse_command_line_arguments(args.as_slice()) {
785 ArgumentParsingResult::ContentProcess(..) => {
786 unreachable!("No preferences for content process")
787 },
788 ArgumentParsingResult::ChromeProcess(_, preferences, _) => preferences,
789 ArgumentParsingResult::Exit => {
790 panic!("we supplied a --pref argument above which should be parsed")
791 },
792 ArgumentParsingResult::ErrorParsing => {
793 unreachable!("we supplied a --pref argument above which should be parsed")
794 },
795 }
796}
797
798#[test]
799fn test_parse_pref_from_command_line() {
800 let preferences = test_parse_pref("dom_bluetooth_enabled=true");
802 assert!(preferences.dom_bluetooth_enabled);
803
804 let preferences = test_parse_pref("dom_bluetooth_enabled=false");
805 assert!(!preferences.dom_bluetooth_enabled);
806
807 let preferences = test_parse_pref("layout_threads=42");
809 assert_eq!(preferences.layout_threads, 42);
810
811 let preferences = test_parse_pref("network_http_cache_size=50");
813 assert_eq!(preferences.network_http_cache_size, 50);
814 let preferences = test_parse_pref("network_connection_timeout=30");
815 assert_eq!(preferences.network_connection_timeout, 30);
816
817 let preferences = test_parse_pref("fonts_default=Lucida");
819 assert_eq!(preferences.fonts_default, "Lucida");
820
821 let preferences = test_parse_pref("dom_bluetooth_enabled");
823 assert!(preferences.dom_bluetooth_enabled);
824}
825
826#[test]
827fn test_invalid_prefs_from_command_line_panics() {
828 let err_msg = std::panic::catch_unwind(|| {
829 test_parse_pref("doesntexist=true");
830 })
831 .err()
832 .and_then(|a| a.downcast_ref::<String>().cloned())
833 .expect("Should panic");
834 assert_eq!(
835 err_msg, "Unknown preference: \"doesntexist\"",
836 "Message should describe the problem"
837 )
838}
839
840#[test]
841fn test_create_prefs_map() {
842 let json_str = "{
843 \"layout.writing-mode.enabled\": true,
844 \"network.mime.sniff\": false,
845 \"shell.homepage\": \"https://servo.org\"
846 }";
847 assert_eq!(read_prefs_map(json_str).len(), 3);
848}
849
850#[cfg(test)]
851fn test_parse(arg: &str) -> (Opts, Preferences, ServoShellPreferences) {
852 let args_split: Vec<&str> = arg.split_whitespace().collect();
854 match parse_command_line_arguments(args_split.as_slice()) {
855 ArgumentParsingResult::ContentProcess(..) => {
856 unreachable!("No preferences for content process")
857 },
858 ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => {
859 (opts, preferences, servoshell_preferences)
860 },
861 ArgumentParsingResult::Exit | ArgumentParsingResult::ErrorParsing => {
862 unreachable!("We always have valid preference in our test cases")
863 },
864 }
865}
866
867#[test]
868fn test_profiling_args() {
869 assert_eq!(
870 test_parse("-p").0.time_profiling.unwrap(),
871 OutputOptions::Stdout(5_f64)
872 );
873
874 assert_eq!(
875 test_parse("-p 10").0.time_profiling.unwrap(),
876 OutputOptions::Stdout(10_f64)
877 );
878
879 assert_eq!(
880 test_parse("-p 10.0").0.time_profiling.unwrap(),
881 OutputOptions::Stdout(10_f64)
882 );
883
884 assert_eq!(
885 test_parse("-p foo.txt").0.time_profiling.unwrap(),
886 OutputOptions::FileName(String::from("foo.txt"))
887 );
888}
889
890#[test]
891fn test_servoshell_cmd() {
892 assert_eq!(
893 test_parse("--screen-size=1000x1000")
894 .2
895 .screen_size_override
896 .unwrap(),
897 Size2D::new(1000, 1000)
898 );
899
900 assert_eq!(
901 test_parse("--certificate-path=/tmp/test")
902 .0
903 .certificate_path
904 .unwrap(),
905 String::from("/tmp/test")
906 );
907}