cob: mutable stable time for testing

The `radicle-cob` crate provides a feature flag, `stable-commit-ids` to allow
for `Oid`s to remain stable for testing by always using the same author and
timestamp when creating commits.

This can be an issue when it interplays with COB related tests that are relying
on the ordering for the DAG walk. If the `Oid` values change and get re-ordered
then the test will fail. The DAG for COBs are now ordered via the `Oid` and
`Timestamp`.

In this change, a global timestamp is introduced that is initialised
per thread. The value is read, via `read_timestamp`, and can be
advanced using `with_advanced_timestamp` – while performing an action
that creates a new commit.
This commit is contained in:
Fintan Halpenny 2024-11-12 16:08:38 +00:00 committed by cloudhead
parent cab56c1113
commit 259adf7dc2
No known key found for this signature in database
4 changed files with 111 additions and 19 deletions

View File

@ -2,6 +2,9 @@
pub mod change; pub mod change;
#[cfg(feature = "stable-commit-ids")]
pub mod stable;
/// Environment variable to set to overwrite the commit date for both the author and the committer. /// Environment variable to set to overwrite the commit date for both the author and the committer.
/// ///
/// The format must be a unix timestamp. /// The format must be a unix timestamp.

View File

@ -301,13 +301,16 @@ fn write_commit(
#[cfg(feature = "stable-commit-ids")] #[cfg(feature = "stable-commit-ids")]
// Ensures the commit id doesn't change on every run. // Ensures the commit id doesn't change on every run.
let (author, timestamp) = ( let (author, timestamp) = {
Author { let stable = crate::git::stable::read_timestamp();
time: git_ext::author::Time::new(1514817556, 0), (
..author Author {
}, time: git_ext::author::Time::new(stable, 0),
1514817556, ..author
); },
stable,
)
};
let (author, timestamp) = if let Ok(s) = std::env::var(crate::git::GIT_COMMITTER_DATE) { let (author, timestamp) = if let Ok(s) = std::env::var(crate::git::GIT_COMMITTER_DATE) {
let Ok(timestamp) = s.trim().parse::<i64>() else { let Ok(timestamp) = s.trim().parse::<i64>() else {
panic!( panic!(

View File

@ -0,0 +1,79 @@
use std::{cell::Cell, ops::Add};
thread_local! {
/// The constant time used by the stable-commit-ids feature.
pub static STABLE_TIME: Cell<i64> = const { Cell::new(1514817556) };
/// An incrementing counter to advance the `STABLE_TIME` value with in
/// [`with_advanced_timestamp`].
pub static STEP: Cell<Step> = Cell::new(Step::default());
}
#[derive(Clone, Copy)]
struct Step(i64);
impl Default for Step {
fn default() -> Self {
Self(1)
}
}
impl Add<Step> for i64 {
type Output = i64;
fn add(self, rhs: Step) -> Self::Output {
self + rhs.0
}
}
impl Add<i64> for Step {
type Output = Step;
fn add(self, rhs: i64) -> Self::Output {
Step(self.0 + rhs)
}
}
/// Read the current value of `STABLE_TIME`.
///
/// # Panics
///
/// The `STABLE_TIME` is declared in `thread_local`, and so the panic
/// information is repeated here.
///
/// Panics if the key currently has its destructor running, and it may panic if
/// the destructor has previously been run for this thread.
#[allow(clippy::unwrap_used)]
pub fn read_timestamp() -> i64 {
STABLE_TIME.get()
}
/// Perform an action `f` that would rely on the `STABLE_TIME` value. This will
/// advance the `STABLE_TIME` by an increment of `1` for each time it is called,
/// within the same thread.
///
/// # Usage
///
/// ```no_run
/// let oid1 = with_advanced_timestamp(|| cob.update("New revision OID"));
/// let oid2 = with_advanced_timestamp(|| cob.update("Another revision OID"));
/// ```
///
/// # Panics
///
/// The `STABLE_TIME` is declared in `thread_local`, and so the panic
/// information is repeated here.
///
/// Panics if the key currently has its destructor running, and it may panic if
/// the destructor has previously been run for this thread.
#[allow(clippy::unwrap_used)]
pub fn with_advanced_timestamp<F, T>(f: F) -> T
where
F: FnOnce() -> T,
{
let step = STEP.get();
let original = read_timestamp();
STABLE_TIME.replace(original + step);
let result = f();
STEP.replace(step + 1);
result
}

View File

@ -1016,7 +1016,10 @@ mod test {
use radicle_crypto::test::signer::MockSigner; use radicle_crypto::test::signer::MockSigner;
use radicle_crypto::Signer as _; use radicle_crypto::Signer as _;
use crate::cob;
use crate::crypto::PublicKey; use crate::crypto::PublicKey;
use crate::identity::did::Did;
use crate::identity::doc::PayloadId;
use crate::identity::Visibility; use crate::identity::Visibility;
use crate::rad; use crate::rad;
use crate::storage::git::Storage; use crate::storage::git::Storage;
@ -1025,8 +1028,6 @@ mod test {
use crate::test::setup::{Network, NodeWithRepo}; use crate::test::setup::{Network, NodeWithRepo};
use super::*; use super::*;
use crate::identity::did::Did;
use crate::identity::doc::PayloadId;
#[quickcheck] #[quickcheck]
fn prop_json_eq_str(pk: PublicKey, proj: RepoId, did: Did) { fn prop_json_eq_str(pk: PublicKey, proj: RepoId, did: Did) {
@ -1319,20 +1320,24 @@ mod test {
eve.repo.fetch(bob); eve.repo.fetch(bob);
let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap(); let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
let b1 = bob_identity.accept(&a2, &bob.signer).unwrap(); let b1 = cob::git::stable::with_advanced_timestamp(|| {
bob_identity.accept(&a2, &bob.signer).unwrap()
});
assert_eq!(bob_identity.current, a2); assert_eq!(bob_identity.current, a2);
let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap(); let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
let mut eve_doc = eve_identity.doc().clone().edit(); let mut eve_doc = eve_identity.doc().clone().edit();
eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]); eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
let e1 = eve_identity let e1 = cob::git::stable::with_advanced_timestamp(|| {
.update( eve_identity
"Change visibility", .update(
"", "Change visibility",
&eve_doc.verified().unwrap(), "",
&eve.signer, &eve_doc.verified().unwrap(),
) &eve.signer,
.unwrap(); )
.unwrap()
});
// Eve's revision is active. // Eve's revision is active.
assert!(eve_identity.revision(&e1).unwrap().is_active()); assert!(eve_identity.revision(&e1).unwrap().is_active());
@ -1506,7 +1511,9 @@ mod test {
assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Active); assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Active);
alice_identity.reload().unwrap(); alice_identity.reload().unwrap();
let a2 = alice_identity.accept(&b1, &alice.signer).unwrap(); let a2 = cob::git::stable::with_advanced_timestamp(|| {
alice_identity.accept(&b1, &alice.signer).unwrap()
});
eve.repo.fetch(alice); eve.repo.fetch(alice);
eve_identity.reload().unwrap(); eve_identity.reload().unwrap();