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