diff --git a/Cargo.toml b/Cargo.toml index ce83ccc..c44dc86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,4 @@ syn = { version = "0.15", features = ["full"] } toml = "0.4" url = "1.5.1" itertools = "0.7" +regex = "1.0" diff --git a/src/lib.rs b/src/lib.rs index 874c1cc..0c52e64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,12 +3,15 @@ //! //! When making a release of a Rust project, you typically need to //! adjust some version numbers in your code and documentation. This -//! crate gives you macros that covers the two usual cases where +//! crate gives you macros that covers some typical cases where //! version numbers need updating: //! //! * TOML examples in the `README.md` files that show how to add a //! dependency on your crate. See [`assert_markdown_deps_updated`]. //! +//! * A `Changelog.md` file that should at least mention the current +//! version. See [`assert_contains_regex`]. +//! //! * The [`html_root_url`] attribute that tells other crates where to //! find your documentation. See [`assert_html_root_url_updated`]. //! @@ -44,12 +47,14 @@ //! [`html_root_url`]: https://rust-lang-nursery.github.io/api-guidelines/documentation.html#crate-sets-html_root_url-attribute-c-html-root //! [`assert_markdown_deps_updated`]: macro.assert_markdown_deps_updated.html //! [`assert_html_root_url_updated`]: macro.assert_html_root_url_updated.html +//! [`assert_contains_regex`]: macro.assert_contains_regex.html #![doc(html_root_url = "https://docs.rs/version-sync/0.5.0")] #![deny(missing_docs)] extern crate itertools; extern crate pulldown_cmark; +extern crate regex; extern crate semver_parser; extern crate syn; extern crate toml; @@ -61,6 +66,7 @@ use std::result; use itertools::join; use pulldown_cmark::{Event, Parser, Tag}; +use regex::Regex; use semver_parser::range::parse as parse_request; use semver_parser::range::{Op, VersionReq}; use semver_parser::version::parse as parse_version; @@ -341,6 +347,120 @@ macro_rules! assert_markdown_deps_updated { }; } +/// Assert that versions numbers are up to date via a regex. +/// +/// This macro allows you verify that the current version number is +/// mentioned in a particular file, such as a changelog file. You do +/// this by specifying a regular expression which will be matched +/// against the file. +/// +/// The macro calls [`check_contains_regex`] on the file name given. +/// The package name and current package version is automatically +/// taken from the `$CARGO_PKG_NAME` and `$CARGO_PKG_VERSION` +/// environment variables. These environment variables are +/// automatically set by Cargo when compiling your crate. +/// +/// # Usage +/// +/// The typical way to use this macro is from an integration test: +/// +/// ```rust +/// #[macro_use] +/// extern crate version_sync; +/// +/// #[test] +/// # fn fake_hidden_test_case() {} +/// # // The above function ensures test_readme_mentions_version is +/// # // compiled. +/// fn test_readme_mentions_version() { +/// assert_contains_regex!("README.md", "^### Version {version}"); +/// } +/// +/// # fn main() { +/// # test_readme_mentions_version(); +/// # } +/// ``` +/// +/// Tests are run with the current directory set to directory where +/// your `Cargo.toml` file is, so this will find a `README.md` file +/// next to your `Cargo.toml` file. It will then check that there is a +/// heading mentioning the current version of your crate. +/// +/// The regular expression can contain placeholders which are replaced +/// before the regular expression search begins: +/// +/// * `{version}`: the current version number of your package. +/// * `{name}`: the name of your package. +/// +/// This way you can search for things like `"Latest version is: +/// {version}"` and make sure you update your READMEs and changelogs +/// consistently. +/// +/// # Panics +/// +/// If the regular expression cannot be found, `panic!` will be +/// invoked and your integration test will fail. +/// +/// [`check_contains_regex`]: fn.check_contains_regex.html +#[macro_export] +macro_rules! assert_contains_regex { + ($path:expr, $format:expr) => { + let pkg_name = env!("CARGO_PKG_NAME"); + let pkg_version = env!("CARGO_PKG_VERSION"); + if let Err(err) = $crate::check_contains_regex($path, $format, pkg_name, pkg_version) { + panic!(err); + } + }; +} + +/// Check that `path` contain the regular expression given by +/// `template`. +/// +/// The placeholders `{name}` and `{version}` will be replaced with +/// `pkg_name` and `pkg_version`, if they are present in `template`. +/// It is okay if `template` do not contain these placeholders. +/// +/// The matching is done in multi-line mode, which means that `^` in +/// the regular expression will match the beginning of any line in the +/// file, not just the very beginning of the file. +/// +/// # Errors +/// +/// If the regular expression cannot be found, an `Err` is returned +/// with a succinct error message. Status information has then already +/// been printed on `stdout`. +pub fn check_contains_regex( + path: &str, + template: &str, + pkg_name: &str, + pkg_version: &str, +) -> Result<()> { + // Expand the optional {name} and {version} placeholders in the + // template. This is almost like + // + // format!(template, name = pkg_name, version = pkg_version) + // + // but allows the user to leave out unnecessary placeholders. + let regex = template + .replace("{name}", pkg_name) + .replace("{version}", pkg_version); + + // We enable multi-line mode by default. + let re = Regex::new(&(String::from("(?m)") + ®ex)) + .map_err(|err| format!("invalid regular expression: {}", err))?; + let text = read_file(path).map_err(|err| format!("could not read {}: {}", path, err))?; + + println!("Searching for {:?} in {}...", regex, path); + match re.find(&text) { + Some(m) => { + let line_no = text[..m.start()].lines().count(); + println!("{} (line {}) ... ok", path, line_no + 1); + Ok(()) + } + None => Err(format!("could not find {:?} in {}", regex, path)), + } +} + fn url_matches(value: &str, pkg_name: &str, version: &Version) -> Result<()> { let url = Url::parse(value).map_err(|err| format!("parse error: {}", err))?; diff --git a/tests/version-numbers.rs b/tests/version-numbers.rs index 411eea5..997b30d 100644 --- a/tests/version-numbers.rs +++ b/tests/version-numbers.rs @@ -6,6 +6,11 @@ fn test_readme_deps() { assert_markdown_deps_updated!("README.md"); } +#[test] +fn test_readme_changelog() { + assert_contains_regex!("README.md", "^### Version {version}"); +} + #[test] fn test_html_root_url() { assert_html_root_url_updated!("src/lib.rs");