Skip to main content

pmpx_testkit/
lib.rs

1//! Test helpers for plugin authors.
2//!
3//! A plugin's [`command`](pmpx_plugin::PackageManager::command) is a pure function: it is handed a
4//! [`Context`](pmpx_plugin::Context) and answers with a command. That makes it testable without a
5//! project, a host, or a process -- and this crate is the two ways of doing it:
6//!
7//! - **By hand, fast.** [`context`] is the builder, re-exported so a test needs one import. Build the
8//!   exact context a case is about and assert the answer.
9//! - **For real.** [`Fixture`] writes a directory of files, runs the **real** detection over them (the
10//!   same [`pmpx-detect`](pmpx_detect) the host uses, with the markers your manifest declares), and hands
11//!   you the context your plugin would have been called with. This is what catches "my plugin stopped
12//!   matching this project".
13//!
14//! And one thing a plugin cannot do alone: [`capture`] installs a fake host, so a test can assert the
15//! lines the plugin wrote with [`debug!`](macro@pmpx_plugin::debug) -- which is the only channel its text has.
16//!
17//! ```no_run
18//! use pmpx_testkit::{capture, context, Fixture};
19//! # use pmpx_plugin::{CommandSpec, Context, Family, PackageManager, PluginError, Verb};
20//! # struct Mine;
21//! # impl PackageManager for Mine {
22//! #     fn name(&self) -> &str { "mine" }
23//! #     fn family(&self) -> Family { Family::NODE }
24//! #     fn command(&self, ctx: &Context, verb: Verb, args: &[std::ffi::OsString]) -> Result<CommandSpec, PluginError> {
25//! #         Ok(CommandSpec::new("tool").args(args.iter()))
26//! #     }
27//! # }
28//! // The case itself: two matched files, one of them read.
29//! let ctx = context()
30//!     .project_root("/work")
31//!     .matched(["pnpm-lock.yaml"])
32//!     .file("package.json", "{\"name\":\"x\"}")
33//!     .build();
34//! let plan = Mine.command(&ctx, Verb::Install, &[]).unwrap();
35//! assert_eq!(plan.program, "tool");
36//!
37//! // The same case, from a directory and the real detection.
38//! let fixture = Fixture::new()
39//!     .file("pnpm-lock.yaml", "")
40//!     .plugin("mine", "node", &["pnpm-lock.yaml"], &["package.json"])
41//!     .declares(["package.json"])
42//!     .file("package.json", "{\"name\":\"x\"}");
43//! let ctx = fixture.context();
44//! assert!(ctx.has_matched("pnpm-lock.yaml"));
45//!
46//! // And what the plugin says while it runs.
47//! let host = capture();
48//! let out = Mine.command(&ctx, Verb::Install, &[]);
49//! assert!(host.take().is_empty(), "this plugin is quiet");
50//! ```
51
52mod fixture;
53mod host;
54
55pub use fixture::Fixture;
56pub use host::{capture, Captured};
57
58/// The context builder, for a test that only needs a [`Context`](pmpx_plugin::Context) by hand.
59///
60/// The same builder the host's own tests use: there is exactly one way to build a context, and a plugin
61/// author should not have to learn a second one.
62pub fn context() -> pmpx_plugin::ContextBuilder {
63    pmpx_plugin::Context::builder()
64}