git-testament-0.2.5/.cargo_vcs_info.json0000644000000001360000000000100136250ustar { "git": { "sha1": "048af8425122bc0b5ec78c01bfa75fc14afd5d34" }, "path_in_vcs": "" }git-testament-0.2.5/Cargo.toml0000644000000021760000000000100116310ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "git-testament" version = "0.2.5" authors = ["Daniel Silverstone "] include = [ "src", "tests", "test-template", ] description = "Record git working tree status when compiling your crate" documentation = "https://docs.rs/git-testament/" readme = "README.md" license = "BSD-3-Clause" repository = "https://github.com/kinnison/git-testament/" [dependencies.git-testament-derive] version = "0.2.0" [dev-dependencies.lazy_static] version = "1" [dev-dependencies.rand] version = "0.8" [dev-dependencies.regex] version = "1" [dev-dependencies.tempfile] version = "3" [features] alloc = [] default = ["alloc"] git-testament-0.2.5/Cargo.toml.orig000064400000000000000000000012261046102023000153050ustar 00000000000000[package] name = "git-testament" version = "0.2.5" authors = ["Daniel Silverstone "] edition = "2021" description = "Record git working tree status when compiling your crate" documentation = "https://docs.rs/git-testament/" repository = "https://github.com/kinnison/git-testament/" license = "BSD-3-Clause" readme = "README.md" include = ["src", "tests", "test-template"] [workspace] members = ["git-testament-derive"] [dependencies] git-testament-derive = { version = "0.2.0", path = "git-testament-derive" } [dev-dependencies] tempfile = "3" rand = "0.8" regex = "1" lazy_static = "1" [features] alloc = [] default = ["alloc"] git-testament-0.2.5/README.md000064400000000000000000000042211046102023000136730ustar 00000000000000# Git Testament ![BSD 3 Clause](https://img.shields.io/github/license/kinnison/git-testament.svg) ![Main build status](https://github.com/kinnison/git-testament/workflows/main/badge.svg) ![Latest docs](https://docs.rs/git-testament/badge.svg) ![Crates.IO](https://img.shields.io/crates/v/git-testament.svg) `git-testament` is a library to embed a testament as to the state of a git working tree during the build of a Rust program. It uses the power of procedural macros to embed commit, tag, and working-tree-state information into your program when it is built. This can then be used to report version information. ```rust use git_testament::{git_testament, render_testament}; git_testament!(TESTAMENT); fn main() { println!("My version information: {}", render_testament!(TESTAMENT)); } ``` ## Reproducible builds In the case that your build is not being done from a Git repository, you still want your testament to be useful to your users. Reproducibility of the binary is critical in that case. The [Reproducible Builds][reprobuild] team have defined a mechanism for this known as [`SOURCE_DATE_EPOCH`][sde] which is an environment variable which can be set to ensure the build date is fixed for reproducibilty reasons. If you have no repo (or a repo but no commit) then `git_testament!()` will use the [`SOURCE_DATE_EPOCH`][sde] environment variable (if present and parseable as a number of seconds since the UNIX epoch) to override `now`. [reprobuild]: https://reproducible-builds.org [sde]: https://reproducible-builds.org/docs/source-date-epoch/ ## Use in `no_std` scenarios This crate does not link to anything in the standard library, but it does rely by default on the `alloc` library being available. Disabling the `alloc` feature allows the crate to work in `no_std` environments where the `alloc` library is not available. You can still generate a `GitTestament` struct though it'll be less easy to work with. Instead it'd be recommended to use the `git_testament_macros!()` macro instead which provides a set of macros which produce string constants to use. This is less flexible/capable but can sometimes be easier to work with in these kinds of situations. git-testament-0.2.5/src/lib.rs000064400000000000000000000341111046102023000143200ustar 00000000000000//! # Generate a testament of the git working tree state for a build //! //! You likely want to see either the [git_testament] macro, or if you //! are in a no-std type situation, the [git_testament_macros] macro instead. //! //! [git_testament]: macro.git_testament.html //! [git_testament_macros]: macro.git_testament_macros.html //! //! If you build this library with the default `alloc` feature disabled then while //! the non-macro form of the testaments are offered, they cannot be rendered //! and the [render_testament] macro will not be provided. //! //! [render_testament]: macro.render_testament.html //! //! ## Trusted branches //! //! In both [render_testament] and [git_testament_macros] you will find mention //! of the concept of a "trusted" branch. This exists as a way to allow releases //! to be made from branches which are not yet tagged. For example, if your //! release process requires that the release binaries be built and tested //! before tagging the repository then by nominating a particular branch as //! trusted, you can cause the rendered testament to trust the crate's version //! rather than being quite noisy about how the crate version and the tag //! version do not match up. #![no_std] #[cfg(feature = "alloc")] extern crate alloc; #[doc(hidden)] pub extern crate core as __core; #[doc(hidden)] pub extern crate git_testament_derive as __derive; use core::fmt::{self, Display, Formatter}; // Clippy thinks our fn main() is needless, but it is needed because otherwise // we cannot have the invocation of the procedural macro (yet) #[allow(clippy::needless_doctest_main)] /// Generate a testament for the working tree. /// /// This macro declares a static data structure which represents a testament /// to the state of a git repository at the point that a crate was built. /// /// The intention is that the macro should be used at the top level of a binary /// crate to provide information about the state of the codebase that the output /// program was built from. This includes a number of things such as the commit /// SHA, any related tag, how many commits since the tag, the date of the commit, /// and if there are any "dirty" parts to the working tree such as modified files, /// uncommitted files, etc. /// /// ``` /// // Bring the procedural macro into scope /// use git_testament::git_testament; /// /// // Declare a testament, it'll end up as a static, so give it a capital /// // letters name or it'll result in a warning. /// git_testament!(TESTAMENT); /// # fn main() { /// /// // ... later, you can display the testament. /// println!("app version {TESTAMENT}"); /// # } /// ``` /// /// See [`GitTestament`] for the type of the defined `TESTAMENT`. #[macro_export] macro_rules! git_testament { ($name:ident) => { $crate::__derive::git_testament! { $crate $name } }; } // Clippy thinks our fn main() is needless, but it is needed because otherwise // we cannot have the invocation of the procedural macro (yet) #[allow(clippy::needless_doctest_main)] /// Generate a testament for the working tree as a set of static string macros. /// /// This macro declares a set of macros which provide you with your testament /// as static strings. /// /// The intention is that the macro should be used at the top level of a binary /// crate to provide information about the state of the codebase that the output /// program was built from. This includes a number of things such as the commit /// SHA, any related tag, how many commits since the tag, the date of the commit, /// and if there are any "dirty" parts to the working tree such as modified files, /// uncommitted files, etc. /// /// ``` /// // Bring the procedural macro into scope /// use git_testament::git_testament_macros; /// /// // Declare a testament, it'll end up as pile of macros, so you can /// // give it whatever ident-like name you want. The name will prefix the /// // macro names. Also you can optionally specify /// // a branch name which will be considered the "trusted" branch like in /// // `git_testament::render_testament!()` /// git_testament_macros!(version); /// # fn main() { /// /// // ... later, you can display the testament. /// println!("app version {}", version_testament!()); /// # } /// ``` /// /// The macros all resolve to string literals, boolean literals, or in the case /// of `NAME_tag_distance!()` a number. This is most valuable when you are /// wanting to include the information into a compile-time-constructed string /// /// ``` /// // Bring the procedural macro into scope /// use git_testament::git_testament_macros; /// /// // Declare a testament, it'll end up as pile of macros, so you can /// // give it whatever ident-like name you want. The name will prefix the /// // macro names. Also you can optionally specify /// // a branch name which will be considered the "trusted" branch like in /// // `git_testament::render_testament!()` /// git_testament_macros!(version, "stable"); /// /// const APP_VERSION: &str = concat!("app version ", version_testament!()); /// # fn main() { /// /// // ... later, you can display the testament. /// println!("{APP_VERSION}"); /// # } /// ``` /// /// The set of macros defined is: /// /// * `NAME_testament!()` -> produces a string similar but not guaranteed to be /// identical to the result of `Display` formatting a normal testament. /// * `NAME_branch!()` -> An Option<&str> of the current branch name /// * `NAME_repo_present!()` -> A boolean indicating if there is a repo at all /// * `NAME_commit_present!()` -> A boolean indicating if there is a commit present at all /// * `NAME_tag_present!()` -> A boolean indicating if there is a tag present /// * `NAME_commit_hash!()` -> A string of the commit hash (or crate version if commit not present) /// * `NAME_commit_date!()` -> A string of the commit date (or build date if no commit present) /// * `NAME_tag_name!()` -> The tag name if present (or crate version if commit not present) /// * `NAME_tag_distance!()` -> The number of commits since the tag if present (zero otherwise) #[macro_export] macro_rules! git_testament_macros { ($name:ident $(, $trusted:literal)?) => { $crate::__derive::git_testament_macros! { $crate $name $($trusted)? } }; } /// A modification to a working tree, recorded when the testament was created. #[derive(Debug)] pub enum GitModification<'a> { /// A file or directory was added but not committed Added(&'a [u8]), /// A file or directory was removed but not committed Removed(&'a [u8]), /// A file was modified in some way, either content or permissions Modified(&'a [u8]), /// A file or directory was present but untracked Untracked(&'a [u8]), } /// The kind of commit available at the point that the testament was created. #[derive(Debug)] pub enum CommitKind<'a> { /// No repository was present. Instead the crate's version and the /// build date are recorded. NoRepository(&'a str, &'a str), /// No commit was present, though it was a repository. Instead the crate's /// version and the build date are recorded. NoCommit(&'a str, &'a str), /// There are no tags in the repository in the history of the commit. /// The commit hash and commit date are recorded. NoTags(&'a str, &'a str), /// There were tags in the history of the commit. /// The tag name, commit hash, commit date, and distance from the tag to /// the commit are recorded. FromTag(&'a str, &'a str, &'a str, usize), } /// A testament to the state of a git repository when a crate is built. /// /// This is the type returned by the [`git_testament_derive::git_testament`] /// macro when used to record the state of a git tree when a crate is built. /// /// The structure contains information about the commit from which the crate /// was built, along with information about any modifications to the working /// tree which could be considered "dirty" as a result. /// /// By default, the `Display` implementation for this structure attempts to /// produce something pleasant but useful to humans. For example it might /// produce a string along the lines of `"1.0.0 (763aa159d 2019-04-02)"` for /// a clean build from a 1.0.0 tag. Alternatively if the working tree is dirty /// and there have been some commits since the last tag, you might get something /// more like `"1.0.0+14 (651af89ed 2019-04-02) dirty 4 modifications"` /// /// If your program wishes to go into more detail, then the `commit` and the /// `modifications` members are available for rendering as the program author /// sees fit. /// /// In general this is only of use for binaries, since libraries will generally /// be built from `crates.io` provided tarballs and as such won't carry the /// information needed. In such a fallback position the string will be something /// along the lines of `"x.y (somedate)"` where `x.y` is the crate's version and /// `somedate` is the date of the build. You'll get similar information if the /// crate is built in a git repository on a branch with no commits yet (e.g. /// when you first have run `cargo init`) though that will include the string /// `uncommitted` to indicate that once commits are made the information will be /// of more use. #[derive(Debug)] pub struct GitTestament<'a> { pub commit: CommitKind<'a>, pub modifications: &'a [GitModification<'a>], pub branch_name: Option<&'a str>, } /// An empty testament. /// /// This is used by the derive macro to fill in defaults /// in the case that an older derive macro is used with a newer version /// of git_testament. /// /// Typically this will not be used directly by a user. pub const EMPTY_TESTAMENT: GitTestament = GitTestament { commit: CommitKind::NoRepository("unknown", "unknown"), modifications: &[], branch_name: None, }; #[cfg(feature = "alloc")] impl<'a> GitTestament<'a> { #[doc(hidden)] pub fn _render_with_version( &self, pkg_version: &str, trusted_branch: Option<&'static str>, ) -> alloc::string::String { match self.commit { CommitKind::FromTag(tag, hash, date, _) => { let trusted = match trusted_branch { Some(_) => { if self.branch_name == trusted_branch { self.modifications.is_empty() } else { false } } None => false, }; if trusted { // We trust our branch, so construct an equivalent // testament to render alloc::format!( "{}", GitTestament { commit: CommitKind::FromTag(pkg_version, hash, date, 0), ..*self } ) } else if tag.contains(pkg_version) { alloc::format!("{self}") } else { alloc::format!("{pkg_version} :: {self}") } } _ => alloc::format!("{self}"), } } } /// Render a testament /// /// This macro can be used to render a testament created with the `git_testament` /// macro. It renders a testament with the added benefit of indicating if the /// tag does not match the version (by substring) then the crate's version and /// the tag will be displayed in the form: "crate-ver :: testament..." /// /// For situations where the crate version MUST override the tag, for example /// if you have a release process where you do not make the tag unless the CI /// constructing the release artifacts passes, then you can pass a second /// argument to this macro stating a branch name to trust. If the working /// tree is clean and the branch name matches then the testament is rendered /// as though the tag had been pushed at the built commit. Since this overrides /// a fundamental part of the behaviour of `git_testament` it is recommended that /// this *ONLY* be used if you have a trusted CI release branch process. /// /// ``` /// use git_testament::{git_testament, render_testament}; /// /// git_testament!(TESTAMENT); /// /// # fn main() { /// println!("The testament is: {}", render_testament!(TESTAMENT)); /// println!("The fiddled testament is: {}", render_testament!(TESTAMENT, "trusted-branch")); /// # } #[cfg(feature = "alloc")] #[macro_export] macro_rules! render_testament { ( $testament:expr ) => { $crate::GitTestament::_render_with_version( &$testament, $crate::__core::env!("CARGO_PKG_VERSION"), $crate::__core::option::Option::None, ) }; ( $testament:expr, $trusted_branch:expr ) => { $crate::GitTestament::_render_with_version( &$testament, $crate::__core::env!("CARGO_PKG_VERSION"), $crate::__core::option::Option::Some($trusted_branch), ) }; } impl<'a> Display for CommitKind<'a> { fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { match self { CommitKind::NoRepository(crate_ver, build_date) => { write!(fmt, "{crate_ver} ({build_date})") } CommitKind::NoCommit(crate_ver, build_date) => { write!(fmt, "{crate_ver} (uncommitted {build_date})") } CommitKind::NoTags(commit, when) => { write!(fmt, "unknown ({} {})", &commit[..9], when) } CommitKind::FromTag(tag, commit, when, depth) => { if *depth > 0 { write!(fmt, "{}+{} ({} {})", tag, depth, &commit[..9], when) } else { write!(fmt, "{} ({} {})", tag, &commit[..9], when) } } } } } impl<'a> Display for GitTestament<'a> { fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { self.commit.fmt(fmt)?; if !self.modifications.is_empty() { write!( fmt, " dirty {} modification{}", self.modifications.len(), if self.modifications.len() > 1 { "s" } else { "" } )?; } Ok(()) } } git-testament-0.2.5/test-template/Cargo.toml.in000064400000000000000000000003271046102023000175440ustar 00000000000000[package] name = "test2" version = "1.0.0" authors = ["Daniel Silverstone "] edition = "2018" [workspace] [features] default = ["alloc"] alloc = ["git-testament/alloc"] [dependencies] git-testament-0.2.5/test-template/src/main.rs000064400000000000000000000010331046102023000172630ustar 00000000000000#[cfg(feature = "alloc")] use git_testament::{git_testament, render_testament}; #[cfg(feature = "alloc")] git_testament!(TESTAMENT); use git_testament::git_testament_macros; git_testament_macros!(version, "trusted"); #[cfg(feature = "alloc")] fn main() { assert_eq!( format!("{}", render_testament!(TESTAMENT, "trusted")), version_testament!() ); println!("{}", render_testament!(TESTAMENT, "trusted")); } #[cfg(not(feature = "alloc"))] fn main() { println!("{}", concat!("", version_testament!())); } git-testament-0.2.5/tests/const.rs000064400000000000000000000010641046102023000152540ustar 00000000000000use git_testament::{git_testament, git_testament_macros}; git_testament!(TESTAMENT); git_testament_macros!(TESTAMENT); const TESTAMENT_BRANCH_NAME_OR_DEFAULT: &str = { match TESTAMENT.branch_name { Some(branch_name) => branch_name, None => "main", } }; const MACROS_BRANCH_NAME_OR_DEFAULT: &str = { match TESTAMENT_branch!() { Some(branch_name) => branch_name, None => "main", } }; #[test] fn it_works() { assert_eq!( TESTAMENT_BRANCH_NAME_OR_DEFAULT, MACROS_BRANCH_NAME_OR_DEFAULT ); } git-testament-0.2.5/tests/no-prelude.rs000064400000000000000000000003631046102023000162010ustar 00000000000000#![no_implicit_prelude] use ::git_testament::{git_testament, git_testament_macros}; git_testament!(TESTAMENT); git_testament_macros!(TESTAMENT); #[test] fn it_works() { ::core::assert_eq!(TESTAMENT_branch!(), TESTAMENT.branch_name); } git-testament-0.2.5/tests/simple.rs000064400000000000000000000101521046102023000154150ustar 00000000000000use git_testament::{git_testament, git_testament_macros, render_testament}; git_testament!(TESTAMENT); git_testament_macros!(version); #[test] fn it_works() { println!("Testament: {TESTAMENT}"); } //testament macro is not guaranteed to be indentical to testament's Display in `no_std` #[cfg(feature = "alloc")] #[test] fn macros_work() { assert_eq!(render_testament!(TESTAMENT), version_testament!()); } mod testutils; #[test] fn verify_builds_ok() { let test = testutils::prep_test("no-git"); assert!(test.run_cmd("cargo", &["build"])); test.assert_manifest_contains("1.0.0"); } #[test] fn verify_no_commit() { let test = testutils::prep_test("no-commit"); assert!(test.basic_git_init()); assert!(test.run_cmd("cargo", &["build"])); test.assert_manifest_contains("uncommitted"); } #[test] fn verify_no_changes_no_tags() { let test = testutils::prep_test("no-changes"); assert!(test.basic_git_init()); assert!(test.run_cmd("cargo", &["check"])); assert!(test.run_cmd("git", &["add", "."])); assert!(test.run_cmd("git", &["commit", "-m", "first"])); assert!(test.run_cmd("cargo", &["build"])); test.assert_manifest_parts("unknown", 0, "TODO", None); } #[test] fn verify_no_changes_with_a_tag() { let test = testutils::prep_test("no-changes-with-tag"); assert!(test.basic_git_init()); assert!(test.run_cmd("cargo", &["check"])); assert!(test.run_cmd("git", &["add", "."])); assert!(test.run_cmd("git", &["commit", "-m", "first"])); assert!(test.run_cmd("git", &["tag", "-m", "1.0.0", "1.0.0"])); assert!(test.run_cmd("cargo", &["build"])); test.assert_manifest_parts("1.0.0", 0, "TODO", None); } #[test] fn verify_dirty_changes_with_a_tag() { let test = testutils::prep_test("dirty-with-tag"); assert!(test.basic_git_init()); assert!(test.run_cmd("cargo", &["check"])); assert!(test.run_cmd("git", &["add", "."])); assert!(test.run_cmd("git", &["commit", "-m", "first"])); assert!(test.run_cmd("git", &["tag", "-m", "1.0.0", "1.0.0"])); test.dirty_code(); assert!(test.run_cmd("cargo", &["build"])); test.assert_manifest_parts("1.0.0", 0, "TODO", Some(1)); } #[test] fn verify_another_commit_with_a_tag() { let test = testutils::prep_test("tag-plus-commit"); assert!(test.basic_git_init()); assert!(test.run_cmd("cargo", &["check"])); assert!(test.run_cmd("git", &["add", "."])); assert!(test.run_cmd("git", &["commit", "-m", "first"])); assert!(test.run_cmd("git", &["tag", "-m", "1.0.0", "1.0.0"])); test.dirty_code(); assert!(test.run_cmd("git", &["add", "."])); assert!(test.run_cmd("git", &["commit", "-m", "second"])); assert!(test.run_cmd("cargo", &["build"])); test.assert_manifest_parts("1.0.0", 1, "TODO", None); } #[test] fn verify_trusted_branch() { let test = testutils::prep_test("trusted-branch"); assert!(test.basic_git_init()); assert!(test.run_cmd("cargo", &["check"])); assert!(test.run_cmd("git", &["add", "."])); assert!(test.run_cmd("git", &["commit", "-m", "first"])); assert!(test.run_cmd("git", &["tag", "-m", "1.0.0", "1.0.0"])); assert!(test.run_cmd("git", &["checkout", "-b", "aaaa"])); test.dirty_code(); assert!(test.run_cmd("git", &["add", "."])); assert!(test.run_cmd("git", &["commit", "-m", "second"])); assert!(test.run_cmd("git", &["checkout", "-b", "trusted"])); assert!(test.run_cmd("cargo", &["build"])); test.assert_manifest_parts("1.0.0", 0, "TODO", None); } #[test] fn verify_source_date_epoch_no_repo() { let mut test = testutils::prep_test("source-date-epoch-norepo"); test.setenv("SOURCE_DATE_EPOCH", "324086400"); assert!(test.run_cmd("cargo", &["build"])); test.assert_manifest_contains("1.0.0"); test.assert_manifest_contains("1980-04-09"); } #[test] fn verify_source_date_epoch_no_commit() { let mut test = testutils::prep_test("source-date-epoch-nocommit"); assert!(test.basic_git_init()); test.setenv("SOURCE_DATE_EPOCH", "324086400"); assert!(test.run_cmd("cargo", &["build"])); test.assert_manifest_contains("1.0.0"); test.assert_manifest_contains("1980-04-09"); } git-testament-0.2.5/tests/testutils.rs000064400000000000000000000220621046102023000161670ustar 00000000000000use lazy_static::lazy_static; use rand::{thread_rng, Rng}; use regex::Regex; use std::collections::HashMap; use std::env; use std::fs; use std::path::PathBuf; use std::process::{Command, Stdio}; use tempfile::Builder; use tempfile::TempDir; pub struct TestSentinel { dir: Option, env: HashMap, prog_name: String, } impl Drop for TestSentinel { fn drop(&mut self) { self.run_cmd("cargo", &["clean", "-p", &self.prog_name]); if env::var("DO_NOT_ERASE_TESTS").is_ok() { let _ = self.dir.take().unwrap().into_path(); } } } pub struct ManifestParts { tag: String, distance: usize, commit: String, #[allow(dead_code)] date: String, dirty: Option, } lazy_static! { static ref MANIFEST_RE: Regex = Regex::new( r"^([^ ]+) \(([0-9a-f]{9}) (\d{4}-\d\d-\d\d)\)(?: dirty (\d+) modifications?)?$" ) .unwrap(); static ref TAG_WITH_DISTANCE: Regex = Regex::new(r"^(.+)\+(\d+)$").unwrap(); } fn test_base_dir() -> PathBuf { let mut base = PathBuf::from(env!("CARGO_TARGET_TMPDIR")); base.push("tests"); base.push("git-testament"); std::fs::create_dir_all(&base).expect("Unable to create test base directory"); base } pub fn prep_test(name: &str) -> TestSentinel { let outdir = Builder::new() .prefix(&format!("test-{name}-")) .tempdir_in(test_base_dir()) .expect("Unable to create temporary directory for test"); let mut rng = thread_rng(); let mut name = (0..10) .map(|_| rng.sample(rand::distributions::Alphanumeric)) .map(|c| c as char) .collect::(); name.make_ascii_lowercase(); let name = format!("gtt-{name}"); // Copy the contents of the test template in fs::create_dir(outdir.path().join("src")).expect("Unable to make src/ dir"); fs::copy( concat!(env!("CARGO_MANIFEST_DIR"), "/test-template/src/main.rs"), outdir.path().join("src/main.rs"), ) .expect("Unable to copy main.rs in"); let toml = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/test-template/Cargo.toml.in" )); let toml = toml.replace("name = \"test2\"", &format!("name = \"{name}\"")); fs::write( outdir.path().join("Cargo.toml"), format!( "{}\ngit-testament = {{ path=\"{}\" }}\n", toml, env::var("CARGO_MANIFEST_DIR") .unwrap_or_else(|_| ".".to_owned()) .replace('\\', "\\\\") ), ) .expect("Unable to write Cargo.toml for test"); println!( "Wrote test Cargo.toml:\n{}", fs::read_to_string(outdir.path().join("Cargo.toml")) .expect("Cannot re-read Cargo.toml for test") ); fs::create_dir(outdir.path().join(".cargo")).expect("Unable to make .cargo/"); fs::write( outdir.path().join(".cargo/config"), format!( "[build]\ntarget-dir=\"{}/target\"", env::var("CARGO_MANIFEST_DIR") .unwrap_or_else(|_| "..".to_owned()) .replace('\\', "\\\\") ), ) .expect("Unable to write .cargo/config"); TestSentinel { dir: Some(outdir), prog_name: name, env: HashMap::new(), } } impl TestSentinel { pub fn setenv(&mut self, key: &str, value: &str) { self.env.insert(key.to_owned(), value.to_owned()); } pub fn run_cmd(&self, cmd: &str, args: &[&str]) -> bool { let mut child = Command::new(cmd); child.args(args).env( "GIT_CEILING_DIRECTORIES", self.dir.as_ref().unwrap().path().parent().unwrap(), ); for (key, value) in self.env.iter() { child.env(key, value); } let child = child .current_dir(self.dir.as_ref().unwrap().path()) .stdin(Stdio::null()) .output() .expect("Unable to run subcommand"); if !child.status.success() { println!("Failed to run {cmd} {args:?}"); println!("Status was: {:?}", child.status.code()); println!("Stdout was:\n{:?}", String::from_utf8(child.stdout)); println!("Stderr was:\n{:?}", String::from_utf8(child.stderr)); } child.status.success() } pub fn run_cmds(&self, cmds: &[(&str, &[&str])]) -> bool { cmds.iter().all(|(cmd, args)| self.run_cmd(cmd, args)) } pub fn basic_git_init(&self) -> bool { self.run_cmds(&[ ("git", &["init"]), ("git", &["config", "user.name", "Git Testament Test Suite"]), ( "git", &["config", "user.email", "git.testament@digital-scurf.org"], ), ("git", &["config", "commit.gpgsign", "false"]), ]) } pub fn get_output(&self, cmd: &str, args: &[&str]) -> Option { let res = Command::new(cmd) .env( "GIT_CEILING_DIRECTORIES", self.dir.as_ref().unwrap().path().parent().unwrap(), ) .current_dir(self.dir.as_ref().unwrap().path()) .args(args) .stdin(Stdio::null()) .output() .expect("Unable to run subcommand"); if res.status.success() { String::from_utf8(res.stdout).ok() } else { println!( "Attempt to get output of {} {:?} failed: {:?}", cmd, args, res.status.code() ); println!("Output: {:?}", String::from_utf8(res.stdout)); println!("Error: {:?}", String::from_utf8(res.stderr)); None } } pub fn get_manifest(&self) -> Option { self.get_output( &format!( "{}/target/debug/{}", env::var("CARGO_MANIFEST_DIR").expect("Unable to run without CARGO_MANIFEST_DIR"), self.prog_name ), &[], ) } pub fn get_manifest_parts(&self) -> ManifestParts { let output = self .get_manifest() .expect("Unable to retrieve full manifest support"); let first = output .lines() .next() .expect("Unable to retrieve manifest line"); let caps = MANIFEST_RE .captures(first) .unwrap_or_else(|| panic!("Unable to parse manifest line: '{first}'")); // Step one, process the tag bit let (tag, distance) = if let Some(tcaps) = TAG_WITH_DISTANCE.captures(caps.get(1).expect("No tag captures?").as_str()) { ( tcaps.get(1).expect("No tag capture?").as_str().to_owned(), tcaps .get(2) .expect("No distance capture?") .as_str() .parse::() .expect("Unable to parse distance"), ) } else { (caps.get(1).unwrap().as_str().to_owned(), 0usize) }; let dirty = caps.get(4).map(|dirtycap| { dirtycap .as_str() .parse::() .expect("Unable to parse dirty count") }); ManifestParts { tag, distance, commit: caps .get(2) .expect("Unable to extract commit") .as_str() .to_owned(), date: caps .get(3) .expect("Unable to extract date") .as_str() .to_owned(), dirty, } } #[allow(dead_code)] pub fn assert_manifest_exact(&self, manifest: &str) { let output = self .get_manifest() .expect("Unable to retrieve full manifest output"); let first = output .lines() .next() .expect("Unable to retrieve manifest line"); assert_eq!(first, manifest); } pub fn assert_manifest_parts( &self, tagname: &str, distance: usize, _date: &str, dirty: Option, ) { let manifest = self.get_manifest_parts(); let curcommit = self .get_output("git", &["rev-parse", "HEAD"]) .expect("Unable to get HEAD commit"); assert_eq!(manifest.tag, tagname); assert_eq!(manifest.distance, distance); assert_eq!(&curcommit[..manifest.commit.len()], manifest.commit); // TODO: Find some sensible way to assert the date assert_eq!(dirty, manifest.dirty); } pub fn assert_manifest_contains(&self, substr: &str) { let manifest = self.get_manifest().expect("Unable to retrieve manifest"); println!("Retrieved manifest: {manifest:?}"); println!("Does it contain: {substr:?}"); assert!(manifest.contains(substr)); } pub fn dirty_code(&self) { let main_rs = self.dir.as_ref().unwrap().path().join("src/main.rs"); let code = fs::read_to_string(&main_rs).expect("Unable to read code"); fs::write(main_rs, format!("{code}\n\n")).expect("Unable to write code"); } }