313 lines
10 KiB
Rust
313 lines
10 KiB
Rust
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
|
|
use radicle::cob::cache::COBS_DB_FILE;
|
|
use radicle::crypto::ssh::{Keystore, keystore::MemorySigner};
|
|
use radicle::crypto::{KeyPair, Seed};
|
|
use radicle::git;
|
|
use radicle::node;
|
|
use radicle::node::policy::store as policy;
|
|
use radicle::node::{Alias, Config, POLICIES_DB_FILE};
|
|
use radicle::profile;
|
|
use radicle::profile::Home;
|
|
use radicle::storage::git::transport;
|
|
use radicle::{Profile, Storage};
|
|
use radicle_localtime::LocalTime;
|
|
|
|
use radicle_node::test::node::{Node, NodeHandle};
|
|
|
|
use crate::util::formula::formula;
|
|
|
|
pub(crate) mod config {
|
|
use super::*;
|
|
use radicle::node::config::{Config, Limits, Network, RateLimit, RateLimits};
|
|
|
|
/// Configuration for a test seed node.
|
|
///
|
|
/// It sets the `RateLimit::capacity` to `usize::MAX` ensuring
|
|
/// that there are no rate limits for test nodes, since they all
|
|
/// operate on the same IP address. This prevents any announcement
|
|
/// messages from being dropped.
|
|
pub fn seed(alias: &'static str) -> Config {
|
|
Config {
|
|
network: Network::Test,
|
|
relay: node::config::Relay::Always,
|
|
limits: Limits {
|
|
rate: RateLimits {
|
|
inbound: RateLimit {
|
|
fill_rate: 1.0,
|
|
capacity: usize::MAX,
|
|
}
|
|
.into(),
|
|
outbound: RateLimit {
|
|
fill_rate: 1.0,
|
|
capacity: usize::MAX,
|
|
}
|
|
.into(),
|
|
},
|
|
..Limits::default()
|
|
},
|
|
external_addresses: vec![
|
|
node::Address::from_str(&format!("{alias}.radicle.example:8776")).unwrap(),
|
|
],
|
|
..node(alias)
|
|
}
|
|
}
|
|
|
|
/// Relay node config.
|
|
pub fn relay(alias: &'static str) -> Config {
|
|
Config {
|
|
relay: node::config::Relay::Always,
|
|
..node(alias)
|
|
}
|
|
}
|
|
|
|
/// Test node config.
|
|
pub fn node(alias: &'static str) -> Config {
|
|
Config::test(Alias::new(alias))
|
|
}
|
|
}
|
|
|
|
/// Test environment.
|
|
pub struct Environment {
|
|
tempdir: tempfile::TempDir,
|
|
users: usize,
|
|
}
|
|
|
|
impl Default for Environment {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Environment {
|
|
/// Create a new test environment.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
tempdir: tempfile::TempDir::new().unwrap(),
|
|
users: 0,
|
|
}
|
|
}
|
|
|
|
/// Return the path of the temporary directory at which
|
|
/// this testing environment is rooted.
|
|
pub fn tempdir(&self) -> PathBuf {
|
|
self.tempdir.path().into()
|
|
}
|
|
|
|
/// Return the home directory of the user with the given alias.
|
|
/// This is in analogy to a Unix home directory.
|
|
pub fn unix_home(&self, has_alias: &impl HasAlias) -> PathBuf {
|
|
self.tempdir().join(has_alias.alias().to_string())
|
|
}
|
|
|
|
/// Return the Radicle path of the user with the given alias.
|
|
/// This is in analogy to `$RAD_HOME` and always a subdirectory of
|
|
/// the user's home directory (see [`Environment::unix_home`]).
|
|
pub fn rad_home(&self, has_alias: &impl HasAlias) -> Home {
|
|
Home::new(self.unix_home(has_alias).join(".radicle")).unwrap()
|
|
}
|
|
|
|
/// Path to the working directory of the user with the given alias.
|
|
/// Tests that need to act on multiple repositories should crate
|
|
/// subdirecories within this directory.
|
|
pub fn work(&self, has_alias: &impl HasAlias) -> PathBuf {
|
|
self.unix_home(has_alias).join("work")
|
|
}
|
|
|
|
/// Create a new default configuration.
|
|
pub fn config(&self, alias: &str) -> profile::Config {
|
|
let alias = Alias::new(alias);
|
|
profile::Config {
|
|
node: node::Config::test(alias),
|
|
cli: radicle::cli::Config { hints: false },
|
|
public_explorer: radicle::explorer::Explorer::default(),
|
|
preferred_seeds: vec![],
|
|
web: radicle::web::Config::default(),
|
|
}
|
|
}
|
|
|
|
/// Create a new profile in this environment.
|
|
/// This should be used when a running node is not required.
|
|
/// Using this function is only necessary if the desired configuration
|
|
/// differs from the default provided by [`Environment::config`] as
|
|
/// for this default the convenience function [`Environment::profile`]
|
|
/// is provided.
|
|
pub fn profile_with(&mut self, config: profile::Config) -> Profile {
|
|
let alias = config.alias().clone();
|
|
let home = self.rad_home(&alias);
|
|
let keypair = KeyPair::from_seed(Seed::from([!(self.users as u8); 32]));
|
|
let policies_db = home.node().join(POLICIES_DB_FILE);
|
|
let cobs_db = home.cobs().join(COBS_DB_FILE);
|
|
|
|
config.write(&home.config()).unwrap();
|
|
|
|
let storage = Storage::open(
|
|
home.storage(),
|
|
radicle::git::UserInfo {
|
|
alias: alias.clone(),
|
|
key: keypair.pk.into(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
let mut db = home.cobs_db_mut().unwrap();
|
|
db.migrate(radicle::cob::migrate::ignore).unwrap();
|
|
|
|
policy::Store::open(policies_db).unwrap();
|
|
home.database_mut(node::db::config::Config::default())
|
|
.unwrap()
|
|
.init(
|
|
&keypair.pk.into(),
|
|
config.node.features(),
|
|
&alias,
|
|
&config.node.user_agent(),
|
|
LocalTime::now().into(),
|
|
config.node.external_addresses.iter(),
|
|
)
|
|
.unwrap();
|
|
|
|
radicle::cob::cache::Store::open(cobs_db).unwrap();
|
|
|
|
transport::local::register(storage.clone());
|
|
let keystore = Keystore::new(&home.keys());
|
|
keystore.store(keypair.clone(), "radicle", None).unwrap();
|
|
|
|
// Ensures that each user has a unique but deterministic public key.
|
|
self.users += 1;
|
|
|
|
Profile {
|
|
home,
|
|
storage,
|
|
keystore,
|
|
public_key: keypair.pk.into(),
|
|
config,
|
|
}
|
|
}
|
|
|
|
/// Create a new profile using a the default configuration from [`Environment::config`].
|
|
pub fn profile(&mut self, alias: &'static str) -> Profile {
|
|
self.profile_with(self.config(alias))
|
|
}
|
|
|
|
/// Create a new node in this environment. This should be used when a running node
|
|
/// is required. Use [`Environment::profile`] otherwise.
|
|
/// Using this function is only necessary when the node configuration differs
|
|
/// from the standard ones ([`config::node`], [`config::relay`], [`config::seed`]),
|
|
/// as for each of them a convenience function
|
|
/// (resp. [`Environment::node`], [`Environment::relay`], [`Environment::seed`]).
|
|
/// is provided to reduce boilerplate.
|
|
pub fn node_with(&mut self, node: Config) -> Node<MemorySigner> {
|
|
let alias = node.alias.clone();
|
|
let profile = self.profile_with(profile::Config {
|
|
node,
|
|
..self.config(alias.as_ref())
|
|
});
|
|
Node::new(profile)
|
|
}
|
|
|
|
/// Convenience method for creating a [`Node<MemorySigner>`]
|
|
/// using configuration [`config::node`] within this [`Environment`].
|
|
pub fn node(&mut self, alias: &'static str) -> Node<MemorySigner> {
|
|
self.node_with(config::node(alias))
|
|
}
|
|
|
|
/// Convenience method for creating a [`Node<MemorySigner>`]
|
|
/// using configuration [`config::relay`] within this [`Environment`].
|
|
pub fn relay(&mut self, alias: &'static str) -> Node<MemorySigner> {
|
|
self.node_with(config::relay(alias))
|
|
}
|
|
|
|
/// Convenience method for creating a [`Node<MemorySigner>`]
|
|
/// using configuration [`config::seed`] within this [`Environment`].
|
|
pub fn seed(&mut self, alias: &'static str) -> Node<MemorySigner> {
|
|
self.node_with(config::seed(alias))
|
|
}
|
|
|
|
/// Convenience method for placing repository fixture into the
|
|
/// directory returned by [`Environment::work`] for the user.
|
|
/// Use this only in tests that act on *a single repository* only.
|
|
/// For tests that need to act on multiple repositories,
|
|
/// create the repositories as subdirectories of the working
|
|
/// directory returned by [`Environment::work`].
|
|
pub fn repository(
|
|
&self,
|
|
has_alias: &impl HasAlias,
|
|
) -> (radicle_cli::git::Repository, git::raw::Oid) {
|
|
radicle::test::fixtures::repository(self.work(has_alias).as_path())
|
|
}
|
|
|
|
// Convenience method for executing a test formula with standard configuration.
|
|
pub fn test(
|
|
&self,
|
|
test_file: &'static str,
|
|
subject: &impl HasAlias,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
formula(
|
|
self.work(subject).as_ref(),
|
|
PathBuf::from("examples").join(test_file.to_owned() + ".md"),
|
|
)?
|
|
.env("USER", subject.alias().as_ref())
|
|
.env("RAD_HOME", self.rad_home(subject).path().to_string_lossy())
|
|
.env(
|
|
"JJ_CONFIG",
|
|
self.unix_home(subject)
|
|
.join(".jjconfig.toml")
|
|
.to_string_lossy(),
|
|
)
|
|
.run()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Convenience method for executing multiple test formulas with standard configuration.
|
|
pub fn tests(
|
|
&self,
|
|
test_files: impl IntoIterator<Item = &'static str>,
|
|
subject: &impl HasAlias,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
for test_file in test_files {
|
|
self.test(test_file, subject)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Convenience method for creating exactly one profile with alias "alice"
|
|
/// and running tests within it.
|
|
pub fn alice(test_files: impl IntoIterator<Item = &'static str>) {
|
|
let mut env = Environment::new();
|
|
let alice = env.profile("alice");
|
|
env.repository(&alice);
|
|
env.tests(test_files, &alice).unwrap();
|
|
}
|
|
}
|
|
|
|
pub trait HasAlias {
|
|
fn alias(&self) -> &Alias;
|
|
}
|
|
|
|
impl HasAlias for Alias {
|
|
fn alias(&self) -> &Alias {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl HasAlias for Node<MemorySigner> {
|
|
fn alias(&self) -> &Alias {
|
|
&self.config.alias
|
|
}
|
|
}
|
|
|
|
impl HasAlias for Profile {
|
|
fn alias(&self) -> &Alias {
|
|
self.config.alias()
|
|
}
|
|
}
|
|
|
|
impl<G> HasAlias for NodeHandle<G> {
|
|
fn alias(&self) -> &Alias {
|
|
&self.alias
|
|
}
|
|
}
|