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