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_ellipse_corners_enabled",
49 "layout_css_progress_function_enabled",
50 "layout_columns_enabled",
51 "layout_container_queries_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(argument("servo-ipc-channel.abcdefg"))]
400 content_process: Option<String>,
401
402 #[bpaf(
405 short('Z'),
406 argument("layout_grid_enabled=true,dom_async_clipboard_enabled"),
407 long,
408 map(map_debug_options),
409 fallback(vec![])
410 )]
411 debug: Vec<String>,
412
413 #[bpaf(argument("1.0"))]
416 device_pixel_ratio: Option<f32>,
417
418 #[bpaf(argument("127.0.0.1:7000"))]
420 devtools: Option<String>,
421
422 #[bpaf(long)]
425 enable_experimental_web_platform_features: bool,
426
427 #[bpaf(short('x'), long)]
429 exit: bool,
430
431 #[bpaf(short('I'), long("force-ipc"))]
433 force_ipc: bool,
434
435 #[bpaf(short('f'), long)]
437 hard_fail: bool,
438
439 #[bpaf(short('z'), long)]
441 headless: bool,
442
443 #[bpaf(long("host-file"), argument("/path/to/hosts"))]
447 host_file: Option<PathBuf>,
448
449 #[bpaf(long)]
452 ignore_certificate_errors: bool,
453
454 #[bpaf(short('y'), long, argument("1"))]
456 layout_threads: Option<i64>,
457
458 #[bpaf(argument("~/.local/share/servo"))]
461 local_script_source: Option<PathBuf>,
462
463 #[cfg(target_env = "ohos")]
464 #[bpaf(argument("FILTER"))]
466 log_filter: Option<String>,
467
468 #[cfg(target_env = "ohos")]
469 #[bpaf(long)]
471 log_to_file: bool,
472
473 #[bpaf(short('M'), long)]
475 multiprocess: bool,
476
477 #[bpaf(short('b'), long)]
479 no_native_titlebar: bool,
480
481 #[bpaf(short('o'), argument("test.png"), long)]
484 output: Option<PathBuf>,
485
486 #[bpaf(external)]
489 profile: Option<OutputOptions>,
490
491 #[bpaf(argument("trace.html"), long)]
494 profiler_trace_path: Option<PathBuf>,
495
496 #[bpaf(argument("dom_bluetooth_enabled"), many)]
499 pref: Vec<String>,
500
501 #[bpaf(long, argument("/path/to/prefs.json"), many)]
504 prefs_file: Vec<PathBuf>,
505
506 #[bpaf(argument("0.25"))]
509 random_pipeline_closure_probability: Option<f32>,
510
511 random_pipeline_closure_seed: Option<usize>,
513
514 #[bpaf(short('S'), long)]
516 sandbox: bool,
517
518 shaders: Option<PathBuf>,
520
521 #[bpaf(long("screen-size"), argument::<String>("1024x768"),
524 parse(parse_resolution_string), fallback(None))]
525 screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>,
526
527 #[bpaf(long("simulate-touch-events"))]
530 simulate_touch_events: bool,
531
532 #[bpaf(long)]
534 temporary_storage: bool,
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(long)]
575 zealous_gc: bool,
576
577 #[bpaf(positional("URL"), fallback(String::from("https://www.servo.org")))]
579 url: String,
580}
581
582fn update_preferences_from_command_line_arguments(
583 preferences: &mut Preferences,
584 cmd_args: &CmdArgs,
585) {
586 if let Some(listen_address) = &cmd_args.devtools {
587 preferences.devtools_server_enabled = true;
588 preferences.devtools_server_listen_address = listen_address.clone();
589 }
590
591 if cmd_args.enable_experimental_web_platform_features {
592 for pref in EXPERIMENTAL_PREFS {
593 preferences.set_value(pref, PrefValue::Bool(true));
594 }
595 }
596
597 for pref in &cmd_args.pref {
598 let split: Vec<&str> = pref.splitn(2, '=').collect();
599 let pref_name = split[0];
600 let pref_value = PrefValue::from_booleanish_str(split.get(1).copied().unwrap_or("true"));
601 preferences.set_value(pref_name, pref_value);
602 }
603
604 if let Some(layout_threads) = cmd_args.layout_threads {
605 preferences.layout_threads = layout_threads;
606 }
607
608 if cmd_args.headless && preferences.media_glvideo_enabled {
609 warn!("GL video rendering is not supported on headless windows.");
610 preferences.media_glvideo_enabled = false;
611 }
612
613 if let Some(user_agent) = cmd_args.user_agent.clone() {
614 preferences.user_agent = user_agent;
615 }
616
617 if cmd_args.webdriver_port.is_some() {
618 preferences.dom_testing_html_input_element_select_files_enabled = true;
619 }
620
621 if cmd_args.zealous_gc {
622 #[cfg(not(feature = "debugmozjs"))]
623 warn!(
624 "The zealous-gc option requires Servo to be compiled with debug-mozjs to take effect."
625 );
626
627 preferences.js_mem_gc_zeal_level = 2;
628 preferences.js_mem_gc_zeal_frequency = 1;
629 }
630}
631
632pub(crate) fn parse_command_line_arguments<'a>(
637 args_without_binary: impl Into<Args<'a>>,
638) -> ArgumentParsingResult {
639 parse_arguments_helper(args_without_binary.into())
640}
641fn parse_arguments_helper(args_without_binary: Args) -> ArgumentParsingResult {
642 let cmd_args = cmd_args().run_inner(args_without_binary);
643 let cmd_args = match cmd_args {
644 Ok(cmd_args) => cmd_args,
645 Err(error) => {
646 if cfg!(target_os = "android") || cfg!(target_env = "ohos") {
649 match &error {
650 ParseFailure::Stderr(doc) => log::error!("{doc}"),
651 ParseFailure::Stdout(doc, _) => log::error!("{doc}"),
653 ParseFailure::Completion(_) => log::error!("Not supported on these platforms"),
654 }
655 } else {
656 error.print_message(80);
657 }
658
659 return if error.exit_code() == 0 {
660 ArgumentParsingResult::Exit
661 } else {
662 ArgumentParsingResult::ErrorParsing
663 };
664 },
665 };
666
667 if let Some(content_process) = cmd_args.content_process {
669 return ArgumentParsingResult::ContentProcess(content_process);
670 }
671
672 let config_dir = cmd_args
673 .config_dir
674 .clone()
675 .or_else(default_config_dir)
676 .inspect(|config_dir| {
677 if !config_dir.exists() {
678 fs::create_dir_all(config_dir).expect("Could not create config_dir");
679 }
680 });
681 let temporary_storage = cmd_args.temporary_storage;
682 if let Some(ref time_profiler_trace_path) = cmd_args.profiler_trace_path {
683 let mut path = PathBuf::from(time_profiler_trace_path);
684 path.pop();
685 fs::create_dir_all(&path).expect("Error in creating profiler trace path");
686 }
687
688 let mut preferences = get_preferences(&cmd_args.prefs_file, &config_dir);
689
690 update_preferences_from_command_line_arguments(&mut preferences, &cmd_args);
691
692 if cfg!(target_os = "android") && cfg!(target_pointer_width = "32") {
694 preferences.js_baseline_interpreter_enabled = false;
695 preferences.js_baseline_jit_enabled = false;
696 preferences.js_ion_enabled = false;
697 }
698
699 let default_window_size = Size2D::new(1024, 740);
701 let default_window_size = cmd_args
702 .screen_size_override
703 .map_or(default_window_size, |screen_size_override| {
704 default_window_size.min(screen_size_override)
705 });
706
707 let servoshell_preferences = ServoShellPreferences {
708 url: Some(cmd_args.url),
709 no_native_titlebar: cmd_args.no_native_titlebar,
710 device_pixel_ratio_override: cmd_args.device_pixel_ratio,
711 clean_shutdown: cmd_args.clean_shutdown,
712 headless: cmd_args.headless,
713 tracing_filter: cmd_args.tracing_filter,
714 initial_window_size: cmd_args.window_size.unwrap_or(default_window_size),
715 screen_size_override: cmd_args.screen_size_override,
716 simulate_touch_events: cmd_args.simulate_touch_events,
717 webdriver_port: Cell::new(cmd_args.webdriver_port),
718 output_image_path: cmd_args.output.map(|p| p.to_string_lossy().into_owned()),
719 exit_after_stable_image: cmd_args.exit,
720 userscripts_directory: cmd_args.userscripts,
721 user_stylesheets: cmd_args.user_stylesheet,
722 experimental_preferences_enabled: cmd_args.enable_experimental_web_platform_features,
723 #[cfg(target_env = "ohos")]
724 log_filter: cmd_args.log_filter.or_else(|| {
725 (!preferences.log_filter.is_empty()).then(|| preferences.log_filter.clone())
726 }),
727 #[cfg(target_env = "ohos")]
728 log_to_file: cmd_args.log_to_file,
729 ..Default::default()
730 };
731
732 let Ok(debug_options) = parse_diagnostics_logging(cmd_args.debug) else {
733 return ArgumentParsingResult::ErrorParsing;
734 };
735
736 #[cfg(not(feature = "multiprocess"))]
737 if cmd_args.multiprocess || cmd_args.force_ipc {
738 log::error!("IPC was disabled at compile time. IPC and multiprocess modes are disabled");
739 }
740
741 let opts = Opts {
742 debug: debug_options,
743 time_profiling: cmd_args.profile,
744 time_profiler_trace_path: cmd_args
745 .profiler_trace_path
746 .map(|p| p.to_string_lossy().into_owned()),
747 hard_fail: cmd_args.hard_fail,
748 multiprocess: cmd_args.multiprocess,
749 background_hang_monitor: cmd_args.background_hang_monitor,
750 sandbox: cmd_args.sandbox,
751 random_pipeline_closure_probability: cmd_args.random_pipeline_closure_probability,
752 random_pipeline_closure_seed: cmd_args.random_pipeline_closure_seed,
753 config_dir,
754 temporary_storage,
755 shaders_path: cmd_args.shaders,
756 certificate_path: cmd_args
757 .certificate_path
758 .map(|p| p.to_string_lossy().into_owned()),
759 host_file: cmd_args.host_file,
760 ignore_certificate_errors: cmd_args.ignore_certificate_errors,
761 unminify_js: cmd_args.unminify_js,
762 local_script_source: cmd_args
763 .local_script_source
764 .map(|p| p.to_string_lossy().into_owned()),
765 unminify_css: cmd_args.unminify_css,
766 force_ipc: cmd_args.force_ipc,
767 };
768
769 ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences)
770}
771
772fn parse_diagnostics_logging(cli_options: Vec<String>) -> Result<DiagnosticsLogging, ()> {
774 fn print_option(name: &str, description: &str) {
775 println!("\t{:<35} {}", name, description);
776 }
777
778 if cli_options.contains(&"help".into()) {
779 println!("Usage: servoshell -Z option,[option,...]\n\twhere options include:");
781 print_option("help", "Show this help message");
782 for option in DiagnosticsLoggingOption::iter() {
783 print_option(option.help_option(), option.help_message())
784 }
785
786 std::process::exit(0);
787 }
788
789 let mut diagnostics_logging = DiagnosticsLogging::new();
790 for cli_option in cli_options.iter() {
791 if let Err(error) = diagnostics_logging.extend_from_string(cli_option) {
792 eprintln!("Could not parse debug logging option: {error}");
793 return Err(());
794 }
795 }
796
797 Ok(diagnostics_logging)
798}
799
800#[cfg(test)]
801fn test_parse_pref(arg: &str) -> Preferences {
802 let args = ["--pref", arg];
803 match parse_command_line_arguments(args.as_slice()) {
804 ArgumentParsingResult::ContentProcess(..) => {
805 unreachable!("No preferences for content process")
806 },
807 ArgumentParsingResult::ChromeProcess(_, preferences, _) => preferences,
808 ArgumentParsingResult::Exit => {
809 panic!("we supplied a --pref argument above which should be parsed")
810 },
811 ArgumentParsingResult::ErrorParsing => {
812 unreachable!("we supplied a --pref argument above which should be parsed")
813 },
814 }
815}
816
817#[test]
818fn test_parse_pref_from_command_line() {
819 let preferences = test_parse_pref("dom_bluetooth_enabled=true");
821 assert!(preferences.dom_bluetooth_enabled);
822
823 let preferences = test_parse_pref("dom_bluetooth_enabled=false");
824 assert!(!preferences.dom_bluetooth_enabled);
825
826 let preferences = test_parse_pref("layout_threads=42");
828 assert_eq!(preferences.layout_threads, 42);
829
830 let preferences = test_parse_pref("network_http_cache_size=50");
832 assert_eq!(preferences.network_http_cache_size, 50);
833 let preferences = test_parse_pref("network_connection_timeout=30");
834 assert_eq!(preferences.network_connection_timeout, 30);
835
836 let preferences = test_parse_pref("fonts_default=Lucida");
838 assert_eq!(preferences.fonts_default, "Lucida");
839
840 let preferences = test_parse_pref("dom_bluetooth_enabled");
842 assert!(preferences.dom_bluetooth_enabled);
843}
844
845#[test]
846fn test_invalid_prefs_from_command_line_panics() {
847 let err_msg = std::panic::catch_unwind(|| {
848 test_parse_pref("doesntexist=true");
849 })
850 .err()
851 .and_then(|a| a.downcast_ref::<String>().cloned())
852 .expect("Should panic");
853 assert_eq!(
854 err_msg, "Unknown preference: \"doesntexist\"",
855 "Message should describe the problem"
856 )
857}
858
859#[test]
860fn test_create_prefs_map() {
861 let json_str = "{
862 \"layout.writing-mode.enabled\": true,
863 \"network.mime.sniff\": false,
864 \"shell.homepage\": \"https://servo.org\"
865 }";
866 assert_eq!(read_prefs_map(json_str).len(), 3);
867}
868
869#[cfg(test)]
870fn test_parse(arg: &str) -> (Opts, Preferences, ServoShellPreferences) {
871 let args_split: Vec<&str> = arg.split_whitespace().collect();
873 match parse_command_line_arguments(args_split.as_slice()) {
874 ArgumentParsingResult::ContentProcess(..) => {
875 unreachable!("No preferences for content process")
876 },
877 ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => {
878 (opts, preferences, servoshell_preferences)
879 },
880 ArgumentParsingResult::Exit | ArgumentParsingResult::ErrorParsing => {
881 unreachable!("We always have valid preference in our test cases")
882 },
883 }
884}
885
886#[test]
887fn test_profiling_args() {
888 assert_eq!(
889 test_parse("-p").0.time_profiling.unwrap(),
890 OutputOptions::Stdout(5_f64)
891 );
892
893 assert_eq!(
894 test_parse("-p 10").0.time_profiling.unwrap(),
895 OutputOptions::Stdout(10_f64)
896 );
897
898 assert_eq!(
899 test_parse("-p 10.0").0.time_profiling.unwrap(),
900 OutputOptions::Stdout(10_f64)
901 );
902
903 assert_eq!(
904 test_parse("-p foo.txt").0.time_profiling.unwrap(),
905 OutputOptions::FileName(String::from("foo.txt"))
906 );
907}
908
909#[test]
910fn test_servoshell_cmd() {
911 assert_eq!(
912 test_parse("--screen-size=1000x1000")
913 .2
914 .screen_size_override
915 .unwrap(),
916 Size2D::new(1000, 1000)
917 );
918
919 assert_eq!(
920 test_parse("--certificate-path=/tmp/test")
921 .0
922 .certificate_path
923 .unwrap(),
924 String::from("/tmp/test")
925 );
926
927 assert!({
928 let p = test_parse("--zealous-gc").1;
929 p.js_mem_gc_zeal_level == 2 && p.js_mem_gc_zeal_frequency == 1
930 });
931}