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