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_css_alpha_color_function_enabled",
49 "layout_css_ellipse_corners_enabled",
50 "layout_css_progress_function_enabled",
51 "layout_columns_enabled",
52 "layout_container_queries_enabled",
53 "layout_variable_fonts_enabled",
54];
55
56#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
57#[derive(Clone)]
58pub(crate) struct ServoShellPreferences {
59 pub url: Option<String>,
61 pub device_pixel_ratio_override: Option<f32>,
63 pub clean_shutdown: bool,
65 pub no_native_titlebar: bool,
67 pub homepage: String,
69 pub searchpage: String,
72 pub headless: bool,
75 pub tracing_filter: Option<String>,
80 pub initial_window_size: Size2D<u32, DeviceIndependentPixel>,
82 pub screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>,
85 pub simulate_touch_events: bool,
87 pub output_image_path: Option<String>,
90 pub exit_after_stable_image: bool,
92 pub userscripts_directory: Option<PathBuf>,
95 pub user_stylesheets: Vec<Rc<UserStyleSheet>>,
97 pub webdriver_port: Cell<Option<u16>>,
100 pub experimental_preferences_enabled: bool,
102 #[cfg(target_env = "ohos")]
105 pub log_filter: Option<String>,
106 #[cfg(target_env = "ohos")]
108 pub log_to_file: bool,
109}
110
111impl Default for ServoShellPreferences {
112 fn default() -> Self {
113 Self {
114 clean_shutdown: false,
115 device_pixel_ratio_override: None,
116 headless: false,
117 homepage: "https://servo.org".into(),
118 initial_window_size: Size2D::new(1024, 740),
119 no_native_titlebar: true,
120 screen_size_override: None,
121 simulate_touch_events: false,
122 searchpage: "https://duckduckgo.com/html/?q=%s".into(),
123 tracing_filter: None,
124 url: None,
125 output_image_path: None,
126 exit_after_stable_image: false,
127 userscripts_directory: None,
128 user_stylesheets: Default::default(),
129 webdriver_port: Cell::new(None),
130 #[cfg(target_env = "ohos")]
131 log_filter: None,
132 #[cfg(target_env = "ohos")]
133 log_to_file: false,
134 experimental_preferences_enabled: false,
135 }
136 }
137}
138
139#[cfg(all(
140 unix,
141 not(target_os = "macos"),
142 not(target_os = "ios"),
143 not(target_os = "android"),
144 not(target_env = "ohos")
145))]
146pub fn default_config_dir() -> Option<PathBuf> {
147 let mut config_dir = ::dirs::config_dir().unwrap();
148 config_dir.push("servo");
149 config_dir.push("default");
150 Some(config_dir)
151}
152
153#[cfg(any(target_os = "android", target_env = "ohos"))]
155pub(crate) static DEFAULT_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
156#[cfg(any(target_os = "android", target_env = "ohos"))]
157pub fn default_config_dir() -> Option<PathBuf> {
158 DEFAULT_CONFIG_DIR.get().cloned()
159}
160
161#[cfg(target_os = "macos")]
162pub fn default_config_dir() -> Option<PathBuf> {
163 let mut config_dir = ::dirs::data_dir().unwrap();
166 config_dir.push("Servo");
167 Some(config_dir)
168}
169
170#[cfg(target_os = "windows")]
171pub fn default_config_dir() -> Option<PathBuf> {
172 let mut config_dir = ::dirs::config_dir().unwrap();
173 config_dir.push("Servo");
174 Some(config_dir)
175}
176
177fn get_preferences(prefs_files: &[PathBuf], config_dir: &Option<PathBuf>) -> Preferences {
181 if cfg!(test) {
184 return Preferences::default();
185 }
186
187 let user_prefs_path = config_dir
188 .clone()
189 .map(|path| path.join("prefs.json"))
190 .filter(|path| path.exists());
191 let user_prefs_hash = user_prefs_path.map(read_prefs_file).unwrap_or_default();
192
193 let apply_preferences =
194 |preferences: &mut Preferences, preferences_hash: HashMap<String, PrefValue>| {
195 for (key, value) in preferences_hash.iter() {
196 preferences.set_value(key, value.clone());
197 }
198 };
199
200 let mut preferences = Preferences::default();
201 apply_preferences(&mut preferences, user_prefs_hash);
202 for pref_file_path in prefs_files.iter() {
203 apply_preferences(&mut preferences, read_prefs_file(pref_file_path))
204 }
205
206 preferences
207}
208
209fn read_prefs_file<P: AsRef<Path>>(path: P) -> HashMap<String, PrefValue> {
210 read_prefs_map(&read_to_string(path).expect("Error opening user prefs"))
211}
212
213pub fn read_prefs_map(txt: &str) -> HashMap<String, PrefValue> {
214 let prefs: HashMap<String, Value> = serde_json::from_str(txt)
215 .map_err(|_| panic!("Could not parse preferences JSON"))
216 .unwrap();
217 prefs
218 .into_iter()
219 .map(|(key, value)| {
220 let value = (&value)
221 .try_into()
222 .map_err(|error| panic!("{error}"))
223 .unwrap();
224 (key, value)
225 })
226 .collect()
227}
228
229#[expect(clippy::large_enum_variant)]
230#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
231pub(crate) enum ArgumentParsingResult {
232 ChromeProcess(Opts, Preferences, ServoShellPreferences),
233 ContentProcess(String),
234 Exit,
235 ErrorParsing,
236}
237
238enum ParseResolutionError {
239 InvalidFormat,
240 ZeroDimension,
241 ParseError(std::num::ParseIntError),
242}
243
244impl fmt::Display for ParseResolutionError {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 match self {
247 ParseResolutionError::InvalidFormat => write!(f, "invalid resolution format"),
248 ParseResolutionError::ZeroDimension => {
249 write!(f, "width and height must be greater than 0")
250 },
251 ParseResolutionError::ParseError(e) => write!(f, "{e}"),
252 }
253 }
254}
255
256fn parse_resolution_string(
258 string: String,
259) -> Result<Option<Size2D<u32, DeviceIndependentPixel>>, ParseResolutionError> {
260 if string.is_empty() {
261 Ok(None)
262 } else {
263 let (width, height) = string
264 .split_once(['x', 'X'])
265 .ok_or(ParseResolutionError::InvalidFormat)?;
266
267 let width = width.trim();
268 let height = height.trim();
269 if width.is_empty() || height.is_empty() {
270 return Err(ParseResolutionError::InvalidFormat);
271 }
272
273 let width = width.parse().map_err(ParseResolutionError::ParseError)?;
274 let height = height.parse().map_err(ParseResolutionError::ParseError)?;
275 if width == 0 || height == 0 {
276 return Err(ParseResolutionError::ZeroDimension);
277 }
278
279 Ok(Some(Size2D::new(width, height)))
280 }
281}
282
283fn parse_user_stylesheets(string: String) -> Result<Vec<Rc<UserStyleSheet>>, std::io::Error> {
286 let mut results = Vec::new();
287 for path_string in string.split([' ', ',']) {
288 let path = env::current_dir()?.join(path_string);
289 results.push(Rc::new(UserStyleSheet::new(
290 read_to_string(&path)?,
291 Url::from_file_path(&path).unwrap(),
292 )));
293 }
294 Ok(results)
295}
296
297fn flag_with_default_parser<S, T>(
303 short_cmd: Option<char>,
304 long_cmd: &'static str,
305 argument_help: &'static str,
306 help: &'static str,
307 default: T,
308 transform: fn(S) -> T,
309) -> impl Parser<Option<T>>
310where
311 S: FromStr + 'static,
312 <S as FromStr>::Err: fmt::Display,
313 T: Clone + 'static,
314{
315 let just_flag = if let Some(c) = short_cmd {
316 short(c).long(long_cmd)
317 } else {
318 long(long_cmd)
319 }
320 .req_flag(default)
321 .hide();
322
323 let arg = if let Some(c) = short_cmd {
324 short(c).long(long_cmd)
325 } else {
326 long(long_cmd)
327 }
328 .argument::<S>(argument_help)
329 .help(help)
330 .map(transform);
331
332 construct!([arg, just_flag]).optional()
333}
334
335fn profile() -> impl Parser<Option<OutputOptions>> {
336 flag_with_default_parser(
337 Some('p'),
338 "profile",
339 "",
340 "uses 5.0 output as standard if no argument supplied",
341 OutputOptions::Stdout(5.0),
342 |val: String| {
343 if let Ok(float) = val.parse::<f64>() {
344 OutputOptions::Stdout(float)
345 } else {
346 OutputOptions::FileName(val)
347 }
348 },
349 )
350}
351
352fn userscripts() -> impl Parser<Option<PathBuf>> {
353 let arg = long("userscripts")
354 .argument::<String>("your/directory")
355 .help("Uses userscripts in specified full path")
356 .map(PathBuf::from);
357
358 construct!([arg]).optional()
359}
360
361fn webdriver_port() -> impl Parser<Option<u16>> {
362 flag_with_default_parser(
363 None,
364 "webdriver",
365 "7000",
366 "Start remote WebDriver server on port",
367 7000,
368 |val| val,
369 )
370}
371
372fn map_debug_options(arg: String) -> Vec<String> {
373 arg.split(',').map(|s| s.to_owned()).collect()
374}
375
376#[derive(Bpaf, Clone, Debug)]
377#[bpaf(options, version(VERSION), usage("servoshell [OPTIONS] URL"))]
378struct CmdArgs {
380 #[bpaf(short('B'), long("bhm"))]
382 background_hang_monitor: bool,
383
384 #[bpaf(argument("/home/servo/resources/certs"))]
387 certificate_path: Option<PathBuf>,
388
389 #[bpaf(long)]
391 clean_shutdown: bool,
392
393 #[bpaf(argument("~/.config/servo"))]
396 config_dir: Option<PathBuf>,
397
398 #[bpaf(argument("servo-ipc-channel.abcdefg"))]
401 content_process: Option<String>,
402
403 #[bpaf(
406 short('Z'),
407 argument("layout_grid_enabled=true,dom_async_clipboard_enabled"),
408 long,
409 map(map_debug_options),
410 fallback(vec![])
411 )]
412 debug: Vec<String>,
413
414 #[bpaf(argument("1.0"))]
417 device_pixel_ratio: Option<f32>,
418
419 #[bpaf(argument("127.0.0.1:7000"))]
421 devtools: Option<String>,
422
423 #[bpaf(long)]
426 enable_experimental_web_platform_features: bool,
427
428 #[bpaf(short('x'), long)]
430 exit: bool,
431
432 #[bpaf(short('I'), long("force-ipc"))]
434 force_ipc: bool,
435
436 #[bpaf(short('f'), long)]
438 hard_fail: bool,
439
440 #[bpaf(short('z'), long)]
442 headless: bool,
443
444 #[bpaf(long("host-file"), argument("/path/to/hosts"))]
448 host_file: Option<PathBuf>,
449
450 #[bpaf(long)]
453 ignore_certificate_errors: bool,
454
455 #[bpaf(short('y'), long, argument("1"))]
457 layout_threads: Option<i64>,
458
459 #[bpaf(argument("~/.local/share/servo"))]
462 local_script_source: Option<PathBuf>,
463
464 #[cfg(target_env = "ohos")]
465 #[bpaf(argument("FILTER"))]
467 log_filter: Option<String>,
468
469 #[cfg(target_env = "ohos")]
470 #[bpaf(long)]
472 log_to_file: bool,
473
474 #[bpaf(short('M'), long)]
476 multiprocess: bool,
477
478 #[bpaf(short('b'), long)]
480 no_native_titlebar: bool,
481
482 #[bpaf(short('o'), argument("test.png"), long)]
485 output: Option<PathBuf>,
486
487 #[bpaf(external)]
490 profile: Option<OutputOptions>,
491
492 #[bpaf(argument("trace.html"), long)]
495 profiler_trace_path: Option<PathBuf>,
496
497 #[bpaf(argument("dom_bluetooth_enabled"), many)]
500 pref: Vec<String>,
501
502 #[bpaf(long, argument("/path/to/prefs.json"), many)]
505 prefs_file: Vec<PathBuf>,
506
507 #[bpaf(argument("0.25"))]
510 random_pipeline_closure_probability: Option<f32>,
511
512 random_pipeline_closure_seed: Option<usize>,
514
515 #[bpaf(short('S'), long)]
517 sandbox: bool,
518
519 shaders: Option<PathBuf>,
521
522 #[bpaf(long("screen-size"), argument::<String>("1024x768"),
525 parse(parse_resolution_string), fallback(None))]
526 screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>,
527
528 #[bpaf(long("simulate-touch-events"))]
531 simulate_touch_events: bool,
532
533 #[bpaf(long)]
535 temporary_storage: bool,
536 #[bpaf(long("tracing-filter"), argument("FILTER"))]
538 tracing_filter: Option<String>,
539
540 #[bpaf(long)]
542 unminify_js: bool,
543
544 #[bpaf(long)]
546 unminify_css: bool,
547
548 #[bpaf(short('u'),long,argument::<String>("NCSA mosaic/1.0 (X11;SunOS 4.1.4 sun4m"))]
551 user_agent: Option<String>,
552
553 #[bpaf(external)]
556 userscripts: Option<PathBuf>,
557
558 #[bpaf(argument::<String>("file.css"), parse(parse_user_stylesheets),
562 fallback(vec![]))]
563 user_stylesheet: Vec<Rc<UserStyleSheet>>,
564
565 #[bpaf(external)]
567 webdriver_port: Option<u16>,
568
569 #[bpaf(argument::<String>("1024x740"), parse(parse_resolution_string), fallback(None))]
572 window_size: Option<Size2D<u32, DeviceIndependentPixel>>,
573
574 #[bpaf(long)]
576 zealous_gc: bool,
577
578 #[bpaf(positional("URL"), fallback(String::from("https://www.servo.org")))]
580 url: String,
581}
582
583fn update_preferences_from_command_line_arguments(
584 preferences: &mut Preferences,
585 cmd_args: &CmdArgs,
586) {
587 if let Some(listen_address) = &cmd_args.devtools {
588 preferences.devtools_server_enabled = true;
589 preferences.devtools_server_listen_address = listen_address.clone();
590 }
591
592 if cmd_args.enable_experimental_web_platform_features {
593 for pref in EXPERIMENTAL_PREFS {
594 preferences.set_value(pref, PrefValue::Bool(true));
595 }
596 }
597
598 for pref in &cmd_args.pref {
599 let split: Vec<&str> = pref.splitn(2, '=').collect();
600 let pref_name = split[0];
601 let pref_value = PrefValue::from_booleanish_str(split.get(1).copied().unwrap_or("true"));
602 preferences.set_value(pref_name, pref_value);
603 }
604
605 if let Some(layout_threads) = cmd_args.layout_threads {
606 preferences.layout_threads = layout_threads;
607 }
608
609 if cmd_args.headless && preferences.media_glvideo_enabled {
610 warn!("GL video rendering is not supported on headless windows.");
611 preferences.media_glvideo_enabled = false;
612 }
613
614 if let Some(user_agent) = cmd_args.user_agent.clone() {
615 preferences.user_agent = user_agent;
616 }
617
618 if cmd_args.webdriver_port.is_some() {
619 preferences.dom_testing_html_input_element_select_files_enabled = true;
620 }
621
622 if cmd_args.zealous_gc {
623 #[cfg(not(feature = "debugmozjs"))]
624 warn!(
625 "The zealous-gc option requires Servo to be compiled with debug-mozjs to take effect."
626 );
627
628 preferences.js_mem_gc_zeal_level = 2;
629 preferences.js_mem_gc_zeal_frequency = 1;
630 }
631}
632
633pub(crate) fn parse_command_line_arguments<'a>(
638 args_without_binary: impl Into<Args<'a>>,
639) -> ArgumentParsingResult {
640 parse_arguments_helper(args_without_binary.into())
641}
642fn parse_arguments_helper(args_without_binary: Args) -> ArgumentParsingResult {
643 let cmd_args = cmd_args().run_inner(args_without_binary);
644 let cmd_args = match cmd_args {
645 Ok(cmd_args) => cmd_args,
646 Err(error) => {
647 if cfg!(target_os = "android") || cfg!(target_env = "ohos") {
650 match &error {
651 ParseFailure::Stderr(doc) => log::error!("{doc}"),
652 ParseFailure::Stdout(doc, _) => log::error!("{doc}"),
654 ParseFailure::Completion(_) => log::error!("Not supported on these platforms"),
655 }
656 } else {
657 error.print_message(80);
658 }
659
660 return if error.exit_code() == 0 {
661 ArgumentParsingResult::Exit
662 } else {
663 ArgumentParsingResult::ErrorParsing
664 };
665 },
666 };
667
668 if let Some(content_process) = cmd_args.content_process {
670 return ArgumentParsingResult::ContentProcess(content_process);
671 }
672
673 let config_dir = cmd_args
674 .config_dir
675 .clone()
676 .or_else(default_config_dir)
677 .inspect(|config_dir| {
678 if !config_dir.exists() {
679 fs::create_dir_all(config_dir).expect("Could not create config_dir");
680 }
681 });
682 let temporary_storage = cmd_args.temporary_storage;
683 if let Some(ref time_profiler_trace_path) = cmd_args.profiler_trace_path {
684 let mut path = PathBuf::from(time_profiler_trace_path);
685 path.pop();
686 fs::create_dir_all(&path).expect("Error in creating profiler trace path");
687 }
688
689 let mut preferences = get_preferences(&cmd_args.prefs_file, &config_dir);
690
691 update_preferences_from_command_line_arguments(&mut preferences, &cmd_args);
692
693 if cfg!(target_os = "android") && cfg!(target_pointer_width = "32") {
695 preferences.js_baseline_interpreter_enabled = false;
696 preferences.js_baseline_jit_enabled = false;
697 preferences.js_ion_enabled = false;
698 }
699
700 let default_window_size = Size2D::new(1024, 740);
702 let default_window_size = cmd_args
703 .screen_size_override
704 .map_or(default_window_size, |screen_size_override| {
705 default_window_size.min(screen_size_override)
706 });
707
708 let servoshell_preferences = ServoShellPreferences {
709 url: Some(cmd_args.url),
710 no_native_titlebar: cmd_args.no_native_titlebar,
711 device_pixel_ratio_override: cmd_args.device_pixel_ratio,
712 clean_shutdown: cmd_args.clean_shutdown,
713 headless: cmd_args.headless,
714 tracing_filter: cmd_args.tracing_filter,
715 initial_window_size: cmd_args.window_size.unwrap_or(default_window_size),
716 screen_size_override: cmd_args.screen_size_override,
717 simulate_touch_events: cmd_args.simulate_touch_events,
718 webdriver_port: Cell::new(cmd_args.webdriver_port),
719 output_image_path: cmd_args.output.map(|p| p.to_string_lossy().into_owned()),
720 exit_after_stable_image: cmd_args.exit,
721 userscripts_directory: cmd_args.userscripts,
722 user_stylesheets: cmd_args.user_stylesheet,
723 experimental_preferences_enabled: cmd_args.enable_experimental_web_platform_features,
724 #[cfg(target_env = "ohos")]
725 log_filter: cmd_args.log_filter.or_else(|| {
726 (!preferences.log_filter.is_empty()).then(|| preferences.log_filter.clone())
727 }),
728 #[cfg(target_env = "ohos")]
729 log_to_file: cmd_args.log_to_file,
730 ..Default::default()
731 };
732
733 let Ok(debug_options) = parse_diagnostics_logging(cmd_args.debug) else {
734 return ArgumentParsingResult::ErrorParsing;
735 };
736
737 let opts = Opts {
738 debug: debug_options,
739 time_profiling: cmd_args.profile,
740 time_profiler_trace_path: cmd_args
741 .profiler_trace_path
742 .map(|p| p.to_string_lossy().into_owned()),
743 hard_fail: cmd_args.hard_fail,
744 multiprocess: cmd_args.multiprocess,
745 background_hang_monitor: cmd_args.background_hang_monitor,
746 sandbox: cmd_args.sandbox,
747 random_pipeline_closure_probability: cmd_args.random_pipeline_closure_probability,
748 random_pipeline_closure_seed: cmd_args.random_pipeline_closure_seed,
749 config_dir,
750 temporary_storage,
751 shaders_path: cmd_args.shaders,
752 certificate_path: cmd_args
753 .certificate_path
754 .map(|p| p.to_string_lossy().into_owned()),
755 host_file: cmd_args.host_file,
756 ignore_certificate_errors: cmd_args.ignore_certificate_errors,
757 unminify_js: cmd_args.unminify_js,
758 local_script_source: cmd_args
759 .local_script_source
760 .map(|p| p.to_string_lossy().into_owned()),
761 unminify_css: cmd_args.unminify_css,
762 force_ipc: cmd_args.force_ipc,
763 };
764
765 ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences)
766}
767
768fn parse_diagnostics_logging(cli_options: Vec<String>) -> Result<DiagnosticsLogging, ()> {
770 fn print_option(name: &str, description: &str) {
771 println!("\t{:<35} {}", name, description);
772 }
773
774 if cli_options.contains(&"help".into()) {
775 println!("Usage: servoshell -Z option,[option,...]\n\twhere options include:");
777 print_option("help", "Show this help message");
778 for option in DiagnosticsLoggingOption::iter() {
779 print_option(option.help_option(), option.help_message())
780 }
781
782 std::process::exit(0);
783 }
784
785 let mut diagnostics_logging = DiagnosticsLogging::new();
786 for cli_option in cli_options.iter() {
787 if let Err(error) = diagnostics_logging.extend_from_string(cli_option) {
788 eprintln!("Could not parse debug logging option: {error}");
789 return Err(());
790 }
791 }
792
793 Ok(diagnostics_logging)
794}
795
796#[cfg(test)]
797fn test_parse_pref(arg: &str) -> Preferences {
798 let args = ["--pref", arg];
799 match parse_command_line_arguments(args.as_slice()) {
800 ArgumentParsingResult::ContentProcess(..) => {
801 unreachable!("No preferences for content process")
802 },
803 ArgumentParsingResult::ChromeProcess(_, preferences, _) => preferences,
804 ArgumentParsingResult::Exit => {
805 panic!("we supplied a --pref argument above which should be parsed")
806 },
807 ArgumentParsingResult::ErrorParsing => {
808 unreachable!("we supplied a --pref argument above which should be parsed")
809 },
810 }
811}
812
813#[test]
814fn test_parse_pref_from_command_line() {
815 let preferences = test_parse_pref("dom_bluetooth_enabled=true");
817 assert!(preferences.dom_bluetooth_enabled);
818
819 let preferences = test_parse_pref("dom_bluetooth_enabled=false");
820 assert!(!preferences.dom_bluetooth_enabled);
821
822 let preferences = test_parse_pref("layout_threads=42");
824 assert_eq!(preferences.layout_threads, 42);
825
826 let preferences = test_parse_pref("network_http_cache_size=50");
828 assert_eq!(preferences.network_http_cache_size, 50);
829 let preferences = test_parse_pref("network_connection_timeout=30");
830 assert_eq!(preferences.network_connection_timeout, 30);
831
832 let preferences = test_parse_pref("fonts_default=Lucida");
834 assert_eq!(preferences.fonts_default, "Lucida");
835
836 let preferences = test_parse_pref("dom_bluetooth_enabled");
838 assert!(preferences.dom_bluetooth_enabled);
839}
840
841#[test]
842fn test_invalid_prefs_from_command_line_panics() {
843 let err_msg = std::panic::catch_unwind(|| {
844 test_parse_pref("doesntexist=true");
845 })
846 .err()
847 .and_then(|a| a.downcast_ref::<String>().cloned())
848 .expect("Should panic");
849 assert_eq!(
850 err_msg, "Unknown preference: \"doesntexist\"",
851 "Message should describe the problem"
852 )
853}
854
855#[test]
856fn test_create_prefs_map() {
857 let json_str = "{
858 \"layout.writing-mode.enabled\": true,
859 \"network.mime.sniff\": false,
860 \"shell.homepage\": \"https://servo.org\"
861 }";
862 assert_eq!(read_prefs_map(json_str).len(), 3);
863}
864
865#[cfg(test)]
866fn test_parse(arg: &str) -> (Opts, Preferences, ServoShellPreferences) {
867 let args_split: Vec<&str> = arg.split_whitespace().collect();
869 match parse_command_line_arguments(args_split.as_slice()) {
870 ArgumentParsingResult::ContentProcess(..) => {
871 unreachable!("No preferences for content process")
872 },
873 ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => {
874 (opts, preferences, servoshell_preferences)
875 },
876 ArgumentParsingResult::Exit | ArgumentParsingResult::ErrorParsing => {
877 unreachable!("We always have valid preference in our test cases")
878 },
879 }
880}
881
882#[test]
883fn test_profiling_args() {
884 assert_eq!(
885 test_parse("-p").0.time_profiling.unwrap(),
886 OutputOptions::Stdout(5_f64)
887 );
888
889 assert_eq!(
890 test_parse("-p 10").0.time_profiling.unwrap(),
891 OutputOptions::Stdout(10_f64)
892 );
893
894 assert_eq!(
895 test_parse("-p 10.0").0.time_profiling.unwrap(),
896 OutputOptions::Stdout(10_f64)
897 );
898
899 assert_eq!(
900 test_parse("-p foo.txt").0.time_profiling.unwrap(),
901 OutputOptions::FileName(String::from("foo.txt"))
902 );
903}
904
905#[test]
906fn test_servoshell_cmd() {
907 assert_eq!(
908 test_parse("--screen-size=1000x1000")
909 .2
910 .screen_size_override
911 .unwrap(),
912 Size2D::new(1000, 1000)
913 );
914
915 assert_eq!(
916 test_parse("--certificate-path=/tmp/test")
917 .0
918 .certificate_path
919 .unwrap(),
920 String::from("/tmp/test")
921 );
922
923 assert!({
924 let p = test_parse("--zealous-gc").1;
925 p.js_mem_gc_zeal_level == 2 && p.js_mem_gc_zeal_frequency == 1
926 });
927}