1use core::panic;
6use std::cell::Cell;
7use std::collections::HashMap;
8use std::fs::{self, File, read_to_string};
9use std::io::Read;
10use std::path::{Path, PathBuf};
11use std::str::FromStr;
12#[cfg(any(target_os = "android", target_env = "ohos"))]
13use std::sync::OnceLock;
14use std::{env, fmt, process};
15
16use bpaf::*;
17use euclid::Size2D;
18use log::warn;
19use serde_json::Value;
20use servo::{
21 DeviceIndependentPixel, DiagnosticsLogging, Opts, OutputOptions, PrefValue, Preferences,
22 ServoUrl,
23};
24use url::Url;
25
26use crate::VERSION;
27
28pub(crate) static EXPERIMENTAL_PREFS: &[&str] = &[
29 "dom_async_clipboard_enabled",
30 "dom_fontface_enabled",
31 "dom_intersection_observer_enabled",
32 "dom_navigator_protocol_handlers_enabled",
33 "dom_navigator_sendbeacon_enabled",
34 "dom_notification_enabled",
35 "dom_offscreen_canvas_enabled",
36 "dom_permissions_enabled",
37 "dom_webgl2_enabled",
38 "dom_webgpu_enabled",
39 "layout_columns_enabled",
40 "layout_container_queries_enabled",
41 "layout_grid_enabled",
42 "layout_variable_fonts_enabled",
43];
44
45#[cfg_attr(any(target_os = "android", target_env = "ohos"), allow(dead_code))]
46#[derive(Clone)]
47pub(crate) struct ServoShellPreferences {
48 pub url: Option<String>,
50 pub device_pixel_ratio_override: Option<f32>,
52 pub clean_shutdown: bool,
54 pub no_native_titlebar: bool,
56 pub homepage: String,
58 pub searchpage: String,
61 pub headless: bool,
64 pub tracing_filter: Option<String>,
69 pub initial_window_size: Size2D<u32, DeviceIndependentPixel>,
71 pub screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>,
74 pub simulate_touch_events: bool,
76 pub output_image_path: Option<String>,
79 pub exit_after_stable_image: bool,
81 pub userscripts_directory: Option<PathBuf>,
84 pub webdriver_port: Cell<Option<u16>>,
87 pub experimental_preferences_enabled: bool,
89 #[cfg(target_env = "ohos")]
92 pub log_filter: Option<String>,
93 #[cfg(target_env = "ohos")]
95 pub log_to_file: bool,
96}
97
98impl Default for ServoShellPreferences {
99 fn default() -> Self {
100 Self {
101 clean_shutdown: false,
102 device_pixel_ratio_override: None,
103 headless: false,
104 homepage: "https://servo.org".into(),
105 initial_window_size: Size2D::new(1024, 740),
106 no_native_titlebar: true,
107 screen_size_override: None,
108 simulate_touch_events: false,
109 searchpage: "https://duckduckgo.com/html/?q=%s".into(),
110 tracing_filter: None,
111 url: None,
112 output_image_path: None,
113 exit_after_stable_image: false,
114 userscripts_directory: None,
115 webdriver_port: Cell::new(None),
116 #[cfg(target_env = "ohos")]
117 log_filter: None,
118 #[cfg(target_env = "ohos")]
119 log_to_file: false,
120 experimental_preferences_enabled: false,
121 }
122 }
123}
124
125#[cfg(all(
126 unix,
127 not(target_os = "macos"),
128 not(target_os = "ios"),
129 not(target_os = "android"),
130 not(target_env = "ohos")
131))]
132pub fn default_config_dir() -> Option<PathBuf> {
133 let mut config_dir = ::dirs::config_dir().unwrap();
134 config_dir.push("servo");
135 config_dir.push("default");
136 Some(config_dir)
137}
138
139#[cfg(any(target_os = "android", target_env = "ohos"))]
141pub(crate) static DEFAULT_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
142#[cfg(any(target_os = "android", target_env = "ohos"))]
143pub fn default_config_dir() -> Option<PathBuf> {
144 DEFAULT_CONFIG_DIR.get().cloned()
145}
146
147#[cfg(target_os = "macos")]
148pub fn default_config_dir() -> Option<PathBuf> {
149 let mut config_dir = ::dirs::data_dir().unwrap();
152 config_dir.push("Servo");
153 Some(config_dir)
154}
155
156#[cfg(target_os = "windows")]
157pub fn default_config_dir() -> Option<PathBuf> {
158 let mut config_dir = ::dirs::config_dir().unwrap();
159 config_dir.push("Servo");
160 Some(config_dir)
161}
162
163fn get_preferences(prefs_files: &[PathBuf], config_dir: &Option<PathBuf>) -> Preferences {
167 if cfg!(test) {
170 return Preferences::default();
171 }
172
173 let user_prefs_path = config_dir
174 .clone()
175 .map(|path| path.join("prefs.json"))
176 .filter(|path| path.exists());
177 let user_prefs_hash = user_prefs_path.map(read_prefs_file).unwrap_or_default();
178
179 let apply_preferences =
180 |preferences: &mut Preferences, preferences_hash: HashMap<String, PrefValue>| {
181 for (key, value) in preferences_hash.iter() {
182 preferences.set_value(key, value.clone());
183 }
184 };
185
186 let mut preferences = Preferences::default();
187 apply_preferences(&mut preferences, user_prefs_hash);
188 for pref_file_path in prefs_files.iter() {
189 apply_preferences(&mut preferences, read_prefs_file(pref_file_path))
190 }
191
192 preferences
193}
194
195fn read_prefs_file<P: AsRef<Path>>(path: P) -> HashMap<String, PrefValue> {
196 read_prefs_map(&read_to_string(path).expect("Error opening user prefs"))
197}
198
199pub fn read_prefs_map(txt: &str) -> HashMap<String, PrefValue> {
200 let prefs: HashMap<String, Value> = serde_json::from_str(txt)
201 .map_err(|_| panic!("Could not parse preferences JSON"))
202 .unwrap();
203 prefs
204 .into_iter()
205 .map(|(key, value)| {
206 let value = (&value)
207 .try_into()
208 .map_err(|error| panic!("{error}"))
209 .unwrap();
210 (key, value)
211 })
212 .collect()
213}
214
215#[allow(clippy::large_enum_variant)]
216#[cfg_attr(any(target_os = "android", target_env = "ohos"), allow(dead_code))]
217pub(crate) enum ArgumentParsingResult {
218 ChromeProcess(Opts, Preferences, ServoShellPreferences),
219 ContentProcess(String),
220 Exit,
221 ErrorParsing,
222}
223
224enum ParseResolutionError {
225 InvalidFormat,
226 ZeroDimension,
227 ParseError(std::num::ParseIntError),
228}
229
230impl fmt::Display for ParseResolutionError {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 match self {
233 ParseResolutionError::InvalidFormat => write!(f, "invalid resolution format"),
234 ParseResolutionError::ZeroDimension => {
235 write!(f, "width and height must be greater than 0")
236 },
237 ParseResolutionError::ParseError(e) => write!(f, "{e}"),
238 }
239 }
240}
241
242fn parse_resolution_string(
244 string: String,
245) -> Result<Option<Size2D<u32, DeviceIndependentPixel>>, ParseResolutionError> {
246 if string.is_empty() {
247 Ok(None)
248 } else {
249 let (width, height) = string
250 .split_once(['x', 'X'])
251 .ok_or(ParseResolutionError::InvalidFormat)?;
252
253 let width = width.trim();
254 let height = height.trim();
255 if width.is_empty() || height.is_empty() {
256 return Err(ParseResolutionError::InvalidFormat);
257 }
258
259 let width = width.parse().map_err(ParseResolutionError::ParseError)?;
260 let height = height.parse().map_err(ParseResolutionError::ParseError)?;
261 if width == 0 || height == 0 {
262 return Err(ParseResolutionError::ZeroDimension);
263 }
264
265 Ok(Some(Size2D::new(width, height)))
266 }
267}
268
269fn parse_user_stylesheets(string: String) -> Result<Vec<(Vec<u8>, ServoUrl)>, std::io::Error> {
271 Ok(string
272 .split_whitespace()
273 .map(|filename| {
274 let cwd = env::current_dir().unwrap();
275 let path = cwd.join(filename);
276 let url = ServoUrl::from_url(Url::from_file_path(&path).unwrap());
277 let mut contents = Vec::new();
278 File::open(path)
279 .unwrap()
280 .read_to_end(&mut contents)
281 .unwrap();
282 (contents, url)
283 })
284 .collect())
285}
286
287fn flag_with_default_parser<S, T>(
293 short_cmd: Option<char>,
294 long_cmd: &'static str,
295 argument_help: &'static str,
296 help: &'static str,
297 default: T,
298 transform: fn(S) -> T,
299) -> impl Parser<Option<T>>
300where
301 S: FromStr + 'static,
302 <S as FromStr>::Err: fmt::Display,
303 T: Clone + 'static,
304{
305 let just_flag = if let Some(c) = short_cmd {
306 short(c).long(long_cmd)
307 } else {
308 long(long_cmd)
309 }
310 .req_flag(default)
311 .hide();
312
313 let arg = if let Some(c) = short_cmd {
314 short(c).long(long_cmd)
315 } else {
316 long(long_cmd)
317 }
318 .argument::<S>(argument_help)
319 .help(help)
320 .map(transform);
321
322 construct!([arg, just_flag]).optional()
323}
324
325fn profile() -> impl Parser<Option<OutputOptions>> {
326 flag_with_default_parser(
327 Some('p'),
328 "profile",
329 "",
330 "uses 5.0 output as standard if no argument supplied",
331 OutputOptions::Stdout(5.0),
332 |val: String| {
333 if let Ok(float) = val.parse::<f64>() {
334 OutputOptions::Stdout(float)
335 } else {
336 OutputOptions::FileName(val)
337 }
338 },
339 )
340}
341
342fn userscripts() -> impl Parser<Option<PathBuf>> {
343 flag_with_default_parser(
344 None,
345 "userscripts_directory",
346 "your/directory",
347 "Uses userscripts in resources/user-agent-js, or a specified full path",
348 PathBuf::from("resources/user-agent-js"),
349 |val: String| PathBuf::from(val),
350 )
351}
352
353fn webdriver_port() -> impl Parser<Option<u16>> {
354 flag_with_default_parser(
355 None,
356 "webdriver",
357 "7000",
358 "Start remote WebDriver server on port",
359 7000,
360 |val| val,
361 )
362}
363
364fn map_debug_options(arg: String) -> Vec<String> {
365 arg.split(',').map(|s| s.to_owned()).collect()
366}
367
368#[derive(Bpaf, Clone, Debug)]
369#[bpaf(options, version(VERSION), usage("servo [OPTIONS] URL"))]
370struct CmdArgs {
372 #[bpaf(short('B'), long("bhm"))]
374 background_hang_monitor: bool,
375
376 #[bpaf(argument("/home/servo/resources/certs"))]
379 certificate_path: Option<PathBuf>,
380
381 #[bpaf(long)]
383 clean_shutdown: bool,
384
385 #[bpaf(argument("~/.config/servo"))]
388 config_dir: Option<PathBuf>,
389
390 #[bpaf(argument("servo-ipc-channel.abcdefg"))]
393 content_process: Option<String>,
394
395 #[bpaf(
398 short('Z'),
399 argument("layout_grid_enabled=true,dom_async_clipboard_enabled"),
400 long,
401 map(map_debug_options),
402 fallback(vec![])
403 )]
404 debug: Vec<String>,
405
406 #[bpaf(argument("1.0"))]
409 device_pixel_ratio: Option<f32>,
410
411 #[bpaf(argument("0"))]
413 devtools: Option<u16>,
414
415 #[bpaf(long)]
418 enable_experimental_web_platform_features: bool,
419
420 #[bpaf(short('x'), long)]
422 exit: bool,
423
424 #[bpaf(short('I'), long("force-ipc"))]
426 force_ipc: bool,
427
428 #[bpaf(short('f'), long)]
430 hard_fail: bool,
431
432 #[bpaf(short('z'), long)]
434 headless: bool,
435
436 #[bpaf(long)]
439 ignore_certificate_errors: bool,
440
441 #[bpaf(short('y'), long, argument("1"))]
443 layout_threads: Option<i64>,
444
445 #[bpaf(argument("~/.local/share/servo"))]
448 local_script_source: Option<PathBuf>,
449
450 #[cfg(target_env = "ohos")]
451 #[bpaf(argument("FILTER"))]
453 log_filter: Option<String>,
454
455 #[cfg(target_env = "ohos")]
456 #[bpaf(long)]
458 log_to_file: bool,
459
460 #[bpaf(short('M'), long)]
462 multiprocess: bool,
463
464 #[bpaf(short('b'), long)]
466 no_native_titlebar: bool,
467
468 #[bpaf(short('i'), long, flag(false, true))]
471 nonincremental_layout: bool,
472
473 #[bpaf(short('o'), argument("test.png"), long)]
476 output: Option<PathBuf>,
477
478 #[bpaf(external)]
481 profile: Option<OutputOptions>,
482
483 #[bpaf(argument("trace.html"), long)]
486 profiler_trace_path: Option<PathBuf>,
487
488 #[bpaf(argument("dom_bluetooth_enabled"), many)]
491 pref: Vec<String>,
492
493 #[bpaf(long, argument("/path/to/prefs.json"), many)]
496 prefs_file: Vec<PathBuf>,
497
498 #[bpaf(long)]
500 print_pwm: bool,
501
502 #[bpaf(argument("0.25"))]
505 random_pipeline_closure_probability: Option<f32>,
506
507 random_pipeline_closure_seed: Option<usize>,
509
510 #[bpaf(short('S'), long)]
512 sandbox: bool,
513
514 shaders: Option<PathBuf>,
516
517 #[bpaf(long("screen-size"), argument::<String>("1024x768"),
520 parse(parse_resolution_string), fallback(None))]
521 screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>,
522
523 #[bpaf(long("simulate-touch-events"))]
526 simulate_touch_events: bool,
527
528 #[bpaf(long("tracing-filter"), argument("FILTER"))]
530 tracing_filter: Option<String>,
531
532 #[bpaf(long)]
534 unminify_js: bool,
535
536 #[bpaf(long)]
538 unminify_css: bool,
539
540 #[bpaf(short('u'),long,argument::<String>("NCSA mosaic/1.0 (X11;SunOS 4.1.4 sun4m"))]
543 user_agent: Option<String>,
544
545 #[bpaf(external)]
548 userscripts: Option<PathBuf>,
549
550 #[bpaf(argument::<String>("file.css"), parse(parse_user_stylesheets), fallback(vec![]))]
553 user_stylesheet: Vec<(Vec<u8>, ServoUrl)>,
554
555 #[bpaf(external)]
557 webdriver_port: Option<u16>,
558
559 #[bpaf(argument::<String>("1024x740"), parse(parse_resolution_string), fallback(None))]
562 window_size: Option<Size2D<u32, DeviceIndependentPixel>>,
563
564 #[bpaf(positional("URL"), fallback(String::from("https://www.servo.org")))]
566 url: String,
567}
568
569fn update_preferences_from_command_line_arguemnts(
570 preferences: &mut Preferences,
571 cmd_args: &CmdArgs,
572) {
573 if let Some(port) = cmd_args.devtools {
574 preferences.devtools_server_enabled = true;
575 preferences.devtools_server_port = port as i64;
576 }
577
578 if cmd_args.enable_experimental_web_platform_features {
579 for pref in EXPERIMENTAL_PREFS {
580 preferences.set_value(pref, PrefValue::Bool(true));
581 }
582 }
583
584 for pref in &cmd_args.pref {
585 let split: Vec<&str> = pref.splitn(2, '=').collect();
586 let pref_name = split[0];
587 let pref_value = PrefValue::from_booleanish_str(split.get(1).copied().unwrap_or("true"));
588 preferences.set_value(pref_name, pref_value);
589 }
590
591 if let Some(layout_threads) = cmd_args.layout_threads {
592 preferences.layout_threads = layout_threads;
593 }
594
595 if cmd_args.headless && preferences.media_glvideo_enabled {
596 warn!("GL video rendering is not supported on headless windows.");
597 preferences.media_glvideo_enabled = false;
598 }
599
600 if let Some(user_agent) = cmd_args.user_agent.clone() {
601 preferences.user_agent = user_agent;
602 }
603
604 if cmd_args.webdriver_port.is_some() {
605 preferences.dom_testing_html_input_element_select_files_enabled = true;
606 }
607}
608
609pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsingResult {
610 let args_without_binary = args
612 .split_first()
613 .expect("Expected executable name and arguments")
614 .1;
615 let cmd_args = cmd_args().run_inner(Args::from(args_without_binary));
616 let cmd_args = match cmd_args {
617 Ok(cmd_args) => cmd_args,
618 Err(error) => {
619 error.print_message(80);
620 return if error.exit_code() == 0 {
621 ArgumentParsingResult::Exit
622 } else {
623 ArgumentParsingResult::ErrorParsing
624 };
625 },
626 };
627
628 if cmd_args
629 .debug
630 .iter()
631 .any(|debug_option| debug_option.contains("help"))
632 {
633 print_debug_options_usage("servo");
634 return ArgumentParsingResult::Exit;
635 }
636
637 if let Some(content_process) = cmd_args.content_process {
639 return ArgumentParsingResult::ContentProcess(content_process);
640 }
641
642 let config_dir = cmd_args
643 .config_dir
644 .clone()
645 .or_else(default_config_dir)
646 .inspect(|config_dir| {
647 if !config_dir.exists() {
648 fs::create_dir_all(config_dir).expect("Could not create config_dir");
649 }
650 });
651 if let Some(ref time_profiler_trace_path) = cmd_args.profiler_trace_path {
652 let mut path = PathBuf::from(time_profiler_trace_path);
653 path.pop();
654 fs::create_dir_all(&path).expect("Error in creating profiler trace path");
655 }
656
657 let mut preferences = get_preferences(&cmd_args.prefs_file, &config_dir);
658
659 update_preferences_from_command_line_arguemnts(&mut preferences, &cmd_args);
660
661 if cfg!(target_os = "android") && cfg!(target_pointer_width = "32") {
663 preferences.js_baseline_interpreter_enabled = false;
664 preferences.js_baseline_jit_enabled = false;
665 preferences.js_ion_enabled = false;
666 }
667
668 let default_window_size = Size2D::new(1024, 740);
670 let default_window_size = cmd_args
671 .screen_size_override
672 .map_or(default_window_size, |screen_size_override| {
673 default_window_size.min(screen_size_override)
674 });
675
676 let servoshell_preferences = ServoShellPreferences {
677 url: Some(cmd_args.url),
678 no_native_titlebar: cmd_args.no_native_titlebar,
679 device_pixel_ratio_override: cmd_args.device_pixel_ratio,
680 clean_shutdown: cmd_args.clean_shutdown,
681 headless: cmd_args.headless,
682 tracing_filter: cmd_args.tracing_filter,
683 initial_window_size: cmd_args.window_size.unwrap_or(default_window_size),
684 screen_size_override: cmd_args.screen_size_override,
685 simulate_touch_events: cmd_args.simulate_touch_events,
686 webdriver_port: Cell::new(cmd_args.webdriver_port),
687 output_image_path: cmd_args.output.map(|p| p.to_string_lossy().into_owned()),
688 exit_after_stable_image: cmd_args.exit,
689 userscripts_directory: cmd_args.userscripts,
690 experimental_preferences_enabled: cmd_args.enable_experimental_web_platform_features,
691 #[cfg(target_env = "ohos")]
692 log_filter: cmd_args.log_filter.or_else(|| {
693 (!preferences.log_filter.is_empty()).then(|| preferences.log_filter.clone())
694 }),
695 #[cfg(target_env = "ohos")]
696 log_to_file: cmd_args.log_to_file,
697 ..Default::default()
698 };
699
700 let mut debug_options = DiagnosticsLogging::default();
701 for debug_string in cmd_args.debug {
702 let result = debug_options.extend(debug_string);
703 if let Err(error) = result {
704 println!("error: unrecognized debug option: {}", error);
705 return ArgumentParsingResult::ErrorParsing;
706 }
707 }
708
709 let opts = Opts {
710 debug: debug_options,
711 time_profiling: cmd_args.profile,
712 time_profiler_trace_path: cmd_args
713 .profiler_trace_path
714 .map(|p| p.to_string_lossy().into_owned()),
715 nonincremental_layout: cmd_args.nonincremental_layout,
716 user_stylesheets: cmd_args.user_stylesheet,
717 hard_fail: cmd_args.hard_fail,
718 multiprocess: cmd_args.multiprocess,
719 background_hang_monitor: cmd_args.background_hang_monitor,
720 sandbox: cmd_args.sandbox,
721 random_pipeline_closure_probability: cmd_args.random_pipeline_closure_probability,
722 random_pipeline_closure_seed: cmd_args.random_pipeline_closure_seed,
723 config_dir: config_dir.clone(),
724 shaders_path: cmd_args.shaders,
725 certificate_path: cmd_args
726 .certificate_path
727 .map(|p| p.to_string_lossy().into_owned()),
728 ignore_certificate_errors: cmd_args.ignore_certificate_errors,
729 unminify_js: cmd_args.unminify_js,
730 local_script_source: cmd_args
731 .local_script_source
732 .map(|p| p.to_string_lossy().into_owned()),
733 unminify_css: cmd_args.unminify_css,
734 print_pwm: cmd_args.print_pwm,
735 force_ipc: cmd_args.force_ipc,
736 };
737
738 ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences)
739}
740
741fn print_debug_options_usage(app: &str) {
742 fn print_option(name: &str, description: &str) {
743 println!("\t{:<35} {}", name, description);
744 }
745
746 println!(
747 "Usage: {} debug option,[options,...]\n\twhere options include\n\nOptions:",
748 app
749 );
750 print_option(
751 "disable-share-style-cache",
752 "Disable the style sharing cache.",
753 );
754 print_option(
755 "dump-stacking-context-tree",
756 "Print the stacking context tree after each layout.",
757 );
758 print_option(
759 "dump-display-list",
760 "Print the display list after each layout.",
761 );
762 print_option(
763 "dump-flow-tree",
764 "Print the fragment tree after each layout.",
765 );
766 print_option(
767 "dump-rule-tree",
768 "Print the style rule tree after each layout.",
769 );
770 print_option(
771 "dump-style-tree",
772 "Print the DOM with computed styles after each restyle.",
773 );
774 print_option("dump-style-stats", "Print style statistics each restyle.");
775 print_option("dump-scroll-tree", "Print scroll tree after each layout.");
776 print_option("gc-profile", "Log GC passes and their durations.");
777 print_option(
778 "profile-script-events",
779 "Enable profiling of script-related events.",
780 );
781 print_option(
782 "relayout-event",
783 "Print notifications when there is a relayout.",
784 );
785
786 println!();
787
788 process::exit(0)
789}
790
791#[cfg(test)]
792fn test_parse_pref(arg: &str) -> Preferences {
793 let args = vec!["servo".to_string(), "--pref".to_string(), arg.to_string()];
794 match parse_command_line_arguments(args) {
795 ArgumentParsingResult::ContentProcess(..) => {
796 unreachable!("No preferences for content process")
797 },
798 ArgumentParsingResult::ChromeProcess(_, preferences, _) => preferences,
799 ArgumentParsingResult::Exit => {
800 panic!("we supplied a --pref argument above which should be parsed")
801 },
802 ArgumentParsingResult::ErrorParsing => {
803 unreachable!("we supplied a --pref argument above which should be parsed")
804 },
805 }
806}
807
808#[test]
809fn test_parse_pref_from_command_line() {
810 let preferences = test_parse_pref("dom_bluetooth_enabled=true");
812 assert!(preferences.dom_bluetooth_enabled);
813
814 let preferences = test_parse_pref("dom_bluetooth_enabled=false");
815 assert!(!preferences.dom_bluetooth_enabled);
816
817 let preferences = test_parse_pref("layout_threads=42");
819 assert_eq!(preferences.layout_threads, 42);
820
821 let preferences = test_parse_pref("fonts_default=Lucida");
823 assert_eq!(preferences.fonts_default, "Lucida");
824
825 let preferences = test_parse_pref("dom_bluetooth_enabled");
827 assert!(preferences.dom_bluetooth_enabled);
828}
829
830#[test]
831fn test_invalid_prefs_from_command_line_panics() {
832 let err_msg = std::panic::catch_unwind(|| {
833 test_parse_pref("doesntexist=true");
834 })
835 .err()
836 .and_then(|a| a.downcast_ref::<String>().cloned())
837 .expect("Should panic");
838 assert_eq!(
839 err_msg, "Unknown preference: \"doesntexist\"",
840 "Message should describe the problem"
841 )
842}
843
844#[test]
845fn test_create_prefs_map() {
846 let json_str = "{
847 \"layout.writing-mode.enabled\": true,
848 \"network.mime.sniff\": false,
849 \"shell.homepage\": \"https://servo.org\"
850 }";
851 assert_eq!(read_prefs_map(json_str).len(), 3);
852}
853
854#[cfg(test)]
855fn test_parse(arg: &str) -> (Opts, Preferences, ServoShellPreferences) {
856 let mut args = vec!["servo".to_string()];
857 let mut args_split = arg.split_whitespace().map(|s| s.to_owned()).collect();
859 args.append(&mut args_split);
860 match parse_command_line_arguments(args) {
861 ArgumentParsingResult::ContentProcess(..) => {
862 unreachable!("No preferences for content process")
863 },
864 ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => {
865 (opts, preferences, servoshell_preferences)
866 },
867 ArgumentParsingResult::Exit | ArgumentParsingResult::ErrorParsing => {
868 unreachable!("We always have valid preference in our test cases")
869 },
870 }
871}
872
873#[test]
874fn test_profiling_args() {
875 assert_eq!(
876 test_parse("-p").0.time_profiling.unwrap(),
877 OutputOptions::Stdout(5_f64)
878 );
879
880 assert_eq!(
881 test_parse("-p 10").0.time_profiling.unwrap(),
882 OutputOptions::Stdout(10_f64)
883 );
884
885 assert_eq!(
886 test_parse("-p 10.0").0.time_profiling.unwrap(),
887 OutputOptions::Stdout(10_f64)
888 );
889
890 assert_eq!(
891 test_parse("-p foo.txt").0.time_profiling.unwrap(),
892 OutputOptions::FileName(String::from("foo.txt"))
893 );
894}
895
896#[test]
897fn test_servoshell_cmd() {
898 assert_eq!(
899 test_parse("--screen-size=1000x1000")
900 .2
901 .screen_size_override
902 .unwrap(),
903 Size2D::new(1000, 1000)
904 );
905
906 assert_eq!(
907 test_parse("--certificate-path=/tmp/test")
908 .0
909 .certificate_path
910 .unwrap(),
911 String::from("/tmp/test")
912 );
913}