From 73270f954c8bab77083799ade9a6d14b7336458b Mon Sep 17 00:00:00 2001 From: Martin Geisler Date: Mon, 20 Sep 2021 19:56:30 +0200 Subject: [PATCH] Add simple `check_contains_version` function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This function searches for the a template using a simple substring match. It is always enabled since it doesn’t need anything outside the standard library. --- .github/workflows/build.yml | 7 +-- src/contains_version.rs | 67 +++++++++++++++++++++++++++ src/helpers.rs | 15 ++++--- src/lib.rs | 90 +++++++++++++++++++++++++++++++++---- 4 files changed, 158 insertions(+), 21 deletions(-) create mode 100644 src/contains_version.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12f8fc9..0951e2a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,12 +34,9 @@ jobs: # Verify that features work by themselves # Features should not interfere with each other - # Should only need to check 0, 1, and (all) 3 features enabled - # Use if statement to invert exit code: this should fail - - name: Check failure to build with no features - run: if cargo check --no-default-features; then false; else true; fi - shell: bash + - name: Build and test with no default feature + run: cargo test --no-default-features - name: Build and test with markdown_deps_updated feature run: cargo test --no-default-features --features markdown_deps_updated diff --git a/src/contains_version.rs b/src/contains_version.rs new file mode 100644 index 0000000..7fafedc --- /dev/null +++ b/src/contains_version.rs @@ -0,0 +1,67 @@ +use crate::helpers::{read_file, Result}; + +/// Check that `path` contain the substring 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. +/// +/// See [`check_contains_regex`](crate::check_contains_regex) if you +/// want to match with a regular expression instead. +/// +/// # Errors +/// +/// If the template 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_version( + 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 pattern = template + .replace("{name}", &pkg_name) + .replace("{version}", &pkg_version); + + let text = read_file(path).map_err(|err| format!("could not read {}: {}", path, err))?; + + println!("Searching for \"{}\" in {}...", template, path); + match text.find(&pattern) { + Some(idx) => { + let line_no = text[..idx].lines().count(); + println!("{} (line {}) ... ok", path, line_no + 1); + Ok(()) + } + None => Err(format!("could not find \"{}\" in {}", pattern, path)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pattern_not_found() { + assert_eq!( + check_contains_version("README.md", "should not be found", "foobar", "1.2.3"), + Err(String::from( + "could not find \"should not be found\" in README.md" + )) + ) + } + + #[test] + fn pattern_found() { + assert_eq!( + check_contains_version("README.md", "{name}", "version-sync", "1.2.3"), + Ok(()) + ) + } +} diff --git a/src/helpers.rs b/src/helpers.rs index 1bd7b3a..aa7fc46 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,4 +1,5 @@ -// Imports are inside the function bodies to scope them in a cfg block. +use std::fs::File; +use std::io::{self, Read}; /// The common result type, our errors will be simple strings. pub type Result = std::result::Result; @@ -28,10 +29,8 @@ where /// Return all data from `path`. Line boundaries are normalized from /// "\r\n" to "\n" to make sure "^" and "$" will match them. See /// https://github.com/rust-lang/regex/issues/244 for details. -pub fn read_file(path: &str) -> std::io::Result { - use std::io::Read; - - let mut file = std::fs::File::open(path)?; +pub fn read_file(path: &str) -> io::Result { + let mut file = File::open(path)?; let mut buf = String::new(); file.read_to_string(&mut buf)?; Ok(buf.replace("\r\n", "\n")) @@ -45,8 +44,10 @@ pub fn indent(text: &str) -> String { /// Verify that the version range request matches the given version. #[cfg(any(feature = "html_root_url_updated", feature = "markdown_deps_updated"))] -pub fn version_matches_request(version: &semver_parser::version::Version, - request: &semver_parser::range::VersionReq) -> Result<()> { +pub fn version_matches_request( + version: &semver_parser::version::Version, + request: &semver_parser::range::VersionReq, +) -> Result<()> { use semver_parser::range::Op; if request.predicates.len() != 1 { // Can only handle simple dependencies diff --git a/src/lib.rs b/src/lib.rs index 5c24ad6..8d14827 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,13 +13,14 @@ //! //! * A `Changelog.md` file that should at least mention the current //! version, gated behind the "regex_version" feature. -//! See [`assert_contains_regex`]. +//! See [`assert_contains_regex`] and [`assert_contains_version`]. //! //! * The [`html_root_url`] attribute that tells other crates where to //! find your documentation, gated behind the "html_root_url" feature. //! See [`assert_html_root_url_updated`]. //! -//! The macros are gated behind individual features, as detailed below. +//! Except for [`assert_contains_version`], the macros are gated +//! behind individual features, as detailed below. //! //! A typical configuration will use an integration test to verify //! that all version numbers are in sync. Create a @@ -40,11 +41,18 @@ //! version_sync::assert_html_root_url_updated!("src/lib.rs"); //! } //! +//! #[test] +//! # fn fake_hidden_test_case_3() {} +//! fn test_readme_mentions_version() { +//! version_sync::assert_contains_version!("README.md", "Version {version}"); +//! } +//! //! # fn main() { //! # #[cfg(feature = "markdown_deps_updated")] //! # test_readme_deps(); //! # #[cfg(feature = "html_root_url_updated")] //! # test_html_root_url(); +//! # test_readme_mentions_version(); //! # } //! ``` //! @@ -53,7 +61,8 @@ //! //! # Cargo Features //! -//! Each of the macros above are gated behind a feature: +//! In case you only need some of the macros above, you can disable +//! them individually using Cargo features. The features are: //! //! * `markdown_deps_updated` enables [`assert_markdown_deps_updated`]. //! * `html_root_url_updated` enables [`assert_html_root_url_updated`]. @@ -71,17 +80,14 @@ #![deny(missing_docs)] mod contains_regex; +mod contains_version; mod helpers; mod html_root_url; mod markdown_deps; -// Ensure that at least one feature is enabled -#[cfg(not(any(feature = "contains_regex", feature = "html_root_url_updated", - feature = "markdown_deps_updated")))] -std::compile_error!("Please select at least one feature."); - #[cfg(feature = "contains_regex")] pub use crate::contains_regex::check_contains_regex; +pub use crate::contains_version::check_contains_version; #[cfg(feature = "html_root_url_updated")] pub use crate::html_root_url::check_html_root_url; #[cfg(feature = "markdown_deps_updated")] @@ -192,12 +198,78 @@ macro_rules! assert_html_root_url_updated { }; } +/// Assert that versions numbers are up to date via pattern matching. +/// +/// 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 template which will be matched against the +/// content of the file. +/// +/// The macro calls [`check_contains_version`] 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 +/// #[test] +/// # fn fake_hidden_test_case() {} +/// # // The above function ensures test_readme_mentions_version is +/// # // compiled. +/// fn test_readme_mentions_version() { +/// version_sync::assert_contains_version!("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 pattern can contain placeholders which are replaced before the +/// 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 of {name} +/// is: {version}"` and make sure you update your READMEs and +/// changelogs consistently. +/// +/// See [`assert_contains_regex`] if you want to search for a regular +/// expression instead. +/// +/// # Panics +/// +/// If the pattern cannot be found, `panic!` will be invoked and your +/// integration test will fail. +/// +/// [`check_contains_version`]: fn.check_contains_version.html +#[macro_export] +macro_rules! assert_contains_version { + ($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_version($path, $format, pkg_name, pkg_version) { + panic!("{}", err); + } + }; +} + /// 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. +/// against the contents of the file. /// /// The macro calls [`check_contains_regex`] on the file name given. /// The package name and current package version is automatically