Skip to main content

pact_plugin_driver/
grpc_plugin.rs

1//! gRPC plugin wrapper and process management
2
3use std::process::Command;
4use std::process::Stdio;
5
6use anyhow::anyhow;
7use async_trait::async_trait;
8use log::max_level;
9use os_info::Type;
10use prost::Message;
11use std::path::PathBuf;
12use sysinfo::{Pid, System};
13use tonic::codegen::InterceptedService;
14use tonic::metadata::{Ascii, MetadataValue};
15use tonic::service::Interceptor;
16use tonic::transport::Channel;
17use tonic::{Request, Status};
18use tracing::{debug, warn};
19use uuid::Uuid;
20
21use crate::child_process::ChildPluginProcess;
22use crate::plugin_models::{
23  PactPlugin, PactPluginManifest, PactPluginRpc, PluginInitRequest, PluginInitResponse,
24  PluginInstance, PluginInterfaceVersion,
25};
26use crate::proto::pact_plugin_client::PactPluginClient as PactPluginClientV1;
27use crate::proto::*;
28use crate::proto_v2::{self, pact_plugin_client::PactPluginClient as PactPluginClientV2};
29
30pub(crate) enum PluginClient {
31  V1(PactPluginClientV1<InterceptedService<Channel, PactPluginInterceptor>>),
32  V2(PactPluginClientV2<InterceptedService<Channel, PactPluginInterceptor>>),
33}
34
35impl PluginClient {
36  pub(crate) fn convert_message<T, U>(message: T) -> Result<U, Status>
37  where
38    T: Message,
39    U: Message + Default,
40  {
41    U::decode(message.encode_to_vec().as_slice()).map_err(|err| {
42      Status::internal(format!(
43        "Failed to convert between plugin interface message versions: {}",
44        err
45      ))
46    })
47  }
48
49  async fn init_plugin(
50    &mut self,
51    request: PluginInitRequest,
52  ) -> Result<PluginInitResponse, Status> {
53    match self {
54      PluginClient::V1(client) => client
55        .init_plugin(Request::new(InitPluginRequest {
56          implementation: request.implementation,
57          version: request.version,
58        }))
59        .await
60        .map(|response| PluginInitResponse {
61          catalogue: response.into_inner().catalogue,
62          plugin_capabilities: vec![],
63        }),
64      PluginClient::V2(client) => client
65        .init_plugin(Request::new(proto_v2::InitPluginRequest {
66          implementation: request.implementation,
67          version: request.version,
68          host_capabilities: request.host_capabilities,
69          plugin_instance_id: request.plugin_instance_id,
70        }))
71        .await
72        .and_then(|response| match response.into_inner().response {
73          Some(proto_v2::init_plugin_response::Response::Success(success)) => {
74            Ok(PluginInitResponse {
75              catalogue: success
76                .catalogue
77                .into_iter()
78                .map(Self::convert_message)
79                .collect::<Result<Vec<CatalogueEntry>, Status>>()?,
80              plugin_capabilities: success.plugin_capabilities,
81            })
82          }
83          Some(proto_v2::init_plugin_response::Response::Failure(failure)) => {
84            let mut error = failure.error;
85            if !failure.missing_host_capabilities.is_empty() {
86              error.push_str(" (missing host capabilities: ");
87              error.push_str(failure.missing_host_capabilities.join(", ").as_str());
88              error.push(')');
89            }
90            Err(Status::failed_precondition(error))
91          }
92          None => Err(Status::internal(
93            "Plugin returned an invalid V2 InitPlugin response",
94          )),
95        }),
96    }
97  }
98
99  async fn compare_contents(
100    &mut self,
101    request: CompareContentsRequest,
102  ) -> Result<CompareContentsResponse, Status> {
103    match self {
104      PluginClient::V1(client) => client
105        .compare_contents(Request::new(request))
106        .await
107        .map(|response| response.into_inner()),
108      PluginClient::V2(client) => {
109        let mut v2_req = Self::convert_message::<_, proto_v2::CompareContentsRequest>(request)?;
110        if let Some(id) = crate::test_context::current_test_run_id() {
111          let ctx = v2_req.test_context.get_or_insert_with(prost_types::Struct::default);
112          ctx.fields.entry("testRunId".to_string()).or_insert_with(|| prost_types::Value {
113            kind: Some(prost_types::value::Kind::StringValue(id)),
114          });
115        }
116        client
117          .compare_contents(Request::new(v2_req))
118          .await
119          .and_then(|response| Self::convert_message(response.into_inner()))
120      }
121    }
122  }
123
124  async fn compare_contents_with_metadata(
125    &mut self,
126    request: CompareContentsRequest,
127    chain_id: &str,
128    deadline_ms: u64,
129  ) -> Result<CompareContentsResponse, Status> {
130    match self {
131      PluginClient::V1(client) => {
132        let mut req = Request::new(request);
133        insert_chain_metadata(&mut req, chain_id, deadline_ms)?;
134        client.compare_contents(req).await.map(|response| response.into_inner())
135      }
136      PluginClient::V2(client) => {
137        let mut v2_req = Self::convert_message::<_, proto_v2::CompareContentsRequest>(request)?;
138        if let Some(id) = crate::test_context::current_test_run_id() {
139          let ctx = v2_req.test_context.get_or_insert_with(prost_types::Struct::default);
140          ctx.fields.entry("testRunId".to_string()).or_insert_with(|| prost_types::Value {
141            kind: Some(prost_types::value::Kind::StringValue(id)),
142          });
143        }
144        let mut req = Request::new(v2_req);
145        insert_chain_metadata(&mut req, chain_id, deadline_ms)?;
146        client
147          .compare_contents(req)
148          .await
149          .and_then(|response| Self::convert_message(response.into_inner()))
150      }
151    }
152  }
153
154  async fn generate_content_with_metadata(
155    &mut self,
156    request: GenerateContentRequest,
157    chain_id: &str,
158    deadline_ms: u64,
159  ) -> Result<GenerateContentResponse, Status> {
160    match self {
161      PluginClient::V1(client) => {
162        let mut req = Request::new(request);
163        insert_chain_metadata(&mut req, chain_id, deadline_ms)?;
164        client.generate_content(req).await.map(|response| response.into_inner())
165      }
166      PluginClient::V2(client) => {
167        let mut req = Request::new(Self::convert_message::<_, proto_v2::GenerateContentRequest>(request)?);
168        insert_chain_metadata(&mut req, chain_id, deadline_ms)?;
169        client
170          .generate_content(req)
171          .await
172          .and_then(|response| Self::convert_message(response.into_inner()))
173      }
174    }
175  }
176
177  async fn configure_interaction(
178    &mut self,
179    request: ConfigureInteractionRequest,
180  ) -> Result<ConfigureInteractionResponse, Status> {
181    match self {
182      PluginClient::V1(client) => client
183        .configure_interaction(Request::new(request))
184        .await
185        .map(|response| response.into_inner()),
186      PluginClient::V2(client) => {
187        let mut v2_req =
188          Self::convert_message::<_, proto_v2::ConfigureInteractionRequest>(request)?;
189        if let Some(id) = crate::test_context::current_test_run_id() {
190          let ctx = v2_req.test_context.get_or_insert_with(prost_types::Struct::default);
191          ctx.fields.entry("testRunId".to_string()).or_insert_with(|| prost_types::Value {
192            kind: Some(prost_types::value::Kind::StringValue(id)),
193          });
194        }
195        client
196          .configure_interaction(Request::new(v2_req))
197          .await
198          .and_then(|response| Self::convert_message(response.into_inner()))
199      }
200    }
201  }
202
203  async fn generate_content(
204    &mut self,
205    request: GenerateContentRequest,
206  ) -> Result<GenerateContentResponse, Status> {
207    match self {
208      PluginClient::V1(client) => client
209        .generate_content(Request::new(request))
210        .await
211        .map(|response| response.into_inner()),
212      PluginClient::V2(client) => client
213        .generate_content(Request::new(Self::convert_message::<
214          _,
215          proto_v2::GenerateContentRequest,
216        >(request)?))
217        .await
218        .and_then(|response| Self::convert_message(response.into_inner())),
219    }
220  }
221
222  async fn start_mock_server(
223    &mut self,
224    request: StartMockServerRequest,
225  ) -> Result<StartMockServerResponse, Status> {
226    match self {
227      PluginClient::V1(client) => client
228        .start_mock_server(Request::new(request))
229        .await
230        .map(|response| response.into_inner()),
231      PluginClient::V2(client) => client
232        .start_mock_server(Request::new(Self::convert_message::<
233          _,
234          proto_v2::StartMockServerRequest,
235        >(request)?))
236        .await
237        .and_then(|response| Self::convert_message(response.into_inner())),
238    }
239  }
240
241  async fn shutdown_mock_server(
242    &mut self,
243    request: ShutdownMockServerRequest,
244  ) -> Result<ShutdownMockServerResponse, Status> {
245    match self {
246      PluginClient::V1(client) => client
247        .shutdown_mock_server(Request::new(request))
248        .await
249        .map(|response| response.into_inner()),
250      PluginClient::V2(client) => client
251        .shutdown_mock_server(Request::new(Self::convert_message::<
252          _,
253          proto_v2::MockServerRequest,
254        >(request)?))
255        .await
256        .and_then(|response| {
257          Self::convert_message::<_, ShutdownMockServerResponse>(response.into_inner())
258        }),
259    }
260  }
261
262  async fn start_mock_server_v2(
263    &mut self,
264    request: proto_v2::StartMockServerRequest,
265  ) -> Result<StartMockServerResponse, Status> {
266    match self {
267      PluginClient::V1(_) => Err(Status::unimplemented("V2 interface not supported on V1 plugin")),
268      PluginClient::V2(client) => client
269        .start_mock_server(Request::new(request))
270        .await
271        .and_then(|response| Self::convert_message(response.into_inner())),
272    }
273  }
274
275  async fn prepare_interaction_for_verification_v2(
276    &mut self,
277    request: proto_v2::VerificationPreparationRequest,
278  ) -> Result<VerificationPreparationResponse, Status> {
279    match self {
280      PluginClient::V1(_) => Err(Status::unimplemented("V2 interface not supported on V1 plugin")),
281      PluginClient::V2(client) => client
282        .prepare_interaction_for_verification(Request::new(request))
283        .await
284        .and_then(|response| Self::convert_message(response.into_inner())),
285    }
286  }
287
288  async fn verify_interaction_v2(
289    &mut self,
290    request: proto_v2::VerifyInteractionRequest,
291  ) -> Result<VerifyInteractionResponse, Status> {
292    match self {
293      PluginClient::V1(_) => Err(Status::unimplemented("V2 interface not supported on V1 plugin")),
294      PluginClient::V2(client) => client
295        .verify_interaction(Request::new(request))
296        .await
297        .and_then(|response| Self::convert_message(response.into_inner())),
298    }
299  }
300
301  async fn get_mock_server_results(
302    &mut self,
303    request: MockServerRequest,
304  ) -> Result<MockServerResults, Status> {
305    match self {
306      PluginClient::V1(client) => client
307        .get_mock_server_results(Request::new(request))
308        .await
309        .map(|response| response.into_inner()),
310      PluginClient::V2(client) => client
311        .get_mock_server_results(Request::new(Self::convert_message::<
312          _,
313          proto_v2::MockServerRequest,
314        >(request)?))
315        .await
316        .and_then(|response| Self::convert_message(response.into_inner())),
317    }
318  }
319
320  async fn prepare_interaction_for_verification(
321    &mut self,
322    request: VerificationPreparationRequest,
323  ) -> Result<VerificationPreparationResponse, Status> {
324    match self {
325      PluginClient::V1(client) => client
326        .prepare_interaction_for_verification(Request::new(request))
327        .await
328        .map(|response| response.into_inner()),
329      PluginClient::V2(client) => client
330        .prepare_interaction_for_verification(Request::new(Self::convert_message::<
331          _,
332          proto_v2::VerificationPreparationRequest,
333        >(request)?))
334        .await
335        .and_then(|response| Self::convert_message(response.into_inner())),
336    }
337  }
338
339  async fn verify_interaction(
340    &mut self,
341    request: VerifyInteractionRequest,
342  ) -> Result<VerifyInteractionResponse, Status> {
343    match self {
344      PluginClient::V1(client) => client
345        .verify_interaction(Request::new(request))
346        .await
347        .map(|response| response.into_inner()),
348      PluginClient::V2(client) => client
349        .verify_interaction(Request::new(Self::convert_message::<
350          _,
351          proto_v2::VerifyInteractionRequest,
352        >(request)?))
353        .await
354        .and_then(|response| Self::convert_message(response.into_inner())),
355    }
356  }
357
358  async fn update_catalogue(&mut self, request: Catalogue) -> Result<(), Status> {
359    match self {
360      PluginClient::V1(client) => client
361        .update_catalogue(Request::new(request))
362        .await
363        .map(|_| ()),
364      PluginClient::V2(client) => client
365        .update_catalogue(Request::new(
366          Self::convert_message::<_, proto_v2::Catalogue>(request)?,
367        ))
368        .await
369        .map(|_| ()),
370    }
371  }
372}
373
374/// Attach call-chain cycle detection and deadline metadata to an outbound request to a plugin,
375/// and bound the request's own gRPC timeout to the remaining deadline budget. See
376/// [`crate::call_chain`].
377fn insert_chain_metadata<T>(request: &mut Request<T>, chain_id: &str, deadline_ms: u64) -> Result<(), Status> {
378  let chain_value = MetadataValue::try_from(chain_id)
379    .map_err(|err| Status::internal(format!("Invalid call chain id '{}': {}", chain_id, err)))?;
380  let deadline_value = MetadataValue::try_from(deadline_ms.to_string())
381    .map_err(|err| Status::internal(format!("Invalid deadline value '{}': {}", deadline_ms, err)))?;
382  request.metadata_mut().insert(crate::call_chain::CALL_CHAIN_ID_METADATA_KEY, chain_value);
383  request.metadata_mut().insert(crate::call_chain::DEADLINE_METADATA_KEY, deadline_value);
384  request.set_timeout(crate::call_chain::remaining(deadline_ms));
385  Ok(())
386}
387
388/// Interceptor to inject the server key as an authorisation header
389#[derive(Clone, Debug)]
390pub(crate) struct PactPluginInterceptor {
391  /// Server key to inject
392  server_key: MetadataValue<Ascii>,
393}
394
395impl PactPluginInterceptor {
396  pub(crate) fn new(server_key: &str) -> anyhow::Result<Self> {
397    let token = MetadataValue::try_from(server_key)?;
398    Ok(PactPluginInterceptor { server_key: token })
399  }
400}
401
402impl Interceptor for PactPluginInterceptor {
403  fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
404    request
405      .metadata_mut()
406      .insert("authorization", self.server_key.clone());
407    Ok(request)
408  }
409}
410
411/// Wrapper around `PactPlugin` that provides gRPC connectivity
412#[derive(Debug, Clone)]
413pub struct GrpcPactPlugin {
414  pub plugin: PactPlugin,
415}
416
417impl GrpcPactPlugin {
418  pub fn new(plugin: PactPlugin) -> Self {
419    GrpcPactPlugin { plugin }
420  }
421
422  #[allow(deprecated)]
423  async fn connect_channel(&self) -> anyhow::Result<Channel> {
424    let port = self.plugin.child.port();
425    match Channel::from_shared(format!("http://[::1]:{}", port))?
426      .connect()
427      .await
428    {
429      Ok(channel) => Ok(channel),
430      Err(err) => {
431        debug!("IP6 connection failed, will try IP4 address - {err}");
432        Channel::from_shared(format!("http://127.0.0.1:{}", port))?
433          .connect()
434          .await
435          .map_err(|err| anyhow!(err))
436      }
437    }
438  }
439
440  #[allow(deprecated)]
441  async fn get_plugin_client(&self) -> anyhow::Result<PluginClient> {
442    let channel = self.connect_channel().await?;
443    let interceptor =
444      PactPluginInterceptor::new(self.plugin.child.plugin_info.server_key.as_str())?;
445    match self.plugin.interface_version {
446      PluginInterfaceVersion::V1 => Ok(PluginClient::V1(PactPluginClientV1::with_interceptor(
447        channel,
448        interceptor,
449      ))),
450      PluginInterfaceVersion::V2 => Ok(PluginClient::V2(PactPluginClientV2::with_interceptor(
451        channel,
452        interceptor,
453      ))),
454    }
455  }
456}
457
458#[async_trait]
459impl PactPluginRpc for GrpcPactPlugin {
460  async fn init_plugin(
461    &mut self,
462    request: PluginInitRequest,
463  ) -> anyhow::Result<PluginInitResponse> {
464    let mut client = self.get_plugin_client().await?;
465    client.init_plugin(request).await.map_err(anyhow::Error::from)
466  }
467}
468
469#[async_trait]
470impl PluginInstance for GrpcPactPlugin {
471  fn manifest(&self) -> &PactPluginManifest {
472    &self.plugin.manifest
473  }
474
475  fn instance_id(&self) -> &str {
476    &self.plugin.instance_id
477  }
478
479  fn has_capability(&self, capability: &str) -> bool {
480    self.plugin.has_capability(capability)
481  }
482
483  #[allow(deprecated)]
484  fn kill(&self) {
485    self.plugin.child.kill();
486  }
487
488  async fn compare_contents(
489    &self,
490    request: CompareContentsRequest,
491  ) -> anyhow::Result<CompareContentsResponse> {
492    let mut client = self.get_plugin_client().await?;
493    client.compare_contents(request).await.map_err(anyhow::Error::from)
494  }
495
496  async fn compare_contents_with_chain(
497    &self,
498    request: CompareContentsRequest,
499    chain_id: &str,
500    deadline_ms: u64,
501  ) -> anyhow::Result<CompareContentsResponse> {
502    let mut client = self.get_plugin_client().await?;
503    client
504      .compare_contents_with_metadata(request, chain_id, deadline_ms)
505      .await
506      .map_err(anyhow::Error::from)
507  }
508
509  async fn configure_interaction(
510    &self,
511    request: ConfigureInteractionRequest,
512  ) -> anyhow::Result<ConfigureInteractionResponse> {
513    let mut client = self.get_plugin_client().await?;
514    client.configure_interaction(request).await.map_err(anyhow::Error::from)
515  }
516
517  async fn generate_content(
518    &self,
519    request: GenerateContentRequest,
520  ) -> anyhow::Result<GenerateContentResponse> {
521    let mut client = self.get_plugin_client().await?;
522    client.generate_content(request).await.map_err(anyhow::Error::from)
523  }
524
525  async fn generate_content_with_chain(
526    &self,
527    request: GenerateContentRequest,
528    chain_id: &str,
529    deadline_ms: u64,
530  ) -> anyhow::Result<GenerateContentResponse> {
531    let mut client = self.get_plugin_client().await?;
532    client
533      .generate_content_with_metadata(request, chain_id, deadline_ms)
534      .await
535      .map_err(anyhow::Error::from)
536  }
537
538  async fn start_mock_server(
539    &self,
540    request: StartMockServerRequest,
541  ) -> anyhow::Result<StartMockServerResponse> {
542    let mut client = self.get_plugin_client().await?;
543    client.start_mock_server(request).await.map_err(anyhow::Error::from)
544  }
545
546  async fn start_mock_server_v2(
547    &self,
548    request: proto_v2::StartMockServerRequest,
549  ) -> anyhow::Result<StartMockServerResponse> {
550    let mut client = self.get_plugin_client().await?;
551    client.start_mock_server_v2(request).await.map_err(anyhow::Error::from)
552  }
553
554  async fn shutdown_mock_server(
555    &self,
556    request: ShutdownMockServerRequest,
557  ) -> anyhow::Result<ShutdownMockServerResponse> {
558    let mut client = self.get_plugin_client().await?;
559    client.shutdown_mock_server(request).await.map_err(anyhow::Error::from)
560  }
561
562  async fn get_mock_server_results(
563    &self,
564    request: MockServerRequest,
565  ) -> anyhow::Result<MockServerResults> {
566    let mut client = self.get_plugin_client().await?;
567    client.get_mock_server_results(request).await.map_err(anyhow::Error::from)
568  }
569
570  async fn prepare_interaction_for_verification(
571    &self,
572    request: VerificationPreparationRequest,
573  ) -> anyhow::Result<VerificationPreparationResponse> {
574    let mut client = self.get_plugin_client().await?;
575    client
576      .prepare_interaction_for_verification(request)
577      .await
578      .map_err(anyhow::Error::from)
579  }
580
581  async fn prepare_interaction_for_verification_v2(
582    &self,
583    request: proto_v2::VerificationPreparationRequest,
584  ) -> anyhow::Result<VerificationPreparationResponse> {
585    let mut client = self.get_plugin_client().await?;
586    client
587      .prepare_interaction_for_verification_v2(request)
588      .await
589      .map_err(anyhow::Error::from)
590  }
591
592  async fn verify_interaction(
593    &self,
594    request: VerifyInteractionRequest,
595  ) -> anyhow::Result<VerifyInteractionResponse> {
596    let mut client = self.get_plugin_client().await?;
597    client.verify_interaction(request).await.map_err(anyhow::Error::from)
598  }
599
600  async fn verify_interaction_v2(
601    &self,
602    request: proto_v2::VerifyInteractionRequest,
603  ) -> anyhow::Result<VerifyInteractionResponse> {
604    let mut client = self.get_plugin_client().await?;
605    client.verify_interaction_v2(request).await.map_err(anyhow::Error::from)
606  }
607
608  async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()> {
609    let mut client = self.get_plugin_client().await?;
610    client.update_catalogue(request).await.map_err(anyhow::Error::from)
611  }
612}
613
614/// Start a plugin process and return a PactPlugin with the child process attached
615pub(crate) async fn start_plugin_process(manifest: &PactPluginManifest) -> anyhow::Result<PactPlugin> {
616  debug!("Starting plugin with manifest {:?}", manifest);
617
618  let os_info = os_info::get();
619  debug!("Detected OS: {}", os_info);
620  let mut path = if let Some(entry_point) = manifest.entry_points.get(&os_info.to_string()) {
621    PathBuf::from(entry_point)
622  } else if os_info.os_type() == Type::Windows && manifest.entry_points.contains_key("windows") {
623    PathBuf::from(manifest.entry_points.get("windows").unwrap())
624  } else {
625    PathBuf::from(&manifest.entry_point)
626  };
627  if !path.is_absolute() || !path.exists() {
628    path = PathBuf::from(manifest.plugin_dir.clone()).join(path);
629  }
630  debug!("Starting plugin using {:?}", &path);
631
632  let host_port = match crate::plugin_host::ensure_plugin_host_running().await {
633    Ok(port) => Some(port),
634    Err(err) => {
635      warn!("Could not start PluginHost server, Log RPC forwarding will be unavailable: {}", err);
636      None
637    }
638  };
639
640  let log_level = max_level();
641  let mut child_command = Command::new(path.clone());
642  let mut child_command = child_command
643    .env("LOG_LEVEL", log_level.to_string())
644    .env("RUST_LOG", log_level.to_string())
645    .current_dir(manifest.plugin_dir.clone());
646
647  let instance_id = Uuid::new_v4().to_string();
648
649  child_command = child_command.env("PACT_PLUGIN_INSTANCE_ID", &instance_id);
650  if let Some(port) = host_port {
651    child_command = child_command.env("PACT_PLUGIN_HOST", format!("127.0.0.1:{}", port));
652  }
653
654  if let Some(args) = &manifest.args {
655    child_command = child_command.args(args);
656  }
657
658  let child = child_command
659    .stdout(Stdio::piped())
660    .stderr(Stdio::piped())
661    .spawn()
662    .map_err(|err| {
663      anyhow!(
664        "Was not able to start plugin process for '{}' - {}",
665        path.to_string_lossy(),
666        err
667      )
668    })?;
669  let child_pid = child.id();
670  debug!("Plugin {} started with PID {} (instance {})", manifest.name, child_pid, instance_id);
671  crate::plugin_manager::register_plugin_instance(&instance_id, &manifest.name);
672
673  match ChildPluginProcess::new(child, manifest, instance_id.clone()).await {
674    Ok(child) => {
675      let plugin = PactPlugin::new(manifest, child)?;
676      Ok(plugin)
677    }
678    Err(err) => {
679      crate::plugin_manager::deregister_plugin_instance(&instance_id);
680      let mut s = System::new();
681      s.refresh_processes();
682      if let Some(process) = s.process(Pid::from_u32(child_pid)) {
683        #[cfg(not(windows))]
684        process.kill();
685        // revert windows specific logic once https://github.com/GuillaumeGomez/sysinfo/pull/1341/files is merged/released
686        #[cfg(windows)]
687        let _ = Command::new("taskkill.exe")
688          .arg("/PID")
689          .arg(child_pid.to_string())
690          .arg("/F")
691          .arg("/T")
692          .output();
693      } else {
694        warn!("Child process with PID {} was not found", child_pid);
695      }
696      Err(err)
697    }
698  }
699}
700
701#[cfg(test)]
702pub(crate) mod tests {
703  use tonic::Status;
704
705  use crate::plugin_models::PluginInitResponse;
706  use crate::proto::*;
707
708  use super::PluginClient;
709
710  #[test]
711  fn converts_between_v1_and_v2_messages() {
712    use std::collections::HashMap;
713    use crate::plugin_models::PluginInitRequest;
714    use crate::proto_v2;
715
716    let request = PluginInitRequest {
717      implementation: "plugin-driver-rust".to_string(),
718      version: "1.0.0-beta.1".to_string(),
719      host_capabilities: vec!["interaction/request-response".to_string()],
720      plugin_instance_id: "test-instance-id".to_string(),
721    };
722
723    let converted_request = proto_v2::InitPluginRequest {
724      implementation: request.implementation,
725      version: request.version,
726      host_capabilities: request.host_capabilities,
727      plugin_instance_id: request.plugin_instance_id,
728    };
729    assert_eq!(converted_request.implementation, "plugin-driver-rust");
730    assert_eq!(converted_request.version, "1.0.0-beta.1");
731    assert_eq!(
732      converted_request.host_capabilities,
733      vec!["interaction/request-response"]
734    );
735
736    let response = proto_v2::InitPluginResponse {
737      response: Some(proto_v2::init_plugin_response::Response::Success(
738        proto_v2::InitPluginSuccess {
739          catalogue: vec![proto_v2::CatalogueEntry {
740            r#type: proto_v2::catalogue_entry::EntryType::ContentMatcher as i32,
741            key: "test".to_string(),
742            values: HashMap::new(),
743          }],
744          plugin_capabilities: vec!["plugin/verification".to_string()],
745        },
746      )),
747    };
748
749    let converted_response = match response.response.unwrap() {
750      proto_v2::init_plugin_response::Response::Success(success) => PluginInitResponse {
751        catalogue: success
752          .catalogue
753          .into_iter()
754          .map(PluginClient::convert_message)
755          .collect::<Result<Vec<CatalogueEntry>, Status>>()
756          .unwrap(),
757        plugin_capabilities: success.plugin_capabilities,
758      },
759      _ => unreachable!(),
760    };
761    assert_eq!(converted_response.catalogue.len(), 1);
762    assert_eq!(converted_response.catalogue[0].key, "test");
763    assert_eq!(
764      converted_response.catalogue[0].r#type,
765      catalogue_entry::EntryType::ContentMatcher as i32
766    );
767    assert_eq!(converted_response.plugin_capabilities, vec!["plugin/verification"]);
768  }
769}