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_attr_enabled",
49 "layout_columns_enabled",
50 "layout_container_queries_enabled",
51 "layout_grid_enabled",
52 "layout_variable_fonts_enabled",
53];
54
55#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
56#[derive(Clone)]
57pub(crate) struct ServoShellPreferences {
58 pub url: Option<String>,
60 pub device_pixel_ratio_override: Option<f32>,
62 pub clean_shutdown: bool,
64 pub no_native_titlebar: bool,
66 pub homepage: String,
68 pub searchpage: String,
71 pub headless: bool,
74 pub tracing_filter: Option<String>,
79 pub initial_window_size: Size2D<u32, DeviceIndependentPixel>,
81 pub screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>,
84 pub simulate_touch_events: bool,
86 pub output_image_path: Option<String>,
89 pub exit_after_stable_image: bool,
91 pub userscripts_directory: Option<PathBuf>,
94 pub user_stylesheets: Vec<Rc<UserStyleSheet>>,
96 pub webdriver_port: Cell<Option<u16>>,
99 pub experimental_preferences_enabled: bool,
101 #[cfg(target_env = "ohos")]
104 pub log_filter: Option<String>,
105 #[cfg(target_env = "ohos")]
107 pub log_to_file: bool,
108}
109
110impl Default for ServoShellPreferences {
111 fn default() -> Self {
112 Self {
113 clean_shutdown: false,
114 device_pixel_ratio_override: None,
115 headless: false,
116 homepage: "https://servo.org".into(),
117 initial_window_size: Size2D::new(1024, 740),
118 no_native_titlebar: true,
119 screen_size_override: None,
120 simulate_touch_events: false,
121 searchpage: "https://duckduckgo.com/html/?q=%s".into(),
122 tracing_filter: None,
123 url: None,
124 output_image_path: None,
125 exit_after_stable_image: false,
126 userscripts_directory: None,
127 user_stylesheets: Default::default(),
128 webdriver_port: Cell::new(None),
129 #[cfg(target_env = "ohos")]
130 log_filter: None,
131 #[cfg(target_env = "ohos")]
132 log_to_file: false,
133 experimental_preferences_enabled: false,
134 }
135 }
136}
137
138#[cfg(all(
139 unix,
140 not(target_os = "macos"),
141 not(target_os = "ios"),
142 not(target_os = "android"),
143 not(target_env = "ohos")
144))]
145pub fn default_config_dir() -> Option<PathBuf> {
146 let mut config_dir = ::dirs::config_dir().unwrap();
147 config_dir.push("servo");
148 config_dir.push("default");
149 Some(config_dir)
150}
151
152#[cfg(any(target_os = "android", target_env = "ohos"))]
154pub(crate) static DEFAULT_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
155#[cfg(any(target_os = "android", target_env = "ohos"))]
156pub fn default_config_dir() -> Option<PathBuf> {
157 DEFAULT_CONFIG_DIR.get().cloned()
158}
159
160#[cfg(target_os = "macos")]
161pub fn default_config_dir() -> Option<PathBuf> {
162 let mut config_dir = ::dirs::data_dir().unwrap();
165 config_dir.push("Servo");
166 Some(config_dir)
167}
168
169#[cfg(target_os = "windows")]
170pub fn default_config_dir() -> Option<PathBuf> {
171 let mut config_dir = ::dirs::config_dir().unwrap();
172 config_dir.push("Servo");
173 Some(config_dir)
174}
175
176fn get_preferences(prefs_files: &[PathBuf], config_dir: &Option<PathBuf>) -> Preferences {
180 if cfg!(test) {
183 return Preferences::default();
184 }
185
186 let user_prefs_path = config_dir
187 .clone()
188 .map(|path| path.join("prefs.json"))
189 .filter(|path| path.exists());
190 let user_prefs_hash = user_prefs_path.map(read_prefs_file).unwrap_or_default();
191
192 let apply_preferences =
193 |preferences: &mut Preferences, preferences_hash: HashMap<String, PrefValue>| {
194 for (key, value) in preferences_hash.iter() {
195 preferences.set_value(key, value.clone());
196 }
197 };
198
199 let mut preferences = Preferences::default();
200 apply_preferences(&mut preferences, user_prefs_hash);
201 for pref_file_path in prefs_files.iter() {
202 apply_preferences(&mut preferences, read_prefs_file(pref_file_path))
203 }
204
205 preferences
206}
207
208fn read_prefs_file<P: AsRef<Path>>(path: P) -> HashMap<String, PrefValue> {
209 read_prefs_map(&read_to_string(path).expect("Error opening user prefs"))
210}
211
212pub fn read_prefs_map(txt: &str) -> HashMap<String, PrefValue> {
213 let prefs: HashMap<String, Value> = serde_json::from_str(txt)
214 .map_err(|_| panic!("Could not parse preferences JSON"))
215 .unwrap();
216 prefs
217 .into_iter()
218 .map(|(key, value)| {
219 let value = (&value)
220 .try_into()
221 .map_err(|error| panic!("{error}"))
222 .unwrap();
223 (key, value)
224 })
225 .collect()
226}
227
228#[expect(clippy::large_enum_variant)]
229#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
230pub(crate) enum ArgumentParsingResult {
231 ChromeProcess(Opts, Preferences, ServoShellPreferences),
232 ContentProcess(String),
233 Exit,
234 ErrorParsing,
235}
236
237enum ParseResolutionError {
238 InvalidFormat,
239 ZeroDimension,
240 ParseError(std::num::ParseIntError),
241}
242
243impl fmt::Display for ParseResolutionError {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 match self {
246 ParseResolutionError::InvalidFormat => write!(f, "invalid resolution format"),
247 ParseResolutionError::ZeroDimension => {
248 write!(f, "width and height must be greater than 0")
249 },
250 ParseResolutionError::ParseError(e) => write!(f, "{e}"),
251 }
252 }
253}
254
255fn parse_resolution_string(
257 string: String,
258) -> Result<Option<Size2D<u32, DeviceIndependentPixel>>, ParseResolutionError> {
259 if string.is_empty() {
260 Ok(None)
261 } else {
262 let (width, height) = string
263 .split_once(['x', 'X'])
264 .ok_or(ParseResolutionError::InvalidFormat)?;
265
266 let width = width.trim();
267 let height = height.trim();
268 if width.is_empty() || height.is_empty() {
269 return Err(ParseResolutionError::InvalidFormat);
270 }
271
272 let width = width.parse().map_err(ParseResolutionError::ParseError)?;
273 let height = height.parse().map_err(ParseResolutionError::ParseError)?;
274 if width == 0 || height == 0 {
275 return Err(ParseResolutionError::ZeroDimension);
276 }
277
278 Ok(Some(Size2D::new(width, height)))
279 }
280}
281
282fn parse_user_stylesheets(string: String) -> Result<Vec<Rc<UserStyleSheet>>, std::io::Error> {
285 let mut results = Vec::new();
286 for path_string in string.split([' ', ',']) {
287 let path = env::current_dir()?.join(path_string);
288 results.push(Rc::new(UserStyleSheet::new(
289 read_to_string(&path)?,
290 Url::from_file_path(&path).unwrap(),
291 )));
292 }
293 Ok(results)
294}
295
296fn flag_with_default_parser<S, T>(
302 short_cmd: Option<char>,
303 long_cmd: &'static str,
304 argument_help: &'static str,
305 help: &'static str,
306 default: T,
307 transform: fn(S) -> T,
308) -> impl Parser<Option<T>>
309where
310 S: FromStr + 'static,
311 <S as FromStr>::Err: fmt::Display,
312 T: Clone + 'static,
313{
314 let just_flag = if let Some(c) = short_cmd {
315 short(c).long(long_cmd)
316 } else {
317 long(long_cmd)
318 }
319 .req_flag(default)
320 .hide();
321
322 let arg = if let Some(c) = short_cmd {
323 short(c).long(long_cmd)
324 } else {
325 long(long_cmd)
326 }
327 .argument::<S>(argument_help)
328 .help(help)
329 .map(transform);
330
331 construct!([arg, just_flag]).optional()
332}
333
334fn profile() -> impl Parser<Option<OutputOptions>> {
335 flag_with_default_parser(
336 Some('p'),
337 "profile",
338 "",
339 "uses 5.0 output as standard if no argument supplied",
340 OutputOptions::Stdout(5.0),
341 |val: String| {
342 if let Ok(float) = val.parse::<f64>() {
343 OutputOptions::Stdout(float)
344 } else {
345 OutputOptions::FileName(val)
346 }
347 },
348 )
349}
350
351fn userscripts() -> impl Parser<Option<PathBuf>> {
352 let arg = long("userscripts")
353 .argument::<String>("your/directory")
354 .help("Uses userscripts in specified full path")
355 .map(PathBuf::from);
356
357 construct!([arg]).optional()
358}
359
360fn webdriver_port() -> impl Parser<Option<u16>> {
361 flag_with_default_parser(
362 None,
363 "webdriver",
364 "7000",
365 "Start remote WebDriver server on port",
366 7000,
367 |val| val,
368 )
369}
370
371fn map_debug_options(arg: String) -> Vec<String> {
372 arg.split(',').map(|s| s.to_owned()).collect()
373}
374
375#[derive(Bpaf, Clone, Debug)]
376#[bpaf(options, version(VERSION), usage("servoshell [OPTIONS] URL"))]
377struct CmdArgs {
379 #[bpaf(short('B'), long("bhm"))]
381 background_hang_monitor: bool,
382
383 #[bpaf(argument("/home/servo/resources/certs"))]
386 certificate_path: Option<PathBuf>,
387
388 #[bpaf(long)]
390 clean_shutdown: bool,
391
392 #[bpaf(argument("~/.config/servo"))]
395 config_dir: Option<PathBuf>,
396
397 #[bpaf(long)]
399 temporary_storage: bool,
400
401 #[bpaf(argument("servo-ipc-channel.abcdefg"))]
404 content_process: Option<String>,
405
406 #[bpaf(
409 short('Z'),
410 argument("layout_grid_enabled=true,dom_async_clipboard_enabled"),
411 long,
412 map(map_debug_options),
413 fallback(vec![])
414 )]
415 debug: Vec<String>,
416
417 #[bpaf(argument("1.0"))]
420 device_pixel_ratio: Option<f32>,
421
422 #[bpaf(argument("127.0.0.1:7000"))]
424 devtools: Option<String>,
425
426 #[bpaf(long)]
429 enable_experimental_web_platform_features: bool,
430
431 #[bpaf(short('x'), long)]
433 exit: bool,
434
435 #[bpaf(short('I'), long("force-ipc"))]
437 force_ipc: bool,
438
439 #[bpaf(short('f'), long)]
441 hard_fail: bool,
442
443 #[bpaf(short('z'), long)]
445 headless: bool,
446
447 #[bpaf(long("host-file"), argument("/path/to/hosts"))]
451 host_file: Option<PathBuf>,
452
453 #[bpaf(long)]
456 ignore_certificate_errors: bool,
457
458 #[bpaf(short('y'), long, argument("1"))]
460 layout_threads: Option<i64>,
461
462 #[bpaf(argument("~/.local/share/servo"))]
465 local_script_source: Option<PathBuf>,
466
467 #[cfg(target_env = "ohos")]
468 #[bpaf(argument("FILTER"))]
470 log_filter: Option<String>,
471
472 #[cfg(target_env = "ohos")]
473 #[bpaf(long)]
475 log_to_file: bool,
476
477 #[bpaf(short('M'), long)]
479 multiprocess: bool,
480
481 #[bpaf(short('b'), long)]
483 no_native_titlebar: bool,
484
485 #[bpaf(short('o'), argument("test.png"), long)]
488 output: Option<PathBuf>,
489
490 #[bpaf(external)]
493 profile: Option<OutputOptions>,
494
495 #[bpaf(argument("trace.html"), long)]
498 profiler_trace_path: Option<PathBuf>,
499
500 #[bpaf(argument("dom_bluetooth_enabled"), many)]
503 pref: Vec<String>,
504
505 #[bpaf(long, argument("/path/to/prefs.json"), many)]
508 prefs_file: Vec<PathBuf>,
509
510 #[bpaf(argument("0.25"))]
513 random_pipeline_closure_probability: Option<f32>,
514
515 random_pipeline_closure_seed: Option<usize>,
517
518 #[bpaf(short('S'), long)]
520 sandbox: bool,
521
522 shaders: Option<PathBuf>,
524
525 #[bpaf(long("screen-size"), argument::<String>("1024x768"),
528 parse(parse_resolution_string), fallback(None))]
529 screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>,
530
531 #[bpaf(long("simulate-touch-events"))]
534 simulate_touch_events: bool,
535
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}