radicle-git init

Port over all git relevant parts of the radicle-link repository[0].
The packages that are kept are:
* git-ext
* git-ref-format
* git-trailers
* link-git
* macros
* std-ext
* test -- only the portions that test the above

The development and testing scripts are also added for local and CI
testing.

[0]: https://github.com/radicle-dev/radicle-link/
Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
Fintan Halpenny
2022-09-08 12:07:51 +01:00
parent c43420956c
commit 70b571b67b
112 changed files with 10341 additions and 7 deletions
+1
View File
@@ -0,0 +1 @@
proptest-regressions
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "tests"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
publish = false
[lib]
doctest = false
test = true
doc = false
[dev-dependencies.git-ext-test]
path = "../git-ext/t"
features = ["test"]
[dev-dependencies.git-ref-format-test]
path = "../git-ref-format/t"
features = ["test"]
[dev-dependencies.git-trailers-test]
path = "../git-trailers/t"
features = ["test"]
# [dev-dependencies.gitd-lib-test]
# path = "../cli/gitd-lib/t"
[dev-dependencies.link-git-test]
path = "../link-git/t"
features = ["test"]
+52
View File
@@ -0,0 +1,52 @@
# Test root crate
Organisation of the test code for the `radicle-git` project crates deviates
from the `cargo` conventions in order to work around some of the limitations of
the current `cargo` / Rust testing infrastructure.
Here is how:
- Project crates are set to `test = false` by default, ie. no `#[cfg(test)]` /
`#[test]` annotated tests are run.
- Instead, project crates are tested via an accompanying `<crate>-test` crate
located in a `t/` directory relative to the crate root.
- This is similar to what `cargo` calls "integration tests", in that only the
public API of the crate under test is available. Test crates are, however,
meant to contain all kinds of tests.
- Conventionally, tests are split into module hierarchies, mainly to support
convenient filtering.
`tests`
: Unit tests. Example-based, preferably-pure.
`properties`
: Property tests. Randomized, preferably-pure.
`integration`
: Stateful tests, scenario-based. May have all kinds of effects.
- Additionally, test crates may export helpers (such as mocks or fixtures) and
`proptest` generators through `gen` and `helpers` modules. Test crates may
depend on each other to make those types / functions available, possibly
mirroring the dependency relationships of their respective "parent" crates.
- `gen` and `helpers` modules are guarded behind a feature flag "test", ie.
#[cfg(any(test, feature = "test"))]
- Additional helpers can be found in the `test-helpers` (preferably-pure) and
`it-helpers` (stateful) crates.
- This crate (`test`) does not contain any code, but depends on all other test
crates in the workspace (which are themselves not proper workspace members).
This prevents unnecessary compilation of test crates if no test target is
being built, but still makes each test crate available to be executed
individually via the `-p` flag, eg.
cargo test -p link-replication-test
- It is recommended to use [`cargo-nextest`](https://nexte.st) instead of `cargo
test` for maximising parallelism.
+48
View File
@@ -0,0 +1,48 @@
[package]
name = "it-helpers"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
publish = false
description = "Integration test helpers"
[lib]
doctest = false
test = false
[dependencies]
anyhow = "1"
futures = "0.3"
once_cell = "1.10"
tempfile = "3.3"
tokio = "1.13"
tracing = "0.1"
either = "1.6"
[dependencies.git2]
version = "0.13.24"
default-features = false
features = ["vendored-libgit2"]
#
# workspace dependencies
#
[dependencies.git-ref-format]
path = "../../git-ref-format"
[dependencies.librad]
path = "../../librad"
[dependencies.link-async]
path = "../../link-async"
[dependencies.lnk-clib]
path = "../../cli/lnk-clib"
[dependencies.radicle-git-ext]
path = "../../git-ext"
[dependencies.test-helpers]
path = "../test-helpers"
+27
View File
@@ -0,0 +1,27 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use git_ref_format::Qualified;
#[tracing::instrument(skip(repo))]
pub fn create_commit(
repo: &git2::Repository,
on_branch: Qualified,
) -> Result<git2::Oid, git2::Error> {
let empty_tree = {
let mut index = repo.index()?;
let oid = index.write_tree()?;
repo.find_tree(oid).unwrap()
};
let author = git2::Signature::now("The Animal", "animal@muppets.com").unwrap();
repo.commit(
Some(on_branch.as_str()),
&author,
&author,
"Initial commit",
&empty_tree,
&[],
)
}
+10
View File
@@ -0,0 +1,10 @@
#[macro_use]
extern crate tracing;
pub mod fixed;
pub mod git;
pub mod layout;
pub mod ssh;
pub mod testnet;
pub mod tmp;
pub mod working_copy;
+68
View File
@@ -0,0 +1,68 @@
// Copyright © 2021 The Radicle Link Contributors
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use std::process::{Command, Stdio};
use lnk_clib::keys::ssh::SshAuthSock;
use test_helpers::tempdir::WithTmpDir;
pub type TmpSshSock = WithTmpDir<SshAuthSock>;
pub fn ssh_auth_sock() -> TmpSshSock {
WithTmpDir::new(|path| -> anyhow::Result<SshAuthSock> {
let sock = path.join("tmp.sock");
Ok(SshAuthSock::Uds(sock))
})
.unwrap()
}
/// Run a computation with a forked `ssh-agent` on a temporary file handle.
///
/// Once the computation is finished, the `ssh-agent` is killed by getting its
/// PID and running the equivalent of `SSH_AGENT_PID=<pid> ssh-agent -k`.
/// This is a best effort of resource cleanup, but has no guarantees if the
/// parsing of the PID or the killin of the agent fail.
pub fn with_ssh_agent<F, T>(callback: F) -> anyhow::Result<T>
where
F: FnOnce(SshAuthSock) -> anyhow::Result<T>,
{
let sock = ssh_auth_sock();
let path = match &*sock {
SshAuthSock::Uds(path) => path,
_ => unreachable!(),
};
let agent = Command::new("ssh-agent").arg("-a").arg(path).output()?;
anyhow::ensure!(agent.status.success(), agent.status);
let pid = agent_pid(&agent.stdout)?;
let res = callback((*sock).clone());
kill_agent_pid(pid)?;
res
}
/// Kill the ssh-agent running on the given PID.
fn kill_agent_pid(pid: &str) -> anyhow::Result<()> {
debug!(pid = %pid, "killing ssh-agent");
let status = Command::new("ssh-agent")
.env("SSH_AGENT_PID", pid)
.args(["-k"])
.stdout(Stdio::null())
.status()?;
debug!(status = %status, "status of killing agent");
Ok(())
}
/// Get the PID of the launched ssh-agent.
///
/// It gets the PID by stripping the output from the command using the text
/// `"echo Agent pid "`.
fn agent_pid(out: &[u8]) -> anyhow::Result<&str> {
const PREFIX: &str = "SSH_AGENT_PID=";
const SEP: u8 = b';';
let pid = out
.split(|b| b == &SEP)
.find_map(|bs| std::str::from_utf8(bs).ok()?.trim().strip_prefix(PREFIX))
.ok_or_else(|| anyhow::anyhow!("could not find SSH_AGENT_PID"))?;
Ok(pid)
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright © 2021 The Radicle Link Contributors
// SPDX-License-Identifier: GPLv3-or-later
use std::io;
use librad::{git::storage::Storage, paths::Paths, SecretKey};
use test_helpers::tempdir::WithTmpDir;
pub type TmpPaths = WithTmpDir<Paths>;
pub fn paths() -> TmpPaths {
WithTmpDir::new(|path| -> Result<_, io::Error> {
let paths = Paths::from_root(path)?;
Ok::<_, io::Error>(paths)
})
.unwrap()
}
type TmpRepo = WithTmpDir<git2::Repository>;
pub fn repo() -> anyhow::Result<TmpRepo> {
Ok(WithTmpDir::new(|path| {
let setup = || {
let repo = git2::Repository::init(path)?;
// We need to set user info to _something_, but that doesn't have to
// be valid, as we're using a shared repo with many keys
let mut config = repo.config()?;
config.set_str("user.name", "shared")?;
config.set_str("user.email", "not.relevant@for.testing")?;
Ok(repo)
};
setup().map_err(|e: git2::Error| io::Error::new(io::ErrorKind::Other, e))
})?)
}
pub type TmpStorage = WithTmpDir<Storage>;
pub fn storage(signer: SecretKey) -> TmpStorage {
WithTmpDir::new(|path| -> Result<_, io::Error> {
let paths = Paths::from_root(path)?;
let storage =
Storage::open(&paths, signer).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok::<_, io::Error>(storage)
})
.unwrap()
}
+301
View File
@@ -0,0 +1,301 @@
use git_ref_format::{lit, name, refspec, Qualified, RefStr, RefString};
use librad::{
git::{
local::url::LocalUrl,
types::{
remote::{LocalFetchspec, LocalPushspec},
Fetchspec,
Force,
Refspec,
Remote,
},
},
git_ext as ext,
net::{peer::Peer, protocol::RequestPullGuard},
refspec_pattern,
PeerId,
Signer,
};
use crate::fixed::TestProject;
/// A remote in the working copy
pub enum WorkingRemote {
/// A remote representing a remote peer, named `PeerId::encode_id`
Peer(PeerId),
/// A remote representing the local peer, named "rad"
Rad,
}
impl From<PeerId> for WorkingRemote {
fn from(p: PeerId) -> Self {
WorkingRemote::Peer(p)
}
}
impl WorkingRemote {
fn fetchspec(&self) -> Fetchspec {
match self {
Self::Peer(peer_id) => {
let name = RefString::try_from(format!("{}", peer_id)).expect("peer is refstring");
let dst = RefString::from(Qualified::from(lit::refs_remotes(name.clone())))
.with_pattern(refspec::STAR);
let src = RefString::from(Qualified::from(lit::refs_remotes(name)))
.and(name::HEADS)
.with_pattern(refspec::STAR);
let refspec = Refspec {
src,
dst,
force: Force::True,
};
refspec.into_fetchspec()
},
Self::Rad => {
let name = RefString::try_from("rad").unwrap();
let src =
RefString::from_iter([name::REFS, name::HEADS]).with_pattern(refspec::STAR);
Refspec {
src,
dst: RefString::from(Qualified::from(lit::refs_remotes(name)))
.with_pattern(refspec::STAR),
force: Force::True,
}
.into_fetchspec()
},
}
}
fn remote_ref(&self, branch: &RefStr) -> RefString {
let name = match self {
Self::Rad => name::RAD.to_owned(),
Self::Peer(peer_id) => {
RefString::try_from(peer_id.to_string()).expect("peer id is refstring")
},
};
RefString::from(Qualified::from(lit::refs_remotes(name))).join(branch)
}
}
/// A `WorkingCopy` for test driving interactions with the monorepo where one
/// needs to update the tree of a project.
///
/// Remotes are named after the peer ID, except in the case of the remote
/// representing the local Peer ID - which is called "rad".
pub struct WorkingCopy<'a, S, G> {
repo: git2::Repository,
_repo_path: tempfile::TempDir,
peer: &'a Peer<S, G>,
project: &'a TestProject,
}
impl<'a, S, G> WorkingCopy<'a, S, G>
where
S: Signer + Clone,
G: RequestPullGuard,
{
/// Create a new working copy. This initializes a git repository and then
/// fetches the state of the local peer into `refs/remotes/rad/*`.
pub fn new(
project: &'a TestProject,
peer: &'a Peer<S, G>,
) -> Result<WorkingCopy<'a, S, G>, anyhow::Error> {
let repo_path = tempfile::tempdir()?;
let repo = git2::Repository::init(repo_path.as_ref())?;
let mut copy = WorkingCopy {
peer,
project,
repo,
_repo_path: repo_path,
};
copy.fetch(WorkingRemote::Rad)?;
Ok(copy)
}
/// Fetch changes from the monorepo into the working copy. The fetchspec
/// used depends on the peer ID.
///
/// * If `from` is `WorkingRemote::Peer` then `refs/remotes/<peer
/// ID>/refs/*:refs/remotes/<peer ID>/heads/*`
/// * If `from` is `WorkingRemote::Rad` then
/// `refs/heads/*:refs/remotes/rad/*`
///
/// I.e. changes from remote peers end up in a remote called
/// `PeerId::encode_id` whilst changes from the local peer end up in a
/// remote called "rad".
pub fn fetch(&mut self, from: WorkingRemote) -> Result<(), anyhow::Error> {
let fetchspec = from.fetchspec();
let url = LocalUrl::from(self.project.project.urn());
let mut remote = Remote::rad_remote(url, fetchspec);
let _ = remote.fetch(self.peer.clone(), &self.repo, LocalFetchspec::Configured)?;
Ok(())
}
/// Push changes from `refs/heads/*` to the local peer
pub fn push(&mut self) -> Result<(), anyhow::Error> {
let url = LocalUrl::from(self.project.project.urn());
let name = RefString::try_from("rad").unwrap();
let fetchspec = Refspec {
src: RefString::from_iter([name::REFS, name::HEADS]).with_pattern(refspec::STAR),
dst: RefString::from(Qualified::from(lit::refs_remotes(name)))
.with_pattern(refspec::STAR),
force: Force::True,
}
.into_fetchspec();
let mut remote = Remote::rad_remote(url, fetchspec);
let _ = remote.push(
self.peer.clone(),
&self.repo,
LocalPushspec::Matching {
pattern: refspec_pattern!("refs/heads/*"),
force: Force::True,
},
)?;
Ok(())
}
/// Create a new commit on top of whichever commit is the head of
/// `on_branch`. If the branch does not exist this will create it.
pub fn commit(
&mut self,
message: &str,
on_branch: Qualified,
) -> Result<git2::Oid, anyhow::Error> {
let branch_name = on_branch.non_empty_components().2;
let parent = match self.repo.find_branch(&branch_name, git2::BranchType::Local) {
Ok(b) => b.get().target().and_then(|o| self.repo.find_commit(o).ok()),
Err(e) if ext::error::is_not_found_err(&e) => None,
Err(e) => return Err(anyhow::Error::from(e)),
};
let empty_tree = {
let mut index = self.repo.index()?;
let oid = index.write_tree()?;
self.repo.find_tree(oid).unwrap()
};
let author = git2::Signature::now("The Animal", "animal@muppets.com").unwrap();
let parents = match &parent {
Some(p) => vec![p],
None => Vec::new(),
};
self.repo
.commit(
Some(&on_branch),
&author,
&author,
message,
&empty_tree,
&parents,
)
.map_err(anyhow::Error::from)
}
pub fn commit_and_push(
&mut self,
message: &str,
on_branch: Qualified,
) -> Result<git2::Oid, anyhow::Error> {
let id = self.commit(message, on_branch)?;
self.push()?;
Ok(id)
}
/// Create a branch at `refs/heads/<branch>` which tracks the given remote.
/// The remote branch name depends on `from`.
///
/// * If `from` is `WorkingCopy::Rad` then `refs/remotes/rad/<branch>`
/// * If `from` is `WorkingCopy::Peer(peer_id)` then `refs/remotes/<peer
/// id>/<branch>`
pub fn create_remote_tracking_branch(
&self,
from: WorkingRemote,
branch: &RefStr,
) -> Result<(), anyhow::Error> {
let target = self
.repo
.find_reference(from.remote_ref(branch).as_str())?
.target()
.ok_or_else(|| anyhow::anyhow!("remote ref is not a direct reference"))?;
let commit = self.repo.find_commit(target)?;
self.repo.branch(branch.as_str(), &commit, false)?;
Ok(())
}
/// Fast forward the local branch `refs/heads/<branch>` to whatever is
/// pointed to by `refs/remotes/<remote>/<branch>`
///
/// * If `from` is `WorkingRemote::Peer(peer_id)` then `remote` is
/// `peer_id.encode_id()`
/// * If `from` is `WorkingRemote::Rad` then `remote` is `"rad"`
///
/// # Errors
///
/// * If the local branch does not exist
/// * If the remote branch does not exist
/// * If either of the branches does not point at a commit
/// * If the remote branch is not a descendant of the local branch
pub fn fast_forward_to(&self, from: WorkingRemote, branch: &RefStr) -> anyhow::Result<()> {
let remote_ref = from.remote_ref(branch);
let remote_target = self
.repo
.find_reference(&remote_ref)?
.target()
.ok_or_else(|| anyhow::anyhow!("remote ref had no target"))?;
let local_ref = RefString::from(Qualified::from(lit::refs_heads(branch)));
let local_target = self
.repo
.find_reference(&local_ref)?
.target()
.ok_or_else(|| anyhow::anyhow!("local ref had no target"))?;
if !self.repo.graph_descendant_of(remote_target, local_target)? {
anyhow::bail!("remote ref was not a descendant of local ref");
} else {
self.repo
.reference(&local_ref, remote_target, true, "fast forward")?;
}
Ok(())
}
/// Create a new commit which merges `refs/heads/<branch>` and
/// `refs/remotes/<remote>/<branch>`
///
/// this will create a new commit with two parents, one for the remote
/// branch and one for the local branch
///
/// # Errors
///
/// * If the remote branch does not exist
/// * If the local branch does not exist
/// * If either of the references does not point to a commit
pub fn merge_remote(&self, remote: PeerId, branch: &RefStr) -> anyhow::Result<git2::Oid> {
let peer_branch = WorkingRemote::Peer(remote).remote_ref(branch);
let peer_commit = self
.repo
.find_reference(peer_branch.as_ref())?
.peel_to_commit()?;
let local_branch = Qualified::from(lit::refs_heads(branch));
let local_commit = self
.repo
.find_reference(local_branch.as_ref())?
.peel_to_commit()?;
let message = format!("merge {} into {}", peer_branch, local_branch);
let empty_tree = {
let mut index = self.repo.index()?;
let oid = index.write_tree()?;
self.repo.find_tree(oid).unwrap()
};
let author = git2::Signature::now("The Animal", "animal@muppets.com").unwrap();
let parents = vec![&peer_commit, &local_commit];
self.repo
.commit(
Some(&local_branch),
&author,
&author,
&message,
&empty_tree,
&parents,
)
.map_err(anyhow::Error::from)
}
}
+4
View File
@@ -0,0 +1,4 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
//! This page intentionally left blank.
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "test-helpers"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
publish = false
description = "Test helpers"
[lib]
doctest = false
test = false
[dependencies]
env_logger = ">= 0.9"
log = ">= 0.4"
pretty_assertions = "1.1"
serde = "1"
serde_json = "1"
tempfile = "3.3"
tracing = "0.1"
proptest = "1"
# [dependencies.link-canonical]
# path = "../../link-canonical"
[dependencies.minicbor]
version = "0.13"
features = ["std"]
[dependencies.tracing-subscriber]
version = "0.3.7"
features = ["std", "env-filter", "fmt", "json"]
+4
View File
@@ -0,0 +1,4 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPLv3-or-later
pub mod std_net;
+47
View File
@@ -0,0 +1,47 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPLv3-or-later
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
use proptest::prelude::*;
prop_compose! {
pub fn gen_ipaddr_v4()
(a in any::<u8>(),
b in any::<u8>(),
c in any::<u8>(),
d in any::<u8>()) -> Ipv4Addr{
Ipv4Addr::new(a, b, c, d)
}
}
prop_compose! {
pub fn gen_ipaddr_v6()
(a in any::<u16>(),
b in any::<u16>(),
c in any::<u16>(),
d in any::<u16>(),
e in any::<u16>(),
f in any::<u16>(),
g in any::<u16>(),
h in any::<u16>()) -> Ipv6Addr
{
Ipv6Addr::new(a, b, c, d, e, f, g, h)
}
}
pub fn gen_socket_v4() -> impl Strategy<Value = SocketAddr> {
any::<u16>().prop_flat_map(move |port| {
gen_ipaddr_v4().prop_map(move |v4| SocketAddr::V4(SocketAddrV4::new(v4, port)))
})
}
pub fn gen_socket_v6() -> impl Strategy<Value = SocketAddr> {
any::<u16>().prop_flat_map(move |port| {
gen_ipaddr_v6().prop_map(move |v6| SocketAddr::V6(SocketAddrV6::new(v6, port, 0, 0)))
})
}
pub fn gen_socket_addr() -> impl Strategy<Value = SocketAddr> {
prop_oneof![gen_socket_v4(), gen_socket_v6()]
}
+7
View File
@@ -0,0 +1,7 @@
// Copyright © 2022 The Radicle Link Contributors
// SPDX-License-Identifier: GPLv3-or-later
pub mod gen;
pub mod logging;
pub mod roundtrip;
pub mod tempdir;
+56
View File
@@ -0,0 +1,56 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use std::env;
use log::{log_enabled, Level};
use tracing::subscriber::set_global_default as set_subscriber;
use tracing_subscriber::{EnvFilter, FmtSubscriber};
/// Initialise logging / tracing
///
/// Note that this will capture logs, so they can be output as part of the test
/// output. Use `RUST_LOG` with care, as this may create unwanted memory
/// pressure. Note, however, that if `RUST_LOG` is not set, we set the level to
/// `error` by default in order to surface errors on CI.
///
/// The `TRACING_FMT` environment variable can be used to control the log
/// formatting. Supported values:
///
/// * "pretty": [`tracing_subscriber::fmt::format::Pretty`]
/// * "compact": [`tracing_subscriber::fmt::format::Compact`]
/// * "json": [`tracing_subscriber::fmt::format::Json`]
///
/// If the variable is not set, or set to any other value, the
/// [`tracing_subscriber::fmt::format::Full`] format is used.
pub fn init() {
if env_logger::builder().is_test(true).try_init().is_ok() {
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "debug");
}
let mut builder = FmtSubscriber::builder()
.with_env_filter(EnvFilter::from_default_env())
.with_test_writer();
if log_enabled!(target: "librad", Level::Trace) {
builder = builder.with_thread_ids(true);
} else if env::var("TRACING_FMT").is_err() {
let default_format = if env::var("CI").is_ok() {
"compact"
} else {
"pretty"
};
env::set_var("TRACING_FMT", default_format);
}
match env::var("TRACING_FMT").ok().as_deref() {
Some("pretty") => set_subscriber(builder.pretty().finish()),
Some("compact") => set_subscriber(builder.compact().finish()),
Some("json") => set_subscriber(builder.json().flatten_event(true).finish()),
_ => set_subscriber(builder.finish()),
}
.expect("setting tracing subscriber failed")
}
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use std::{
fmt::{Debug, Display},
str::FromStr,
};
use pretty_assertions::assert_eq;
pub fn json<A>(a: A)
where
for<'de> A: Debug + PartialEq + serde::Serialize + serde::Deserialize<'de>,
{
assert_eq!(
a,
serde_json::from_str(&serde_json::to_string(&a).unwrap()).unwrap()
)
}
pub fn cbor<A>(a: A)
where
for<'de> A: Debug + PartialEq + minicbor::Encode + minicbor::Decode<'de>,
{
assert_eq!(a, minicbor::decode(&minicbor::to_vec(&a).unwrap()).unwrap())
}
pub fn str<A>(a: A)
where
A: Debug + PartialEq + Display + FromStr,
<A as FromStr>::Err: Debug,
{
assert_eq!(a, a.to_string().parse().unwrap())
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
use std::{
io,
ops::{Deref, DerefMut},
path::Path,
};
use tempfile::{tempdir, TempDir};
pub struct WithTmpDir<A> {
_tmp: TempDir,
inner: A,
}
impl<A> WithTmpDir<A> {
pub fn new<F, E>(mk_inner: F) -> Result<Self, E>
where
F: FnOnce(&Path) -> Result<A, E>,
E: From<io::Error>,
{
let tmp = tempdir()?;
let inner = mk_inner(tmp.path())?;
Ok(Self { _tmp: tmp, inner })
}
}
impl<A> Deref for WithTmpDir<A> {
type Target = A;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<A> DerefMut for WithTmpDir<A> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}