mod comparison;
mod reference;
mod server;
use std::path::Path;
use std::time::Duration;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::launch_executable::DefaultLaunchHooks;
use super::real_browser::LaunchHooks;
use super::{launch_browser, LaunchOptions, LaunchResult};
use crate::core::{EngineAdapter, EngineType};
use comparison::command_line_differences;
pub use comparison::{
classify_differences, compare_command_lines, diff_reports, parse_switch_args, parse_switches,
ChangedSwitch, CommandLineComparison, FeatureComparison, ParityContext, ParityDifference,
ProbeDifference,
};
pub use reference::build_reference_args;
use server::ProbeServer;
pub const PROBE_SOURCE: &str = include_str!("probe.js");
pub(crate) const VERSION_EXPRESSION: &str = r#"(() => {
const text = id => (document.getElementById(id)?.textContent ?? '').trim();
if (!text('command_line')) return null;
return { commandLine:text('command_line'), version:text('version'), executablePath:text('executable_path') };
})()"#;
#[derive(Debug, Clone)]
pub struct MeasureParityOptions {
pub launch: LaunchOptions,
pub attached: bool,
pub timeout: Duration,
pub reference_args: Vec<String>,
}
impl Default for MeasureParityOptions {
fn default() -> Self {
Self {
launch: LaunchOptions::default(),
attached: false,
timeout: Duration::from_secs(60),
reference_args: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParityBrowser {
pub executable_path: String,
pub version: String,
pub engine: EngineType,
pub launch: String,
pub headless: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParityCommandLine {
pub launched: String,
pub reference: String,
#[serde(flatten)]
pub comparison: CommandLineComparison,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParityReport {
pub browser: ParityBrowser,
pub command_line: ParityCommandLine,
pub differences: Vec<ParityDifference>,
pub unlisted: Vec<ParityDifference>,
pub ok: bool,
}
async fn capture_candidate(
session: &LaunchResult,
server: &ProbeServer,
timeout: Duration,
) -> Result<(Value, Value)> {
tokio::time::timeout(timeout, session.page.goto(&server.candidate_url)).await??;
let report = server.report(false, timeout).await?;
let version = tokio::time::timeout(timeout, session.page.read_browser_version_page()).await??;
Ok((report, version))
}
fn assemble_report(
session: &LaunchResult,
options: &MeasureParityOptions,
executable: &Path,
reference: Value,
reference_version: Value,
candidate: Value,
candidate_version: Value,
) -> Result<ParityReport> {
let command = |value: &Value| {
value
.get("commandLine")
.and_then(Value::as_str)
.map(str::to_owned)
.context("chrome://version did not report its command line")
};
let launched = command(&candidate_version)?;
let reference_command = command(&reference_version)?;
let comparison = compare_command_lines(&reference_command, &launched);
let mut raw_differences = command_line_differences(&comparison);
raw_differences.extend(diff_reports(&reference, &candidate));
let mut requested_args = options.launch.args.clone();
requested_args.extend(options.launch.extra_args.clone());
if !options.launch.sandbox {
requested_args.push("--no-sandbox".into());
}
if !options.launch.restrictions.is_empty() {
requested_args.extend(
session
.args
.iter()
.filter(|arg| {
!arg.starts_with("--user-data-dir")
&& !arg.starts_with("--remote-debugging-port")
})
.cloned(),
);
}
let launch = session
.launch
.unwrap_or(options.launch.launch)
.as_str()
.to_owned();
let (differences, unlisted) = classify_differences(
&raw_differences,
&ParityContext {
launch: launch.clone(),
attached: options.attached,
extra_switches: comparison.extra.clone(),
requested_args,
},
);
Ok(ParityReport {
browser: ParityBrowser {
executable_path: executable.to_string_lossy().into_owned(),
version: candidate_version
.get("version")
.and_then(Value::as_str)
.unwrap_or("")
.split_whitespace()
.collect::<Vec<_>>()
.join(" "),
engine: session.browser.engine,
launch,
headless: session.browser.headless,
},
command_line: ParityCommandLine {
launched,
reference: reference_command,
comparison,
},
ok: unlisted.is_empty(),
differences,
unlisted,
})
}
async fn measure(
options: MeasureParityOptions,
supplied: Option<&LaunchResult>,
) -> Result<ParityReport> {
let real = options.launch.real_browser_options();
let executable =
if let Some(path) = supplied.and_then(|session| session.executable_path.clone()) {
path
} else {
DefaultLaunchHooks {
explicit_selection: options.launch.executable_path.is_some()
|| options.launch.channel.is_some(),
}
.resolve_executable(&real)?
};
let server = ProbeServer::start().await?;
let reference = reference::capture(&server, &executable, &options).await?;
let version_options = LaunchOptions::chromiumoxide()
.executable_path(&executable)
.headless(options.launch.headless)
.with_extra_args(options.reference_args.clone());
let reference_session = launch_browser(version_options).await?;
let reference_version = tokio::time::timeout(
options.timeout,
reference_session.page.read_browser_version_page(),
)
.await;
let reference_closed = reference_session.close().await;
let reference_version = reference_version??;
reference_closed?;
if let Some(session) = supplied {
let (candidate, candidate_version) =
capture_candidate(session, &server, options.timeout).await?;
assemble_report(
session,
&options,
&executable,
reference,
reference_version,
candidate,
candidate_version,
)
} else {
let mut launch = options.launch.clone();
launch.executable_path = Some(executable.clone());
let session = launch_browser(launch).await?;
let captured = capture_candidate(&session, &server, options.timeout).await;
let closed = session.close().await;
let (candidate, candidate_version) = captured?;
closed?;
assemble_report(
&session,
&options,
&executable,
reference,
reference_version,
candidate,
candidate_version,
)
}
}
pub async fn measure_parity(options: MeasureParityOptions) -> Result<ParityReport> {
measure(options, None).await
}
pub async fn measure_session_parity(
session: &LaunchResult,
mut options: MeasureParityOptions,
) -> Result<ParityReport> {
options.launch.headless = session.browser.headless;
options.launch.engine = session.browser.engine;
measure(options, Some(session)).await
}
pub async fn read_browser_version_page(page: &dyn EngineAdapter) -> Result<Value> {
Ok(page.read_browser_version_page().await?)
}