Skip to content
Snippets Groups Projects
Commit f3fac52c authored by Éloïs's avatar Éloïs
Browse files

WIP: feat(tests): allow custom genesis for each cucumber scenario

parent 701913b9
Branches
Tags
1 merge request!50Custom genesis for end2end tests & cucumber binary in docker
......@@ -58,21 +58,26 @@ Feature: My awesome feature
- eve
- ferdie
### Currency amounts
### genesis state
Amounts must be expressed as an integer of `ĞD` or `UD`, decimal numbers are not supported.
If you need more precision, you can express amounts in cents of ĞD (write `cĞD`), or in thousandths
of UD (write `mUD`).
Each scenario bootstraps its own blockchain with its own genesis state.
#### Given
By default, all scenarios use the same configuration for the genesis, which is located in the file
`/cucumber-genesis/default.json`.
You can give any currency balance to each of the test users, so money will be created ex-nihilo for
that user. Note that this created money is not included in the monetary mass used to revalue the UD
amount.
You can define a custom genesis state for each scenario with the tag `@genesis.confName`.
Usage: `{user} have {amount} {unit}`
The genesis configuration must then be defined in a json file located at
`/cucumber-genesis/confName.json`.
You can also define a custom genesis at the feature level, all the scenarios of this feature will
then inherit the genesis configuration.
Example: `alice have 10 ĞD`
### Currency amounts
Amounts must be expressed as an integer of `ĞD` or `UD`, decimal numbers are not supported.
If you need more precision, you can express amounts in cents of ĞD (write `cĞD`), or in thousandths
of UD (write `mUD`).
#### When
......
Feature: Balance transfer
Scenario: After 10 blocks, the monetary mass should be 30 ĞD
Then Monetary mass should be 0.00 ĞD
Then Current UD amount should be 10.00 ĞD
When 10 blocks later
Scenario: After 10 blocks, the monetary mass should be 60 ĞD
Then Monetary mass should be 30.00 ĞD
Then Current UD amount should be 10.00 ĞD
When 10 blocks later
Then Monetary mass should be 60.00 ĞD
When 10 blocks later
Then Monetary mass should be 90.00 ĞD
Then Current UD amount should be 10.00 ĞD
@genesis.default
Feature: Balance transfer all
Scenario: If alice sends all her ĞDs to Dave, Dave will get 10 ĞD
Given alice have 10 ĞD
Scenario: If alice sends all her ĞDs to Dave, Dave will get 8 ĞD
When alice sends all her ĞDs to dave
Then dave should have 10 ĞD
"""
Alice is a smith member, as such she is not allowed to empty her account completely,
if she tries to do so, the existence deposit (2 ĞD) must remain.
"""
Then alice should have 2 ĞD
Then dave should have 8 ĞD
{
"first_ud": 1000,
"first_ud_reeval": 100,
"identities": {
"Alice": {
"balance": 1000,
"certs": ["Bob", "Charlie"],
"pubkey": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
},
"Bob": {
"balance": 1000,
"certs": ["Alice", "Charlie"],
"pubkey": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
},
"Charlie": {
"balance": 1000,
"certs": ["Alice", "Bob"],
"pubkey": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y"
}
},
"parameters": {
"babe_epoch_duration": 30,
"cert_period": 15,
"cert_max_by_issuer": 10,
"cert_min_received_cert_to_issue_cert": 2,
"cert_renewable_period": 50,
"cert_validity_period": 1000,
"idty_confirm_period": 40,
"idty_creation_period": 50,
"membership_period": 1000,
"membership_renewable_period": 50,
"pending_membership_period": 500,
"ud_creation_period": 10,
"ud_reeval_period": 100,
"smith_cert_period": 15,
"smith_cert_max_by_issuer": 8,
"smith_cert_min_received_cert_to_issue_cert": 2,
"smith_cert_renewable_period": 50,
"smith_cert_validity_period": 1000,
"smith_membership_period": 1000,
"smith_membership_renewable_period": 20,
"smith_pending_membership_period": 500,
"smiths_wot_first_cert_issuable_on": 20,
"smiths_wot_min_cert_for_membership": 2,
"wot_first_cert_issuable_on": 20,
"wot_min_cert_for_create_idty_right": 2,
"wot_min_cert_for_membership": 2
},
"smiths": {
"Alice": {
"certs": ["Bob", "Charlie"]
},
"Bob": {
"certs": ["Alice", "Charlie"]
},
"Charlie": {
"certs": ["Alice", "Bob"]
}
},
"sudo_key": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
}
......@@ -24,6 +24,7 @@ pub mod node_runtime {}
use serde_json::Value;
use sp_keyring::AccountKeyring;
use std::io::prelude::*;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use subxt::rpc::{rpc_params, ClientT, SubscriptionClientT};
......@@ -38,9 +39,8 @@ pub type TransactionProgress<'client> =
pub const SUDO_ACCOUNT: AccountKeyring = AccountKeyring::Alice;
pub struct Process(std::process::Child);
impl Drop for Process {
fn drop(&mut self) {
impl Process {
pub fn kill(&mut self) {
self.0.kill().expect("node already down");
}
}
......@@ -51,7 +51,8 @@ struct FullNode {
ws_port: u16,
}
pub async fn spawn_node() -> (Api, Client, Process) {
pub async fn spawn_node(maybe_genesis_conf_file: Option<PathBuf>) -> (Api, Client, Process) {
println!("maybe_genesis_conf_file={:?}", maybe_genesis_conf_file);
let duniter_binary_path = std::env::var("DUNITER_BINARY_PATH")
.unwrap_or_else(|_| "../target/debug/duniter".to_owned());
let FullNode {
......@@ -59,8 +60,9 @@ pub async fn spawn_node() -> (Api, Client, Process) {
p2p_port: _,
ws_port,
} = spawn_full_node(
&duniter_binary_path,
&["--dev", "--execution=Native", "--sealing=manual"],
&duniter_binary_path,
maybe_genesis_conf_file,
);
let client = ClientBuilder::new()
.set_url(format!("ws://127.0.0.1:{}", ws_port))
......@@ -110,12 +112,27 @@ pub async fn create_block_with_extrinsic(
.map_err(Into::into)
}
fn spawn_full_node(duniter_binary_path: &str, args: &[&str]) -> FullNode {
fn spawn_full_node(
args: &[&str],
duniter_binary_path: &str,
maybe_genesis_conf_file: Option<PathBuf>,
) -> FullNode {
// Ports
let p2p_port = portpicker::pick_unused_port().expect("No ports free");
let rpc_port = portpicker::pick_unused_port().expect("No ports free");
let ws_port = portpicker::pick_unused_port().expect("No ports free");
// Env vars
let mut envs = Vec::new();
if let Some(genesis_conf_file) = maybe_genesis_conf_file {
envs.push(("DUNITER_GENESIS_CONFIG", genesis_conf_file));
}
// Logs
let log_file_path = format!("duniter-v2s-{}.log", ws_port);
let log_file = std::fs::File::create(&log_file_path).expect("fail to create log file");
// Command
let process = Process(
Command::new(duniter_binary_path)
.args(
......@@ -133,6 +150,7 @@ fn spawn_full_node(duniter_binary_path: &str, args: &[&str]) -> FullNode {
.iter()
.chain(args),
)
.envs(envs)
.stdout(std::process::Stdio::null())
.stderr(log_file)
.spawn()
......
......@@ -21,13 +21,38 @@ use common::*;
use cucumber::{given, then, when, World, WorldInit};
use sp_keyring::AccountKeyring;
use std::convert::Infallible;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(WorldInit)]
pub struct DuniterWorld {
api: Api,
client: Client,
_process: Process,
pub struct DuniterWorld(Option<DuniterWorldInner>);
impl DuniterWorld {
async fn init(&mut self, maybe_genesis_conf_file: Option<PathBuf>) {
if let Some(ref mut inner) = self.0 {
inner.kill();
}
self.0 = Some(DuniterWorldInner::new(maybe_genesis_conf_file).await);
}
fn api(&self) -> &Api {
if let Some(ref inner) = self.0 {
&inner.api
} else {
panic!("uninit")
}
}
fn client(&self) -> &Client {
if let Some(ref inner) = self.0 {
&inner.client
} else {
panic!("uninit")
}
}
fn kill(&mut self) {
if let Some(ref mut inner) = self.0 {
inner.kill();
}
}
}
impl std::fmt::Debug for DuniterWorld {
......@@ -42,12 +67,27 @@ impl World for DuniterWorld {
type Error = Infallible;
async fn new() -> std::result::Result<Self, Infallible> {
let (api, client, _process) = spawn_node().await;
Ok(DuniterWorld {
Ok(DuniterWorld(None))
}
}
struct DuniterWorldInner {
api: Api,
client: Client,
process: Process,
}
impl DuniterWorldInner {
async fn new(maybe_genesis_conf_file: Option<PathBuf>) -> Self {
let (api, client, process) = spawn_node(maybe_genesis_conf_file).await;
DuniterWorldInner {
api,
client,
_process,
})
process,
}
}
fn kill(&mut self) {
self.process.kill();
}
}
......@@ -69,7 +109,7 @@ async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: Stri
if is_ud {
let current_ud_amount = world
.api
.api()
.storage()
.universal_dividend()
.current_ud(None)
......@@ -78,7 +118,7 @@ async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: Stri
}
// Create {amount} ĞD for {who}
common::balances::set_balance(&world.api, &world.client, who, amount).await?;
common::balances::set_balance(&world.api(), &world.client(), who, amount).await?;
Ok(())
}
......@@ -86,7 +126,7 @@ async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: Stri
#[when(regex = r"(\d+) blocks? later")]
async fn n_blocks_later(world: &mut DuniterWorld, n: usize) -> Result<()> {
for _ in 0..n {
common::create_empty_block(&world.client).await?;
common::create_empty_block(&world.client()).await?;
}
Ok(())
}
......@@ -105,9 +145,9 @@ async fn transfer(
let (amount, is_ud) = parse_amount(amount, &unit);
if is_ud {
common::balances::transfer_ud(&world.api, &world.client, from, amount, to).await
common::balances::transfer_ud(&world.api(), &world.client(), from, amount, to).await
} else {
common::balances::transfer(&world.api, &world.client, from, amount, to).await
common::balances::transfer(&world.api(), &world.client(), from, amount, to).await
}
}
......@@ -117,7 +157,7 @@ async fn send_all_to(world: &mut DuniterWorld, from: String, to: String) -> Resu
let from = AccountKeyring::from_str(&from).expect("unknown from");
let to = AccountKeyring::from_str(&to).expect("unknown to");
common::balances::transfer_all(&world.api, &world.client, from, to).await
common::balances::transfer_all(&world.api(), &world.client(), from, to).await
}
#[then(regex = r"([a-zA-Z]+) should have (\d+) ĞD")]
......@@ -128,7 +168,7 @@ async fn should_have(world: &mut DuniterWorld, who: String, amount: u64) -> Resu
.to_account_id();
let amount = amount * 100;
let who_account = world.api.storage().system().account(who, None).await?;
let who_account = world.api().storage().system().account(who, None).await?;
assert_eq!(who_account.data.free, amount);
Ok(())
}
......@@ -141,7 +181,7 @@ async fn current_ud_amount_should_be(
) -> Result<()> {
let expected = (amount * 100) + cents;
let actual = world
.api
.api()
.storage()
.universal_dividend()
.current_ud(None)
......@@ -154,7 +194,7 @@ async fn current_ud_amount_should_be(
async fn monetary_mass_should_be(world: &mut DuniterWorld, amount: u64, cents: u64) -> Result<()> {
let expected = (amount * 100) + cents;
let actual = world
.api
.api()
.storage()
.universal_dividend()
.monetary_mass(None)
......@@ -170,6 +210,35 @@ async fn main() {
DuniterWorld::cucumber()
//.fail_on_skipped()
.max_concurrent_scenarios(4)
.before(|feature, _rule, scenario, world| {
let mut genesis_conf_file_path = PathBuf::new();
genesis_conf_file_path.push("cucumber-genesis");
genesis_conf_file_path.push(&format!(
"{}.json",
genesis_conf_name(&feature.tags, &scenario.tags)
));
Box::pin(world.init(Some(genesis_conf_file_path)))
})
.after(|_feature, _rule, _scenario, maybe_world| {
if let Some(world) = maybe_world {
world.kill();
}
Box::pin(std::future::ready(()))
})
.run_and_exit("cucumber-features")
.await;
}
fn genesis_conf_name(feature_tags: &[String], scenario_tags: &[String]) -> String {
for tag in scenario_tags {
if let Some(("genesis", conf_name)) = tag.split_once(".") {
return conf_name.to_owned();
}
}
for tag in feature_tags {
if let Some(("genesis", conf_name)) = tag.split_once(".") {
return conf_name.to_owned();
}
}
"default".to_owned()
}
......@@ -62,6 +62,49 @@ pub fn get_authority_keys_from_seed(s: &str) -> AuthorityKeys {
pub fn development_chain_spec() -> Result<ChainSpec, String> {
let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?;
if std::env::var("DUNITER_GENESIS_CONFIG").is_ok() {
super::gen_genesis_data::generate_genesis_data(
|genesis_data| {
ChainSpec::from_genesis(
// Name
"Development",
// ID
"gdev",
sc_service::ChainType::Development,
move || genesis_data_to_gdev_genesis_conf(genesis_data.clone(), wasm_binary),
// Bootnodes
vec![],
// Telemetry
None,
// Protocol ID
None,
//Fork ID
None,
// Properties
Some(
serde_json::json!({
"tokenDecimals": TOKEN_DECIMALS,
"tokenSymbol": TOKEN_SYMBOL,
})
.as_object()
.expect("must be a map")
.clone(),
),
// Extensions
None,
)
},
Some(get_authority_keys_from_seed("Alice").encode()),
Some(super::gen_genesis_data::ParamsAppliedAtGenesis {
genesis_certs_expire_on: 100_000,
genesis_smith_certs_expire_on: 100_000,
genesis_memberships_expire_on: 100_000,
genesis_memberships_renewable_on: 50,
genesis_smith_memberships_expire_on: 100_000,
genesis_smith_memberships_renewable_on: 50,
}),
)
} else {
Ok(ChainSpec::from_genesis(
// Name
"Development",
......@@ -104,6 +147,7 @@ pub fn development_chain_spec() -> Result<ChainSpec, String> {
None,
))
}
}
pub fn gen_live_conf() -> Result<ChainSpec, String> {
let wasm_binary = WASM_BINARY.ok_or_else(|| "wasm not available".to_string())?;
......@@ -139,6 +183,7 @@ pub fn gen_live_conf() -> Result<ChainSpec, String> {
None,
)
},
None,
Some(super::gen_genesis_data::ParamsAppliedAtGenesis {
genesis_certs_expire_on: 100_000,
genesis_smith_certs_expire_on: 100_000,
......
......@@ -85,6 +85,7 @@ struct SmithData {
pub fn generate_genesis_data<CS, P, SK, F>(
f: F,
maybe_force_authority: Option<Vec<u8>>,
params_applied_at_genesis: Option<ParamsAppliedAtGenesis>,
) -> Result<CS, String>
where
......@@ -234,6 +235,7 @@ where
// SMITHS SUB-WOT //
let mut initial_authorities = BTreeMap::new();
let mut online_authorities_counter = 0;
let mut session_keys_map = BTreeMap::new();
let mut smiths_memberships = BTreeMap::new();
let mut smiths_certs_by_issuer = BTreeMap::new();
......@@ -253,15 +255,27 @@ where
}
// Initial authorities
if maybe_force_authority.is_some() {
if smith_data.session_keys.is_some() {
return Err(format!("session_keys field forbidden",));
}
if *idty_index == 1 {
initial_authorities.insert(1, (identity.pubkey.clone(), true));
}
} else {
initial_authorities.insert(
*idty_index,
(identity.pubkey.clone(), smith_data.session_keys.is_some()),
);
}
// Session keys
let session_keys_bytes = if let Some(ref session_keys) = smith_data.session_keys {
online_authorities_counter += 1;
hex::decode(&session_keys[2..])
.map_err(|_| format!("invalid session keys for idty {}", &idty_name))?
} else if let (1, Some(ref session_keys_bytes)) = (*idty_index, &maybe_force_authority) {
session_keys_bytes.clone()
} else {
// Create fake session keys (must be unique and deterministic)
let mut fake_session_keys_bytes = Vec::with_capacity(128);
......@@ -297,6 +311,12 @@ where
);
}
if maybe_force_authority.is_none() && online_authorities_counter == 0 {
return Err(format!(
"The session_keys field must be filled in for at least one smith.",
));
}
let genesis_data = GenesisData {
accounts,
certs_by_issuer,
......
......@@ -32,8 +32,7 @@
"pending_membership_period": 500,
"ud_creation_period": 10,
"ud_first_reeval": 0,
"ud_reeval_period": 50,
"ud_reeval_period_in_blocks": 500,
"ud_reeval_period": 200,
"smith_cert_period": 15,
"smith_cert_max_by_issuer": 8,
"smith_cert_min_received_cert_to_issue_cert": 2,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment