mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-18 19:10:31 +08:00
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:
@@ -0,0 +1 @@
|
||||
proptest-regressions
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
//! This page intentionally left blank.
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPLv3-or-later
|
||||
|
||||
pub mod std_net;
|
||||
@@ -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()]
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user