use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use futures::stream::BoxStream;
use super::jsonfmt::Json;
use crate::core::engine::{EngineAdapter, TraceEngineEvent};
#[async_trait]
pub trait TracePage: Send + Sync {
fn engine(&self) -> Option<String> {
None
}
fn context_key(&self) -> Option<usize> {
None
}
fn page_key(&self) -> Option<usize> {
None
}
fn has_context(&self) -> bool {
false
}
async fn evaluate_function(&self, source: &str, argument: &Json) -> Result<Json, String>;
async fn screenshot(&self) -> Result<Option<Vec<u8>>, String> {
Ok(None)
}
async fn add_init_script(
&self,
_source: &str,
_argument: &Json,
) -> Result<Option<String>, String> {
Ok(None)
}
async fn remove_init_script(&self, _identifier: &str) -> Result<(), String> {
Ok(())
}
async fn events(&self) -> Option<BoxStream<'static, TraceEngineEvent>> {
None
}
}
pub(crate) fn call_expression(source: &str, argument: &Json) -> String {
format!("({source})({})", argument.to_compact())
}
pub(crate) fn json_text_expression(source: &str, argument: &Json) -> String {
format!(
"Promise.resolve().then(async () => {{ const value = await {}; \
return value === undefined ? null : JSON.stringify(value); }})",
call_expression(source, argument)
)
}
#[derive(Clone)]
pub struct AdapterTracePage {
adapter: Arc<dyn EngineAdapter>,
}
impl AdapterTracePage {
pub fn new(adapter: Arc<dyn EngineAdapter>) -> Self {
Self { adapter }
}
fn key(&self) -> usize {
Arc::as_ptr(&self.adapter).cast::<()>() as usize
}
}
#[async_trait]
impl TracePage for AdapterTracePage {
fn engine(&self) -> Option<String> {
Some(self.adapter.engine_type().to_string())
}
fn context_key(&self) -> Option<usize> {
Some(self.key())
}
fn page_key(&self) -> Option<usize> {
Some(self.key())
}
fn has_context(&self) -> bool {
true
}
async fn evaluate_function(&self, source: &str, argument: &Json) -> Result<Json, String> {
let value = self
.adapter
.evaluate(&json_text_expression(source, argument))
.await
.map_err(|error| error.to_string())?;
match value {
serde_json::Value::String(text) => {
Json::parse(&text).map_err(|error| format!("unreadable page result: {error}"))
}
serde_json::Value::Null => Ok(Json::Null),
other => Ok(Json::from(other)),
}
}
async fn screenshot(&self) -> Result<Option<Vec<u8>>, String> {
self.adapter
.screenshot()
.await
.map(Some)
.map_err(|error| error.to_string())
}
async fn add_init_script(
&self,
source: &str,
argument: &Json,
) -> Result<Option<String>, String> {
self.adapter
.add_init_script(&call_expression(source, argument))
.await
.map_err(|error| error.to_string())
}
async fn remove_init_script(&self, identifier: &str) -> Result<(), String> {
self.adapter
.remove_init_script(identifier)
.await
.map_err(|error| error.to_string())
}
async fn events(&self) -> Option<BoxStream<'static, TraceEngineEvent>> {
self.adapter.trace_events().await
}
}
pub(crate) async fn with_deadline<T>(
work: impl Future<Output = Result<T, String>>,
timeout_ms: u64,
what: &str,
) -> Result<T, String> {
if timeout_ms == 0 {
return work.await;
}
match tokio::time::timeout(Duration::from_millis(timeout_ms), work).await {
Ok(outcome) => outcome,
Err(_) => Err(format!("{what} timed out after {timeout_ms}ms")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traces::jsonfmt::JsonObject;
#[test]
fn calls_keep_the_argument_order() {
let argument = Json::from(JsonObject::new().with("b", 1).with("a", "x"));
assert_eq!(
call_expression("function f(o) {}", &argument),
r#"(function f(o) {})({"b":1,"a":"x"})"#
);
assert!(json_text_expression("function f() {}", &Json::Null)
.contains("await (function f() {})(null);"));
}
#[tokio::test]
async fn deadlines_name_what_timed_out() {
let slow = async {
tokio::time::sleep(Duration::from_secs(5)).await;
Ok::<_, String>(())
};
assert_eq!(
with_deadline(slow, 5, "trace screenshot").await,
Err("trace screenshot timed out after 5ms".to_string())
);
assert_eq!(
with_deadline(async { Ok::<_, String>(1) }, 0, "x").await,
Ok(1)
);
}
}