cli: templates check: add option to --stabilise date and RNG

This commit is contained in:
Olivier 'reivilibre
2025-10-27 14:40:28 +00:00
parent 1744c89239
commit 56e2918030
5 changed files with 113 additions and 92 deletions

View File

@@ -8,6 +8,7 @@ use std::{fmt::Write, process::ExitCode};
use anyhow::{Context as _, bail};
use camino::Utf8PathBuf;
use chrono::DateTime;
use clap::Parser;
use figment::Figment;
use mas_config::{
@@ -34,6 +35,12 @@ enum Subcommand {
/// The directory must either not exist or be empty.
#[arg(long = "out-dir")]
out_dir: Option<Utf8PathBuf>,
/// Attempt to remove 'unstable' template input data such as asset
/// hashes, in order to make renders more reproducible between
/// versions.
#[arg(long = "stabilise")]
stabilise: bool,
},
}
@@ -41,7 +48,7 @@ impl Options {
pub async fn run(self, figment: &Figment) -> anyhow::Result<ExitCode> {
use Subcommand as SC;
match self.subcommand {
SC::Check { out_dir } => {
SC::Check { out_dir, stabilise } => {
let _span = info_span!("cli.templates.check").entered();
let template_config = TemplatesConfig::extract_or_default(figment)
@@ -59,9 +66,17 @@ impl Options {
let captcha_config = CaptchaConfig::extract_or_default(figment)
.map_err(anyhow::Error::from_boxed)?;
let clock = SystemClock::default();
// XXX: we should disallow SeedableRng::from_entropy
let mut rng = rand_chacha::ChaChaRng::from_entropy();
let now = if stabilise {
DateTime::from_timestamp_secs(0).unwrap()
} else {
SystemClock::default().now()
};
let rng = if stabilise {
rand_chacha::ChaChaRng::from_seed([42; 32])
} else {
// XXX: we should disallow SeedableRng::from_entropy
rand_chacha::ChaChaRng::from_entropy()
};
let url_builder =
mas_router::UrlBuilder::new("https://example.com/".parse()?, None, None);
let site_config = site_config_from_config(
@@ -79,7 +94,7 @@ impl Options {
true,
)
.await?;
let all_renders = templates.check_render(clock.now(), &mut rng)?;
let all_renders = templates.check_render(now, &rng)?;
if let Some(out_dir) = out_dir {
// Save renders to disk.

View File

@@ -106,9 +106,9 @@ pub trait TemplateContext: Serialize {
///
/// This is then used to check for template validity in unit tests and in
/// the CLI (`cargo run -- templates check`)
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -144,9 +144,9 @@ pub(crate) fn sample_list<T: TemplateContext>(samples: Vec<T>) -> BTreeMap<Sampl
}
impl TemplateContext for () {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -181,9 +181,9 @@ impl<T> std::ops::Deref for WithLanguage<T> {
}
impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -192,7 +192,9 @@ impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
locales
.iter()
.flat_map(|locale| {
T::sample(now, rng, locales)
// Make samples deterministic between locales
let mut rng = rng.clone();
T::sample(now, &mut rng, locales)
.into_iter()
.map(|(sample_id, sample)| {
(
@@ -218,9 +220,9 @@ pub struct WithCsrf<T> {
}
impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -251,9 +253,9 @@ pub struct WithSession<T> {
}
impl<T: TemplateContext> TemplateContext for WithSession<T> {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -289,9 +291,9 @@ pub struct WithOptionalSession<T> {
}
impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -340,9 +342,9 @@ impl Serialize for EmptyContext {
}
impl TemplateContext for EmptyContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -368,9 +370,9 @@ impl IndexContext {
}
impl TemplateContext for IndexContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -414,9 +416,9 @@ impl AppContext {
}
impl TemplateContext for AppContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -447,9 +449,9 @@ impl ApiDocContext {
}
impl TemplateContext for ApiDocContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -539,9 +541,9 @@ pub struct LoginContext {
}
impl TemplateContext for LoginContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -647,9 +649,9 @@ pub struct RegisterContext {
}
impl TemplateContext for RegisterContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -690,9 +692,9 @@ pub struct PasswordRegisterContext {
}
impl TemplateContext for PasswordRegisterContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -732,9 +734,9 @@ pub struct ConsentContext {
}
impl TemplateContext for ConsentContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -790,9 +792,9 @@ pub struct PolicyViolationContext {
}
impl TemplateContext for PolicyViolationContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -865,9 +867,9 @@ pub struct CompatSsoContext {
}
impl TemplateContext for CompatSsoContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -927,9 +929,9 @@ impl EmailRecoveryContext {
}
impl TemplateContext for EmailRecoveryContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -992,9 +994,9 @@ impl EmailVerificationContext {
}
impl TemplateContext for EmailVerificationContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1067,9 +1069,9 @@ impl RegisterStepsVerifyEmailContext {
}
impl TemplateContext for RegisterStepsVerifyEmailContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1107,9 +1109,9 @@ impl RegisterStepsEmailInUseContext {
}
impl TemplateContext for RegisterStepsEmailInUseContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1162,9 +1164,9 @@ impl RegisterStepsDisplayNameContext {
}
impl TemplateContext for RegisterStepsDisplayNameContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<chrono::Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1217,9 +1219,9 @@ impl RegisterStepsRegistrationTokenContext {
}
impl TemplateContext for RegisterStepsRegistrationTokenContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<chrono::Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1268,9 +1270,9 @@ impl RecoveryStartContext {
}
impl TemplateContext for RecoveryStartContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1310,9 +1312,9 @@ impl RecoveryProgressContext {
}
impl TemplateContext for RecoveryProgressContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1356,9 +1358,9 @@ impl RecoveryExpiredContext {
}
impl TemplateContext for RecoveryExpiredContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1420,9 +1422,9 @@ impl RecoveryFinishContext {
}
impl TemplateContext for RecoveryFinishContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1469,9 +1471,9 @@ impl UpstreamExistingLinkContext {
}
impl TemplateContext for UpstreamExistingLinkContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1507,9 +1509,9 @@ impl UpstreamSuggestLink {
}
impl TemplateContext for UpstreamSuggestLink {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1636,9 +1638,9 @@ impl UpstreamRegister {
}
impl TemplateContext for UpstreamRegister {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1722,9 +1724,9 @@ impl DeviceLinkContext {
}
impl TemplateContext for DeviceLinkContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1756,9 +1758,9 @@ impl DeviceConsentContext {
}
impl TemplateContext for DeviceConsentContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1801,9 +1803,9 @@ impl AccountInactiveContext {
}
impl TemplateContext for AccountInactiveContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1837,9 +1839,9 @@ impl DeviceNameContext {
}
impl TemplateContext for DeviceNameContext {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1863,9 +1865,9 @@ pub struct FormPostContext<T> {
}
impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<Utc>,
rng: &mut impl Rng,
rng: &mut R,
locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -1945,9 +1947,9 @@ impl std::fmt::Display for ErrorContext {
}
impl TemplateContext for ErrorContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: chrono::DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where
@@ -2039,9 +2041,9 @@ impl NotFoundContext {
}
impl TemplateContext for NotFoundContext {
fn sample(
fn sample<R: Rng + Clone>(
_now: DateTime<Utc>,
_rng: &mut impl Rng,
_rng: &mut R,
_locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where

View File

@@ -11,6 +11,7 @@ use minijinja::{
Value,
value::{Enumerator, Object},
};
use rand::Rng;
use serde::Serialize;
use crate::{TemplateContext, context::SampleIdentifier};
@@ -58,9 +59,9 @@ impl<T> WithCaptcha<T> {
}
impl<T: TemplateContext> TemplateContext for WithCaptcha<T> {
fn sample(
fn sample<R: Rng + Clone>(
now: chrono::DateTime<chrono::prelude::Utc>,
rng: &mut impl rand::prelude::Rng,
rng: &mut R,
locales: &[DataLocale],
) -> BTreeMap<SampleIdentifier, Self>
where

View File

@@ -471,10 +471,10 @@ impl Templates {
/// # Errors
///
/// Returns an error if any of the templates fails to render
pub fn check_render(
pub fn check_render<R: Rng + Clone>(
&self,
now: chrono::DateTime<chrono::Utc>,
rng: &mut impl Rng,
rng: &R,
) -> anyhow::Result<BTreeMap<(&'static str, SampleIdentifier), String>> {
check::all(self, now, rng)
}
@@ -489,7 +489,7 @@ mod tests {
#[allow(clippy::disallowed_methods)]
let now = chrono::Utc::now();
#[allow(clippy::disallowed_methods)]
let mut rng = rand::thread_rng();
let rng = rand::thread_rng();
let path = Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../templates/");
let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
@@ -517,6 +517,6 @@ mod tests {
)
.await
.unwrap();
templates.check_render(now, &mut rng).unwrap();
templates.check_render(now, &rng).unwrap();
}
}

View File

@@ -78,15 +78,18 @@ macro_rules! register_templates {
/// # Errors
///
/// Returns an error if any template fails to render with any of the sample.
pub(crate) fn all(templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &mut impl rand::Rng) -> anyhow::Result<::std::collections::BTreeMap<(&'static str, SampleIdentifier), String>> {
pub(crate) fn all<R: Rng + Clone>(templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &R) -> anyhow::Result<::std::collections::BTreeMap<(&'static str, SampleIdentifier), String>> {
let mut out = ::std::collections::BTreeMap::new();
// TODO shouldn't the Rng be independent for each render?
$(
out.extend(
$name $(::< $( $generic_default ),* >)? (templates, now, rng)?
.into_iter()
.map(|(sample_identifier, rendered)| (($template, sample_identifier), rendered))
);
{
let mut rng = rng.clone();
out.extend(
$name $(::< _ $( , $generic_default ),* >)? (templates, now, &mut rng)?
.into_iter()
.map(|(sample_identifier, rendered)| (($template, sample_identifier), rendered))
);
}
)*
Ok(out)
@@ -101,8 +104,8 @@ macro_rules! register_templates {
///
/// Returns an error if the template fails to render with any of the sample.
pub(crate) fn $name
$(< $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ >)?
(templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &mut impl rand::Rng)
< __R: Rng + Clone $( , $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ )? >
(templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &mut __R)
-> anyhow::Result<BTreeMap<SampleIdentifier, String>> {
let locales = templates.translator().available_locales();
let samples: BTreeMap<SampleIdentifier, $param > = TemplateContext::sample(now, rng, &locales);