browser_commander/browser/parity/
mod.rs1mod comparison;
9mod reference;
10mod server;
11
12use std::path::Path;
13use std::time::Duration;
14
15use anyhow::{Context, Result};
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18
19use super::launch_executable::DefaultLaunchHooks;
20use super::real_browser::LaunchHooks;
21use super::{launch_browser, LaunchOptions, LaunchResult};
22use crate::core::{EngineAdapter, EngineType};
23
24use comparison::command_line_differences;
25pub use comparison::{
26 classify_differences, compare_command_lines, diff_reports, parse_switch_args, parse_switches,
27 ChangedSwitch, CommandLineComparison, FeatureComparison, ParityContext, ParityDifference,
28 ProbeDifference,
29};
30pub use reference::build_reference_args;
31use server::ProbeServer;
32
33pub const PROBE_SOURCE: &str = include_str!("probe.js");
35
36pub(crate) const VERSION_EXPRESSION: &str = r#"(() => {
37 const text = id => (document.getElementById(id)?.textContent ?? '').trim();
38 if (!text('command_line')) return null;
39 return { commandLine:text('command_line'), version:text('version'), executablePath:text('executable_path') };
40})()"#;
41
42#[derive(Debug, Clone)]
44pub struct MeasureParityOptions {
45 pub launch: LaunchOptions,
47 pub attached: bool,
49 pub timeout: Duration,
51 pub reference_args: Vec<String>,
54}
55
56impl Default for MeasureParityOptions {
57 fn default() -> Self {
58 Self {
59 launch: LaunchOptions::default(),
60 attached: false,
61 timeout: Duration::from_secs(60),
62 reference_args: Vec::new(),
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct ParityBrowser {
71 pub executable_path: String,
73 pub version: String,
75 pub engine: EngineType,
77 pub launch: String,
79 pub headless: bool,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ParityCommandLine {
86 pub launched: String,
88 pub reference: String,
90 #[serde(flatten)]
92 pub comparison: CommandLineComparison,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct ParityReport {
99 pub browser: ParityBrowser,
101 pub command_line: ParityCommandLine,
103 pub differences: Vec<ParityDifference>,
105 pub unlisted: Vec<ParityDifference>,
107 pub ok: bool,
109}
110
111async fn capture_candidate(
112 session: &LaunchResult,
113 server: &ProbeServer,
114 timeout: Duration,
115) -> Result<(Value, Value)> {
116 tokio::time::timeout(timeout, session.page.goto(&server.candidate_url)).await??;
117 let report = server.report(false, timeout).await?;
118 let version = tokio::time::timeout(timeout, session.page.read_browser_version_page()).await??;
119 Ok((report, version))
120}
121
122fn assemble_report(
123 session: &LaunchResult,
124 options: &MeasureParityOptions,
125 executable: &Path,
126 reference: Value,
127 reference_version: Value,
128 candidate: Value,
129 candidate_version: Value,
130) -> Result<ParityReport> {
131 let command = |value: &Value| {
132 value
133 .get("commandLine")
134 .and_then(Value::as_str)
135 .map(str::to_owned)
136 .context("chrome://version did not report its command line")
137 };
138 let launched = command(&candidate_version)?;
139 let reference_command = command(&reference_version)?;
140 let comparison = compare_command_lines(&reference_command, &launched);
141 let mut raw_differences = command_line_differences(&comparison);
142 raw_differences.extend(diff_reports(&reference, &candidate));
143 let mut requested_args = options.launch.args.clone();
144 requested_args.extend(options.launch.extra_args.clone());
145 if !options.launch.sandbox {
146 requested_args.push("--no-sandbox".into());
147 }
148 if !options.launch.restrictions.is_empty() {
149 requested_args.extend(
150 session
151 .args
152 .iter()
153 .filter(|arg| {
154 !arg.starts_with("--user-data-dir")
155 && !arg.starts_with("--remote-debugging-port")
156 })
157 .cloned(),
158 );
159 }
160 let launch = session
161 .launch
162 .unwrap_or(options.launch.launch)
163 .as_str()
164 .to_owned();
165 let (differences, unlisted) = classify_differences(
166 &raw_differences,
167 &ParityContext {
168 launch: launch.clone(),
169 attached: options.attached,
170 extra_switches: comparison.extra.clone(),
171 requested_args,
172 },
173 );
174 Ok(ParityReport {
175 browser: ParityBrowser {
176 executable_path: executable.to_string_lossy().into_owned(),
177 version: candidate_version
178 .get("version")
179 .and_then(Value::as_str)
180 .unwrap_or("")
181 .split_whitespace()
182 .collect::<Vec<_>>()
183 .join(" "),
184 engine: session.browser.engine,
185 launch,
186 headless: session.browser.headless,
187 },
188 command_line: ParityCommandLine {
189 launched,
190 reference: reference_command,
191 comparison,
192 },
193 ok: unlisted.is_empty(),
194 differences,
195 unlisted,
196 })
197}
198
199async fn measure(
200 options: MeasureParityOptions,
201 supplied: Option<&LaunchResult>,
202) -> Result<ParityReport> {
203 let real = options.launch.real_browser_options();
204 let executable =
205 if let Some(path) = supplied.and_then(|session| session.executable_path.clone()) {
206 path
207 } else {
208 DefaultLaunchHooks {
209 explicit_selection: options.launch.executable_path.is_some()
210 || options.launch.channel.is_some(),
211 }
212 .resolve_executable(&real)?
213 };
214 let server = ProbeServer::start().await?;
215 let reference = reference::capture(&server, &executable, &options).await?;
216 let version_options = LaunchOptions::chromiumoxide()
219 .executable_path(&executable)
220 .headless(options.launch.headless)
221 .with_extra_args(options.reference_args.clone());
222 let reference_session = launch_browser(version_options).await?;
223 let reference_version = tokio::time::timeout(
224 options.timeout,
225 reference_session.page.read_browser_version_page(),
226 )
227 .await;
228 let reference_closed = reference_session.close().await;
229 let reference_version = reference_version??;
230 reference_closed?;
231 if let Some(session) = supplied {
232 let (candidate, candidate_version) =
233 capture_candidate(session, &server, options.timeout).await?;
234 assemble_report(
235 session,
236 &options,
237 &executable,
238 reference,
239 reference_version,
240 candidate,
241 candidate_version,
242 )
243 } else {
244 let mut launch = options.launch.clone();
245 launch.executable_path = Some(executable.clone());
246 let session = launch_browser(launch).await?;
247 let captured = capture_candidate(&session, &server, options.timeout).await;
248 let closed = session.close().await;
249 let (candidate, candidate_version) = captured?;
250 closed?;
251 assemble_report(
252 &session,
253 &options,
254 &executable,
255 reference,
256 reference_version,
257 candidate,
258 candidate_version,
259 )
260 }
261}
262
263pub async fn measure_parity(options: MeasureParityOptions) -> Result<ParityReport> {
265 measure(options, None).await
266}
267
268pub async fn measure_session_parity(
271 session: &LaunchResult,
272 mut options: MeasureParityOptions,
273) -> Result<ParityReport> {
274 options.launch.headless = session.browser.headless;
275 options.launch.engine = session.browser.engine;
276 measure(options, Some(session)).await
277}
278
279pub async fn read_browser_version_page(page: &dyn EngineAdapter) -> Result<Value> {
281 Ok(page.read_browser_version_page().await?)
282}