#![allow(clippy::await_holding_lock)]
#![allow(dead_code)]
mod common;
use std::process::Command;
use common::registry_guard;
use lingshu_tools::tools::web::search::backends::ddgs::{
DdgsEngine, DdgsSelectionMode, DdgsSettings, ImpersonateOs, metasearch_text, parse_bing_html,
parse_ddg_html, pick_random_profile, resolve_profile, resolve_profile_with, select_ranked,
select_raw,
};
use lingshu_tools::tools::web::search::config::SearchOptions;
const BING_RUST_FIXTURE: &str = include_str!("fixtures/ddgs/bing_rust_programming.html");
const BING_PERSON_FIXTURE: &str = include_str!("fixtures/ddgs/bing_person_mixed.html");
const DDG_HTML_FIXTURE: &str = include_str!("fixtures/ddgs/ddg_html_div_h2.html");
fn parse_fixture_bing(html: &str, max: usize) -> Vec<String> {
select_raw(parse_bing_html(html, max, "ddgs").expect("parse"), max)
.into_iter()
.map(|r| r.url)
.collect()
}
#[test]
fn e2e_default_settings_strict_python_parity() {
let _lock = registry_guard();
let s = DdgsSettings::default();
assert_eq!(s.selection_mode, DdgsSelectionMode::Raw);
assert_eq!(s.engine_order(), vec![DdgsEngine::Bing]);
assert_eq!(s.max_retries, 0);
assert!(s.region().is_none(), "Python text(region=None) default");
}
#[test]
fn e2e_python_parity_bing_fixture_urls_and_order() {
let _lock = registry_guard();
let urls = parse_fixture_bing(BING_RUST_FIXTURE, 5);
assert_eq!(urls.len(), 4);
assert_eq!(urls[0], "https://www.rust-lang.org/");
assert_eq!(urls[1], "https://doc.rust-lang.org/book/");
assert!(
urls[2].contains("y.js?ad_domain"),
"Bing keeps ad rows like Python"
);
assert_eq!(
urls[3],
"https://en.wikipedia.org/wiki/Rust_(programming_language)"
);
}
#[test]
fn e2e_python_parity_ddg_html_div_h2_fixture() {
let _lock = registry_guard();
let results = parse_ddg_html(DDG_HTML_FIXTURE, 3, "ddgs").expect("parse");
assert_eq!(results.len(), 1);
assert_eq!(results[0].url, "https://www.example.com/page");
}
#[test]
fn e2e_python_parity_raw_keeps_poison_serp_rows() {
let _lock = registry_guard();
let parsed = parse_bing_html(BING_PERSON_FIXTURE, 5, "ddgs").expect("parse");
assert_eq!(parsed.len(), 3);
let raw = select_raw(parsed, 5);
assert_eq!(raw.len(), 3, "Python returns unrelated SERP rows as-is");
}
#[test]
fn e2e_python_parity_ranked_reorders_not_drops_poison() {
let _lock = registry_guard();
let parsed = parse_bing_html(BING_PERSON_FIXTURE, 5, "ddgs").expect("parse");
let ranked = select_ranked("Raphaël MANSUY", parsed, 5);
assert_eq!(ranked.len(), 3);
assert!(ranked[0].url.contains("linkedin.com"));
}
#[test]
fn e2e_python_parity_bing_does_not_filter_ad_urls() {
let _lock = registry_guard();
let html = r#"
<li class="b_algo"><h2><a href="https://duckduckgo.com/y.js?ad_domain=spam">Ad</a></h2><p>x</p></li>
<li class="b_algo"><h2><a href="https://example.org/">Real</a></h2><p>snippet</p></li>
"#;
let urls = parse_fixture_bing(html, 5);
assert_eq!(urls.len(), 2);
assert!(urls[0].contains("y.js?ad_domain"));
}
#[test]
fn e2e_python_parity_html_filters_ad_urls() {
let _lock = registry_guard();
let html = r#"
<div>
<a href="https://duckduckgo.com/y.js?ad_domain=spam">
<h2><a>Ad</a></h2>
<a>buy</a>
</a>
</div>
<div>
<a href="https://example.org/">
<h2><a>Real</a></h2>
<a>snippet</a>
</a>
</div>
"#;
let results = parse_ddg_html(html, 5, "ddgs").expect("parse");
assert_eq!(results.len(), 1);
assert_eq!(results[0].url, "https://example.org/");
}
#[test]
fn e2e_selection_mode_env_ranked() {
let _lock = registry_guard();
let prev = std::env::var("DDGS_SELECTION").ok();
unsafe { std::env::set_var("DDGS_SELECTION", "ranked") };
let s = DdgsSettings::resolve(&Default::default());
assert_eq!(s.selection_mode, DdgsSelectionMode::Ranked);
unsafe { std::env::remove_var("DDGS_SELECTION") };
if let Some(v) = prev {
unsafe { std::env::set_var("DDGS_SELECTION", v) };
}
}
#[test]
fn e2e_python_script_parses_bing_fixture_same_urls() {
let _lock = registry_guard();
let script = r#"
import sys, base64, re
from html import unescape
from urllib.parse import parse_qs, urlparse, unquote
from lxml.html import document_fromstring
html = open(sys.argv[1]).read()
tree = document_fromstring(html)
urls = []
cache = set()
for e in tree.xpath("//li[contains(@class, 'b_algo')]"):
hrefs = e.xpath("./h2/a/@href")
if not hrefs:
continue
href = hrefs[0]
if href.startswith("https://www.bing.com/ck/a?"):
u = parse_qs(urlparse(href).query).get("u", [""])[0]
if u.startswith("a1"):
href = base64.urlsafe_b64decode(u[2:] + "=" * (-len(u[2:]) % 4)).decode()
if href in cache:
continue
cache.add(href)
urls.append(unquote(href))
print("\n".join(urls))
"#;
let tmp = tempfile::NamedTempFile::new().expect("tmp");
std::fs::write(tmp.path(), BING_RUST_FIXTURE).expect("write");
let py = Command::new("python3")
.args(["-c", script, tmp.path().to_str().expect("path")])
.output()
.expect("python");
if !py.status.success() {
eprintln!("python skip: {}", String::from_utf8_lossy(&py.stderr));
return;
}
let py_urls: Vec<String> = String::from_utf8_lossy(&py.stdout)
.lines()
.map(str::to_string)
.collect();
let rust_urls = parse_fixture_bing(BING_RUST_FIXTURE, 5);
assert_eq!(
rust_urls, py_urls,
"Rust parse+select_raw must match Python Bing href cache order"
);
}
#[test]
fn e2e_python_script_parses_bing_fixture_full_rows() {
let _lock = registry_guard();
let script = r#"
import sys, base64, json, re
from html import unescape
from urllib.parse import parse_qs, urlparse, unquote
from lxml.html import document_fromstring
REGEX_STRIP_TAGS = re.compile("<.*?>")
def normalize(raw):
return unescape(REGEX_STRIP_TAGS.sub("", raw)) if raw else ""
def normalize_url(url):
return unquote(url).replace(" ", "+") if url else ""
html = open(sys.argv[1]).read()
tree = document_fromstring(html)
rows = []
cache = set()
for e in tree.xpath("//li[contains(@class, 'b_algo')]"):
hrefs = e.xpath("./h2/a/@href | ./div[contains(@class, 'header')]/a/@href")
href = str(hrefs[0]) if hrefs else None
if href and href.startswith("https://www.bing.com/ck/a?"):
u = parse_qs(urlparse(href).query).get("u", [""])[0]
if u.startswith("a1"):
href = base64.urlsafe_b64decode(u[2:] + "=" * (-len(u[2:]) % 4)).decode()
if href and href not in cache:
cache.add(href)
titlexpath = e.xpath("./h2/a//text() | ./div[contains(@class, 'header')]/a/h2//text()")
title = "".join(str(x) for x in titlexpath) if titlexpath else ""
bodyxpath = e.xpath(".//p//text()")
body = "".join(str(x) for x in bodyxpath) if bodyxpath else ""
rows.append({
"title": normalize(title),
"href": normalize_url(href),
"body": normalize(body).replace("\xa0", " "),
})
print(json.dumps(rows))
"#;
let tmp = tempfile::NamedTempFile::new().expect("tmp");
std::fs::write(tmp.path(), BING_RUST_FIXTURE).expect("write");
let py = Command::new("python3")
.args(["-c", script, tmp.path().to_str().expect("path")])
.output()
.expect("python");
if !py.status.success() {
eprintln!("python skip: {}", String::from_utf8_lossy(&py.stderr));
return;
}
let py_rows: Vec<serde_json::Value> = serde_json::from_slice(&py.stdout).expect("python json");
let rust_rows = select_raw(
parse_bing_html(BING_RUST_FIXTURE, 10, "ddgs").expect("parse"),
10,
);
assert_eq!(rust_rows.len(), py_rows.len());
for (i, (rust, py)) in rust_rows.iter().zip(py_rows.iter()).enumerate() {
assert_eq!(rust.url, py["href"].as_str().unwrap(), "row {i} href");
assert_eq!(rust.title, py["title"].as_str().unwrap(), "row {i} title");
assert_eq!(rust.snippet, py["body"].as_str().unwrap(), "row {i} body");
}
}
fn canonical_host_path(url: &str) -> String {
url::Url::parse(url)
.ok()
.map(|u| {
let host = u
.host_str()
.unwrap_or("")
.trim_start_matches("www.")
.trim_start_matches("m.");
let path = u.path().trim_end_matches('/');
format!("{host}{path}")
})
.unwrap_or_else(|| url.to_string())
}
fn url_sets_overlap(rust_urls: &[String], py_urls: &[String]) -> usize {
let py_keys: Vec<String> = py_urls.iter().map(|u| canonical_host_path(u)).collect();
rust_urls
.iter()
.filter(|u| py_keys.iter().any(|p| p == &canonical_host_path(u)))
.count()
}
#[test]
fn e2e_ddgs_impersonate_env_pins_profile() {
let _lock = registry_guard();
let prev = std::env::var("DDGS_IMPERSONATE").ok();
unsafe { std::env::set_var("DDGS_IMPERSONATE", "firefox_135") };
let p = resolve_profile(std::env::var("DDGS_IMPERSONATE").ok().as_deref());
assert_eq!(p.id(), "firefox_135");
unsafe { std::env::remove_var("DDGS_IMPERSONATE") };
if let Some(v) = prev {
unsafe { std::env::set_var("DDGS_IMPERSONATE", v) };
}
}
#[test]
fn e2e_chromium_fingerprint_includes_sec_ch_ua() {
let _lock = registry_guard();
let p = resolve_profile(Some("chrome_131"));
assert!(p.user_agent().contains("Chrome/131"));
}
#[test]
fn e2e_fingerprint_pool_matches_primp_families() {
let _lock = registry_guard();
let p = pick_random_profile();
let id = p.id();
assert!(
id.starts_with("chrome_")
|| id.starts_with("edge_")
|| id.starts_with("firefox_")
|| id.starts_with("safari_"),
"unexpected profile id: {id}"
);
}
#[test]
fn e2e_impersonate_os_windows_pins_chrome_win() {
let _lock = registry_guard();
let p = resolve_profile_with(Some("chrome_131"), Some(ImpersonateOs::Windows));
assert_eq!(p.impersonate_os(), ImpersonateOs::Windows);
}
#[tokio::test]
#[ignore = "live network — compares Rust metasearch vs python ddgs on same query"]
async fn e2e_live_rust_vs_python_ddgs_urls_overlap() {
let _lock = registry_guard();
let query = "Rust programming language";
let settings = DdgsSettings::resolve(&Default::default());
assert_eq!(settings.selection_mode, DdgsSelectionMode::Raw);
assert_eq!(settings.engine_order(), vec![DdgsEngine::Bing]);
let opts = SearchOptions {
max_results: 5,
timeout_secs: 20,
..Default::default()
};
let rust_urls: Vec<String> = match metasearch_text(query, &opts, &settings, "ddgs").await {
Ok(rows) => rows.into_iter().map(|r| r.url).collect(),
Err(e) if e.message.contains("bot") || e.message.contains("blocked") => {
eprintln!("skip live: rust blocked: {e}");
return;
}
Err(e) => panic!("rust metasearch: {e:?}"),
};
let py = Command::new("python3")
.args(["-c", "import sys; from ddgs import DDGS; rows = DDGS(timeout=20).text(sys.argv[1], max_results=5); print(chr(10).join(r['href'] for r in rows))", query])
.env("PYTHONPATH", "/Users/raphaelmansuy/.venv/lib/python3.12/site-packages")
.output()
.expect("python ddgs");
if !py.status.success() {
eprintln!(
"skip live: python failed: {}",
String::from_utf8_lossy(&py.stderr)
);
return;
}
let py_urls: Vec<String> = String::from_utf8_lossy(&py.stdout)
.lines()
.filter(|l| !l.is_empty())
.map(str::to_string)
.collect();
if rust_urls.is_empty() && py_urls.is_empty() {
eprintln!("both empty — IP block or true empty SERP (inconclusive)");
return;
}
eprintln!("rust: {rust_urls:?}");
eprintln!("python: {py_urls:?}");
if !rust_urls.is_empty() && py_urls.is_empty() {
eprintln!(
"rust succeeded where python ddgs 9.0 returned [] (known script quirk in Python)"
);
assert!(rust_urls.len() >= 3, "rust should return meaningful rows");
return;
}
assert_eq!(
rust_urls.is_empty(),
py_urls.is_empty(),
"Rust/Python must agree on success vs empty when Python also succeeds"
);
let overlap = url_sets_overlap(&rust_urls, &py_urls);
assert!(
overlap >= 3,
"expect ≥3 canonical URL overlaps when both succeed (got {overlap})"
);
}