use std::{env, thread};
use std::fmt::Write;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::anyhow;
use itertools::Itertools;
use pact_mock_server::builder::MockServerBuilder;
use pact_mock_server::matching::MatchResult;
use pact_mock_server::mock_server;
use pact_mock_server::mock_server::{MockServerConfig, MockServerMetrics};
#[cfg(feature = "plugins")] use pact_plugin_driver::plugin_manager::{drop_plugin_access, increment_plugin_access};
#[cfg(feature = "plugins")] use pact_plugin_driver::plugin_models::{PluginDependency, PluginDependencyType};
use tokio::runtime::Runtime;
#[allow(unused_imports)] use tracing::{debug, trace, warn};
use url::Url;
#[cfg(feature = "colour")] use yansi::Paint;
use pact_matching::metrics::{MetricEvent, send_metrics};
use pact_models::pact::Pact;
#[cfg(feature = "plugins")] use pact_models::plugins::PluginData;
use pact_models::v4::http_parts::HttpRequest;
use crate::mock_server::ValidatingMockServer;
use crate::util::panic_or_print_error;
pub struct ValidatingHttpMockServer {
description: String,
url: Url,
mock_server: mock_server::MockServer,
output_dir: Option<PathBuf>,
overwrite: bool,
runtime: Option<Arc<Runtime>>
}
impl ValidatingHttpMockServer {
pub fn start(
pact: Box<dyn Pact + Send + Sync>,
output_dir: Option<PathBuf>,
mock_server_config: Option<MockServerConfig>
) -> Box<dyn ValidatingMockServer> {
debug!("Starting mock server from pact {:?}", pact);
let runtime = Arc::new(tokio::runtime::Builder::new_multi_thread()
.enable_all()
.worker_threads(2)
.build()
.expect("Could not start a new Tokio runtime"));
#[cfg(feature = "plugins")]
Self::increment_plugin_access(&pact.plugin_data());
let tname = format!("test({})-pact-mock-server",
thread::current().name().unwrap_or("<unknown>")
);
let rt = runtime.clone();
let mock_server = thread::Builder::new()
.name(tname)
.spawn(move || {
let mut builder = MockServerBuilder::new()
.with_pact(pact);
if let Some(config) = mock_server_config {
builder = builder.with_config(config);
}
if !builder.address_assigned() {
builder = builder.bind_to_ip4_port(0)
};
rt.block_on(builder.start())
})
.expect("INTERNAL ERROR: Could not spawn a thread to run the mock server")
.join()
.expect("INTERNAL ERROR: Failed to spawn the mock server task onto the runtime")
.expect("Failed to start the mock server");
let pact = &mock_server.pact;
let description = format!("{}/{}", pact.consumer().name, pact.provider().name);
let url_str = mock_server.url();
Box::new(ValidatingHttpMockServer {
description,
url: url_str.parse().expect(format!("invalid mock server URL '{}'", url_str).as_str()),
mock_server,
output_dir,
overwrite: false,
runtime: Some(runtime)
})
}
#[cfg(feature = "plugins")]
fn decrement_plugin_access(plugins: &Vec<PluginData>) {
for plugin in plugins {
let dependency = PluginDependency {
name: plugin.name.clone(),
version: Some(plugin.version.clone()),
dependency_type: PluginDependencyType::Plugin
};
drop_plugin_access(&dependency);
}
}
#[cfg(feature = "plugins")]
fn increment_plugin_access(plugins: &Vec<PluginData>) {
for plugin in plugins {
let dependency = PluginDependency {
name: plugin.name.clone(),
version: Some(plugin.version.clone()),
dependency_type: PluginDependencyType::Plugin
};
increment_plugin_access(&dependency);
}
}
pub async fn start_async(
pact: Box<dyn Pact + Send + Sync>,
output_dir: Option<PathBuf>,
mock_server_config: Option<MockServerConfig>
) -> Box<dyn ValidatingMockServer> {
debug!("Starting mock server from pact {:?}", pact);
#[cfg(feature = "plugins")] Self::increment_plugin_access(&pact.plugin_data());
let mut builder = MockServerBuilder::new()
.with_pact(pact);
if let Some(config) = mock_server_config {
builder = builder.with_config(config);
}
if !builder.address_assigned() {
builder = builder.bind_to_ip4_port(0)
};
let mock_server = builder
.start()
.await
.expect("Could not start the mock server");
let pact = &mock_server.pact;
let description = format!("{}/{}", pact.consumer().name, pact.provider().name);
let url_str = mock_server.url();
Box::new(ValidatingHttpMockServer {
description,
url: url_str.parse().expect("invalid mock server URL"),
mock_server,
output_dir,
overwrite: false,
runtime: None
})
}
fn drop_helper(&mut self) -> anyhow::Result<()> {
self.mock_server.shutdown()?;
#[cfg(feature = "plugins")] Self::decrement_plugin_access(&self.mock_server.pact.plugin_data());
if let Some(runtime) = self.runtime.take() {
if let Some(runtime) = Arc::into_inner(runtime) {
runtime.shutdown_background();
}
}
let interactions = self.mock_server.pact.interactions().len();
thread::spawn(move || {
send_metrics(MetricEvent::ConsumerTestRun {
interactions,
test_framework: "pact_consumer".to_string(),
app_name: "pact_consumer".to_string(),
app_version: env!("CARGO_PKG_VERSION").to_string()
});
});
let mismatches = self.mock_server.mismatches();
if mismatches.is_empty() {
let output_dir = self.output_dir.as_ref()
.map(|dir| {
let dir = dir.to_string_lossy().to_string();
if dir.is_empty() { None } else { Some(dir) }
})
.flatten()
.unwrap_or_else(|| {
let val = env::var("PACT_OUTPUT_DIR");
debug!("env:PACT_OUTPUT_DIR = {:?}", val);
val.unwrap_or_else(|_| "target/pacts".to_owned())
});
debug!("Pact output_dir = '{}'", output_dir);
let overwrite = env::var("PACT_OVERWRITE")
.map(|v| {
debug!("env:PACT_OVERWRITE = {:?}", v);
v == "true"
})
.ok()
.unwrap_or(self.overwrite);
self.mock_server.write_pact(&Some(output_dir), overwrite)
.map_err(|err| anyhow!("error writing pact: {}", err))?;
Ok(())
} else {
Err(anyhow!(self.display_errors(mismatches)))
}
}
#[cfg(feature = "colour")]
fn display_errors(&self, mismatches: Vec<MatchResult>) -> String {
let size = termsize::get()
.map(|sz| if sz.cols > 2 { sz.cols - 2 } else { 0 })
.unwrap_or(78);
let pad = "-".repeat(size as usize);
let mut msg = format!(" {} \nMock server {} failed verification:\n", pad, self.description.white().bold());
for mismatch in mismatches {
match mismatch {
MatchResult::RequestMatch(..) => {
warn!("list of mismatches contains a match");
}
MatchResult::RequestMismatch(request, _, mismatches) => {
let _ = writeln!(&mut msg, "\n - request {}:\n", request);
for m in mismatches {
let _ = writeln!(&mut msg, " - {}", m.description());
}
}
MatchResult::RequestNotFound(request) => {
let _ = writeln!(&mut msg, "\n - received unexpected request {}:\n", short_description(&request).white().bold());
let debug_str = format!("{:#?}", request);
let debug_padded = debug_str.lines().map(|ln| format!(" {}", ln)).join("\n");
let _ = writeln!(&mut msg, "{}", debug_padded.italic());
}
MatchResult::MissingRequest(request) => {
let _ = writeln!(
&mut msg,
"\n - request {} expected, but never occurred:\n", short_description(&request).white().bold(),
);
let debug_str = format!("{:#?}", request);
let debug_padded = debug_str.lines().map(|ln| format!(" {}", ln)).join("\n");
let _ = writeln!(&mut msg, "{}", debug_padded.italic());
}
}
}
let _ = writeln!(&mut msg, " {} ", pad);
msg
}
#[cfg(not(feature = "colour"))]
fn display_errors(&self, mismatches: Vec<MatchResult>) -> String {
let size = termsize::get()
.map(|sz| if sz.cols > 2 { sz.cols - 2 } else { 0 })
.unwrap_or(78);
let pad = "-".repeat(size as usize);
let mut msg = format!(" {} \nMock server {} failed verification:\n", pad, self.description);
for mismatch in mismatches {
match mismatch {
MatchResult::RequestMatch(..) => {
warn!("list of mismatches contains a match");
}
MatchResult::RequestMismatch(request, _, mismatches) => {
let _ = writeln!(&mut msg, "\n - request {}:\n", request);
for m in mismatches {
let _ = writeln!(&mut msg, " - {}", m.description());
}
}
MatchResult::RequestNotFound(request) => {
let _ = writeln!(&mut msg, "\n - received unexpected request {}:\n", short_description(&request));
let debug_str = format!("{:#?}", request);
let _ = writeln!(&mut msg, "{}", debug_str.lines().map(|ln| format!(" {}", ln)).join("\n"));
}
MatchResult::MissingRequest(request) => {
let _ = writeln!(
&mut msg,
"\n - request {} expected, but never occurred:\n", short_description(&request),
);
let debug_str = format!("{:#?}", request);
let _ = writeln!(&mut msg, "{}", debug_str.lines().map(|ln| format!(" {}", ln)).join("\n"));
}
}
}
let _ = writeln!(&mut msg, " {} ", pad);
msg
}
}
fn short_description(request: &HttpRequest) -> String {
format!("{} {}", request.method.to_uppercase(), request.path)
}
impl ValidatingMockServer for ValidatingHttpMockServer {
fn url(&self) -> Url {
self.url.clone()
}
fn path(&self, path: &str) -> Url {
self.url.join(path.as_ref()).expect("could not parse URL")
}
fn status(&self) -> Vec<MatchResult> {
self.mock_server.mismatches()
}
fn metrics(&self) -> MockServerMetrics {
self.mock_server.metrics.lock().unwrap().clone()
}
}
impl Drop for ValidatingHttpMockServer {
fn drop(&mut self) {
let result = self.drop_helper();
if let Err(msg) = result {
panic_or_print_error(&msg);
}
}
}