##// END OF EJS Templates
tests: use sha256line.py instead of /dev/random in test-censor.t (issue6858)...
tests: use sha256line.py instead of /dev/random in test-censor.t (issue6858) Sometimes the systems that run our test suite don't have enough entropy and they cannot produce target file of the expected size using /dev/random, which results in test failures. Switching to /dev/urandom would give us way more available data at the cost of it being less "random", but we don't really need to use entropy for this task at all, since we only care if the file size after compression is big enough to not be stored inline in the revlog. So let's use something that we already have used to generate this kind of data in other tests.

File last commit:

r51876:8343947a default
r52255:e7be2ddf stable
Show More
config_items.rs
725 lines | 22.7 KiB | application/rls-services+xml | RustLexer
//! Code for parsing default Mercurial config items.
use itertools::Itertools;
use serde::Deserialize;
use crate::{errors::HgError, exit_codes, FastHashMap};
/// Corresponds to the structure of `mercurial/configitems.toml`.
#[derive(Debug, Deserialize)]
pub struct ConfigItems {
items: Vec<DefaultConfigItem>,
templates: FastHashMap<String, Vec<TemplateItem>>,
#[serde(rename = "template-applications")]
template_applications: Vec<TemplateApplication>,
}
/// Corresponds to a config item declaration in `mercurial/configitems.toml`.
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(try_from = "RawDefaultConfigItem")]
pub struct DefaultConfigItem {
/// Section of the config the item is in (e.g. `[merge-tools]`)
section: String,
/// Name of the item (e.g. `meld.gui`)
name: String,
/// Default value (can be dynamic, see [`DefaultConfigItemType`])
default: Option<DefaultConfigItemType>,
/// If the config option is generic (e.g. `merge-tools.*`), defines
/// the priority of this item relative to other generic items.
/// If we're looking for <pattern>, then all generic items within the same
/// section will be sorted by order of priority, and the first regex match
/// against `name` is returned.
#[serde(default)]
priority: Option<isize>,
/// Aliases, if any. Each alias is a tuple of `(section, name)` for each
/// option that is aliased to this one.
#[serde(default)]
alias: Vec<(String, String)>,
/// Whether the config item is marked as experimental
#[serde(default)]
experimental: bool,
/// The (possibly empty) docstring for the item
#[serde(default)]
documentation: String,
/// Whether the item is part of an in-core extension. This allows us to
/// hide them if the extension is not enabled, to preserve legacy
/// behavior.
#[serde(default)]
in_core_extension: Option<String>,
}
/// Corresponds to the raw (i.e. on disk) structure of config items. Used as
/// an intermediate step in deserialization.
#[derive(Clone, Debug, Deserialize)]
struct RawDefaultConfigItem {
section: String,
name: String,
default: Option<toml::Value>,
#[serde(rename = "default-type")]
default_type: Option<String>,
#[serde(default)]
priority: isize,
#[serde(default)]
generic: bool,
#[serde(default)]
alias: Vec<(String, String)>,
#[serde(default)]
experimental: bool,
#[serde(default)]
documentation: String,
#[serde(default)]
in_core_extension: Option<String>,
}
impl TryFrom<RawDefaultConfigItem> for DefaultConfigItem {
type Error = HgError;
fn try_from(value: RawDefaultConfigItem) -> Result<Self, Self::Error> {
Ok(Self {
section: value.section,
name: value.name,
default: raw_default_to_concrete(
value.default_type,
value.default,
)?,
priority: if value.generic {
Some(value.priority)
} else {
None
},
alias: value.alias,
experimental: value.experimental,
documentation: value.documentation,
in_core_extension: value.in_core_extension,
})
}
}
impl DefaultConfigItem {
fn is_generic(&self) -> bool {
self.priority.is_some()
}
pub fn in_core_extension(&self) -> Option<&str> {
self.in_core_extension.as_deref()
}
pub fn section(&self) -> &str {
self.section.as_ref()
}
}
impl<'a> TryFrom<&'a DefaultConfigItem> for Option<&'a str> {
type Error = HgError;
fn try_from(
value: &'a DefaultConfigItem,
) -> Result<Option<&'a str>, Self::Error> {
match &value.default {
Some(default) => {
let err = HgError::abort(
format!(
"programming error: wrong query on config item '{}.{}'",
value.section,
value.name
),
exit_codes::ABORT,
Some(format!(
"asked for '&str', type of default is '{}'",
default.type_str()
)),
);
match default {
DefaultConfigItemType::Primitive(toml::Value::String(
s,
)) => Ok(Some(s)),
_ => Err(err),
}
}
None => Ok(None),
}
}
}
impl<'a> TryFrom<&'a DefaultConfigItem> for Option<&'a [u8]> {
type Error = HgError;
fn try_from(
value: &'a DefaultConfigItem,
) -> Result<Option<&'a [u8]>, Self::Error> {
match &value.default {
Some(default) => {
let err = HgError::abort(
format!(
"programming error: wrong query on config item '{}.{}'",
value.section,
value.name
),
exit_codes::ABORT,
Some(format!(
"asked for bytes, type of default is '{}', \
which cannot be interpreted as bytes",
default.type_str()
)),
);
match default {
DefaultConfigItemType::Primitive(p) => {
Ok(p.as_str().map(str::as_bytes))
}
_ => Err(err),
}
}
None => Ok(None),
}
}
}
impl TryFrom<&DefaultConfigItem> for Option<bool> {
type Error = HgError;
fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
match &value.default {
Some(default) => {
let err = HgError::abort(
format!(
"programming error: wrong query on config item '{}.{}'",
value.section,
value.name
),
exit_codes::ABORT,
Some(format!(
"asked for 'bool', type of default is '{}'",
default.type_str()
)),
);
match default {
DefaultConfigItemType::Primitive(
toml::Value::Boolean(b),
) => Ok(Some(*b)),
_ => Err(err),
}
}
None => Ok(None),
}
}
}
impl TryFrom<&DefaultConfigItem> for Option<u32> {
type Error = HgError;
fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
match &value.default {
Some(default) => {
let err = HgError::abort(
format!(
"programming error: wrong query on config item '{}.{}'",
value.section,
value.name
),
exit_codes::ABORT,
Some(format!(
"asked for 'u32', type of default is '{}'",
default.type_str()
)),
);
match default {
DefaultConfigItemType::Primitive(
toml::Value::Integer(b),
) => {
Ok(Some((*b).try_into().expect("TOML integer to u32")))
}
_ => Err(err),
}
}
None => Ok(None),
}
}
}
impl TryFrom<&DefaultConfigItem> for Option<u64> {
type Error = HgError;
fn try_from(value: &DefaultConfigItem) -> Result<Self, Self::Error> {
match &value.default {
Some(default) => {
let err = HgError::abort(
format!(
"programming error: wrong query on config item '{}.{}'",
value.section,
value.name
),
exit_codes::ABORT,
Some(format!(
"asked for 'u64', type of default is '{}'",
default.type_str()
)),
);
match default {
DefaultConfigItemType::Primitive(
toml::Value::Integer(b),
) => {
Ok(Some((*b).try_into().expect("TOML integer to u64")))
}
_ => Err(err),
}
}
None => Ok(None),
}
}
}
/// Allows abstracting over more complex default values than just primitives.
/// The former `configitems.py` contained some dynamic code that is encoded
/// in this enum.
#[derive(Debug, PartialEq, Clone, Deserialize)]
pub enum DefaultConfigItemType {
/// Some primitive type (string, integer, boolean)
Primitive(toml::Value),
/// A dynamic value that will be given by the code at runtime
Dynamic,
/// An lazily-returned array (possibly only relevant in the Python impl)
/// Example: `lambda: [b"zstd", b"zlib"]`
Lambda(Vec<String>),
/// For now, a special case for `web.encoding` that points to the
/// `encoding.encoding` module in the Python impl so that local encoding
/// is correctly resolved at runtime
LazyModule(String),
ListType,
}
impl DefaultConfigItemType {
pub fn type_str(&self) -> &str {
match self {
DefaultConfigItemType::Primitive(primitive) => {
primitive.type_str()
}
DefaultConfigItemType::Dynamic => "dynamic",
DefaultConfigItemType::Lambda(_) => "lambda",
DefaultConfigItemType::LazyModule(_) => "lazy_module",
DefaultConfigItemType::ListType => "list_type",
}
}
}
/// Most of the fields are shared with [`DefaultConfigItem`].
#[derive(Debug, Clone, Deserialize)]
#[serde(try_from = "RawTemplateItem")]
struct TemplateItem {
suffix: String,
default: Option<DefaultConfigItemType>,
priority: Option<isize>,
#[serde(default)]
alias: Vec<(String, String)>,
#[serde(default)]
experimental: bool,
#[serde(default)]
documentation: String,
}
/// Corresponds to the raw (i.e. on disk) representation of a template item.
/// Used as an intermediate step in deserialization.
#[derive(Clone, Debug, Deserialize)]
struct RawTemplateItem {
suffix: String,
default: Option<toml::Value>,
#[serde(rename = "default-type")]
default_type: Option<String>,
#[serde(default)]
priority: isize,
#[serde(default)]
generic: bool,
#[serde(default)]
alias: Vec<(String, String)>,
#[serde(default)]
experimental: bool,
#[serde(default)]
documentation: String,
}
impl TemplateItem {
fn into_default_item(
self,
application: TemplateApplication,
) -> DefaultConfigItem {
DefaultConfigItem {
section: application.section,
name: application
.prefix
.map(|prefix| format!("{}.{}", prefix, self.suffix))
.unwrap_or(self.suffix),
default: self.default,
priority: self.priority,
alias: self.alias,
experimental: self.experimental,
documentation: self.documentation,
in_core_extension: None,
}
}
}
impl TryFrom<RawTemplateItem> for TemplateItem {
type Error = HgError;
fn try_from(value: RawTemplateItem) -> Result<Self, Self::Error> {
Ok(Self {
suffix: value.suffix,
default: raw_default_to_concrete(
value.default_type,
value.default,
)?,
priority: if value.generic {
Some(value.priority)
} else {
None
},
alias: value.alias,
experimental: value.experimental,
documentation: value.documentation,
})
}
}
/// Transforms the on-disk string-based representation of complex default types
/// to the concrete [`DefaultconfigItemType`].
fn raw_default_to_concrete(
default_type: Option<String>,
default: Option<toml::Value>,
) -> Result<Option<DefaultConfigItemType>, HgError> {
Ok(match default_type.as_deref() {
None => default.as_ref().map(|default| {
DefaultConfigItemType::Primitive(default.to_owned())
}),
Some("dynamic") => Some(DefaultConfigItemType::Dynamic),
Some("list_type") => Some(DefaultConfigItemType::ListType),
Some("lambda") => match &default {
Some(default) => Some(DefaultConfigItemType::Lambda(
default.to_owned().try_into().map_err(|e| {
HgError::abort(
e.to_string(),
exit_codes::ABORT,
Some("Check 'mercurial/configitems.toml'".into()),
)
})?,
)),
None => {
return Err(HgError::abort(
"lambda defined with no return value".to_string(),
exit_codes::ABORT,
Some("Check 'mercurial/configitems.toml'".into()),
))
}
},
Some("lazy_module") => match &default {
Some(default) => {
Some(DefaultConfigItemType::LazyModule(match default {
toml::Value::String(module) => module.to_owned(),
_ => {
return Err(HgError::abort(
"lazy_module module name should be a string"
.to_string(),
exit_codes::ABORT,
Some("Check 'mercurial/configitems.toml'".into()),
))
}
}))
}
None => {
return Err(HgError::abort(
"lazy_module should have a default value".to_string(),
exit_codes::ABORT,
Some("Check 'mercurial/configitems.toml'".into()),
))
}
},
Some(invalid) => {
return Err(HgError::abort(
format!("invalid default_type '{}'", invalid),
exit_codes::ABORT,
Some("Check 'mercurial/configitems.toml'".into()),
))
}
})
}
#[derive(Debug, Clone, Deserialize)]
struct TemplateApplication {
template: String,
section: String,
#[serde(default)]
prefix: Option<String>,
}
/// Represents the (dynamic) set of default core Mercurial config items from
/// `mercurial/configitems.toml`.
#[derive(Clone, Debug, Default)]
pub struct DefaultConfig {
/// Mapping of section -> (mapping of name -> item)
items: FastHashMap<String, FastHashMap<String, DefaultConfigItem>>,
}
impl DefaultConfig {
pub fn empty() -> DefaultConfig {
Self {
items: Default::default(),
}
}
/// Returns `Self`, given the contents of `mercurial/configitems.toml`
#[logging_timer::time("trace")]
pub fn from_contents(contents: &str) -> Result<Self, HgError> {
let mut from_file: ConfigItems =
toml::from_str(contents).map_err(|e| {
HgError::abort(
e.to_string(),
exit_codes::ABORT,
Some("Check 'mercurial/configitems.toml'".into()),
)
})?;
let mut flat_items = from_file.items;
for application in from_file.template_applications.drain(..) {
match from_file.templates.get(&application.template) {
None => return Err(
HgError::abort(
format!(
"template application refers to undefined template '{}'",
application.template
),
exit_codes::ABORT,
Some("Check 'mercurial/configitems.toml'".into())
)
),
Some(template_items) => {
for template_item in template_items {
flat_items.push(
template_item
.clone()
.into_default_item(application.clone()),
)
}
}
};
}
let items = flat_items.into_iter().fold(
FastHashMap::default(),
|mut acc, item| {
acc.entry(item.section.to_owned())
.or_insert_with(|| {
let mut section = FastHashMap::default();
section.insert(item.name.to_owned(), item.to_owned());
section
})
.insert(item.name.to_owned(), item);
acc
},
);
Ok(Self { items })
}
/// Return the default config item that matches `section` and `item`.
pub fn get(
&self,
section: &[u8],
item: &[u8],
) -> Option<&DefaultConfigItem> {
// Core items must be valid UTF-8
let section = String::from_utf8_lossy(section);
let section_map = self.items.get(section.as_ref())?;
let item_name_lossy = String::from_utf8_lossy(item);
match section_map.get(item_name_lossy.as_ref()) {
Some(item) => Some(item),
None => {
for generic_item in section_map
.values()
.filter(|item| item.is_generic())
.sorted_by_key(|item| match item.priority {
Some(priority) => (priority, &item.name),
_ => unreachable!(),
})
{
if regex::bytes::Regex::new(&generic_item.name)
.expect("invalid regex in configitems")
.is_match(item)
{
return Some(generic_item);
}
}
None
}
}
}
}
#[cfg(test)]
mod tests {
use crate::config::config_items::{
DefaultConfigItem, DefaultConfigItemType,
};
use super::DefaultConfig;
#[test]
fn test_config_read() {
let contents = r#"
[[items]]
section = "alias"
name = "abcd.*"
default = 3
generic = true
priority = -1
[[items]]
section = "alias"
name = ".*"
default-type = "dynamic"
generic = true
[[items]]
section = "cmdserver"
name = "track-log"
default-type = "lambda"
default = [ "chgserver", "cmdserver", "repocache",]
[[items]]
section = "chgserver"
name = "idletimeout"
default = 3600
[[items]]
section = "cmdserver"
name = "message-encodings"
default-type = "list_type"
[[items]]
section = "web"
name = "encoding"
default-type = "lazy_module"
default = "encoding.encoding"
[[items]]
section = "command-templates"
name = "graphnode"
alias = [["ui", "graphnodetemplate"]]
documentation = """This is a docstring.
This is another line \
but this is not."""
[[items]]
section = "censor"
name = "policy"
default = "abort"
experimental = true
[[template-applications]]
template = "diff-options"
section = "commands"
prefix = "revert.interactive"
[[template-applications]]
template = "diff-options"
section = "diff"
[templates]
[[templates.diff-options]]
suffix = "nodates"
default = false
[[templates.diff-options]]
suffix = "showfunc"
default = false
[[templates.diff-options]]
suffix = "unified"
"#;
let res = DefaultConfig::from_contents(contents);
let config = match res {
Ok(config) => config,
Err(e) => panic!("{}", e),
};
let expected = DefaultConfigItem {
section: "censor".into(),
name: "policy".into(),
default: Some(DefaultConfigItemType::Primitive("abort".into())),
priority: None,
alias: vec![],
experimental: true,
documentation: "".into(),
in_core_extension: None,
};
assert_eq!(config.get(b"censor", b"policy"), Some(&expected));
// Test generic priority. The `.*` pattern is wider than `abcd.*`, but
// `abcd.*` has priority, so it should match first.
let expected = DefaultConfigItem {
section: "alias".into(),
name: "abcd.*".into(),
default: Some(DefaultConfigItemType::Primitive(3.into())),
priority: Some(-1),
alias: vec![],
experimental: false,
documentation: "".into(),
in_core_extension: None,
};
assert_eq!(config.get(b"alias", b"abcdsomething"), Some(&expected));
//... but if it doesn't, we should fallback to `.*`
let expected = DefaultConfigItem {
section: "alias".into(),
name: ".*".into(),
default: Some(DefaultConfigItemType::Dynamic),
priority: Some(0),
alias: vec![],
experimental: false,
documentation: "".into(),
in_core_extension: None,
};
assert_eq!(config.get(b"alias", b"something"), Some(&expected));
let expected = DefaultConfigItem {
section: "chgserver".into(),
name: "idletimeout".into(),
default: Some(DefaultConfigItemType::Primitive(3600.into())),
priority: None,
alias: vec![],
experimental: false,
documentation: "".into(),
in_core_extension: None,
};
assert_eq!(config.get(b"chgserver", b"idletimeout"), Some(&expected));
let expected = DefaultConfigItem {
section: "cmdserver".into(),
name: "track-log".into(),
default: Some(DefaultConfigItemType::Lambda(vec![
"chgserver".into(),
"cmdserver".into(),
"repocache".into(),
])),
priority: None,
alias: vec![],
experimental: false,
documentation: "".into(),
in_core_extension: None,
};
assert_eq!(config.get(b"cmdserver", b"track-log"), Some(&expected));
let expected = DefaultConfigItem {
section: "command-templates".into(),
name: "graphnode".into(),
default: None,
priority: None,
alias: vec![("ui".into(), "graphnodetemplate".into())],
experimental: false,
documentation:
"This is a docstring.\nThis is another line but this is not."
.into(),
in_core_extension: None,
};
assert_eq!(
config.get(b"command-templates", b"graphnode"),
Some(&expected)
);
}
}