other: add schema test for uncommented default config (#1484)

* other: add schema test for uncommented default time

* ahhh

* forgot to disable this
This commit is contained in:
Clement Tsang 2024-06-17 00:59:36 -04:00 committed by GitHub
parent 7666a09162
commit 3602429529
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 257 additions and 95 deletions

View File

@ -49,7 +49,9 @@ jobs:
- name: Test nightly validates on valid sample configs
run: |
python3 scripts/schema/validator.py -s ./schema/nightly/bottom.json -f ./sample_configs/default_config.toml
python3 scripts/schema/validator.py --uncomment -s ./schema/nightly/bottom.json -f ./sample_configs/default_config.toml
python3 scripts/schema/validator.py -s ./schema/nightly/bottom.json -f ./sample_configs/demo_config.toml
- name: Test nightly catches on a bad sample config
run: |

1
Cargo.lock generated
View File

@ -185,6 +185,7 @@ dependencies = [
"serde",
"serde_json",
"starship-battery",
"strum",
"sysctl",
"sysinfo",
"thiserror",

View File

@ -72,7 +72,7 @@ nvidia = ["nvml-wrapper"]
gpu = ["nvidia"]
zfs = []
logging = ["fern", "log", "time/local-offset"]
generate_schema = ["schemars", "serde_json"]
generate_schema = ["schemars", "serde_json", "strum"]
deploy = ["battery", "gpu", "zfs"]
default = ["deploy"]
@ -85,18 +85,14 @@ concat-string = "1.0.1"
crossterm = "0.27.0"
ctrlc = { version = "3.4.4", features = ["termination"] }
dirs = "5.0.1"
fern = { version = "0.6.2", optional = true }
hashbrown = "0.14.5"
humantime = "2.1.0"
indexmap = "2.2.6"
indoc = "2.0.5"
itertools = "0.13.0"
log = { version = "0.4.21", optional = true }
nvml-wrapper = { version = "0.10.0", optional = true, features = ["legacy-functions"] }
regex = "1.10.4"
schemars = { version = "0.8.21", optional = true }
serde = { version = "1.0.203", features = ["derive"] }
serde_json = { version = "1.0.117", optional = true }
starship-battery = { version = "0.8.3", optional = true }
sysinfo = "=0.30.12"
thiserror = "1.0.61"
@ -107,6 +103,15 @@ unicode-ellipsis = "0.1.4"
unicode-segmentation = "1.11.0"
unicode-width = "0.1.12"
# Used for logging.
fern = { version = "0.6.2", optional = true }
log = { version = "0.4.21", optional = true }
# These are just used for for schema generation.
schemars = { version = "0.8.21", optional = true }
serde_json = { version = "1.0.117", optional = true }
strum = { version = "0.26", features = ["derive"], optional = true }
[target.'cfg(unix)'.dependencies]
libc = "0.2.155"

View File

@ -27,13 +27,13 @@
#whole_word = false
# Whether to make process searching use regex by default.
#regex = false
# Defaults to Celsius. Temperature is one of:
#temperature_type = "k"
#temperature_type = "f"
# The temperature unit. One of the following, defaults to "c" for Celsius:
#temperature_type = "c"
#temperature_type = "kelvin"
#temperature_type = "fahrenheit"
#temperature_type = "celsius"
##temperature_type = "k"
##temperature_type = "f"
##temperature_type = "kelvin"
##temperature_type = "fahrenheit"
##temperature_type = "celsius"
# The default time interval (in milliseconds).
#default_time_value = "60s"
# The time delta on each zoom in/out action (in milliseconds).
@ -84,7 +84,7 @@
#[processes]
# The columns shown by the process widget. The following columns are supported:
# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMEM%", "GPU%"]
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"]
# CPU widget configuration
#[cpu]

View File

@ -87,6 +87,7 @@
},
"definitions": {
"ColoursConfig": {
"description": "Colour configuration.",
"type": "object",
"properties": {
"all_cpu_color": {
@ -242,7 +243,7 @@
}
},
"CpuConfig": {
"description": "Process column settings.",
"description": "CPU column settings.",
"type": "object",
"properties": {
"default": {
@ -611,23 +612,33 @@
}
}
},
"ProcWidgetColumn": {
"description": "A hacky workaround for now.",
"ProcColumn": {
"description": "A column in the process widget.",
"type": "string",
"enum": [
"PidOrCount",
"ProcNameOrCommand",
"Cpu",
"PID",
"Count",
"Name",
"Command",
"CPU%",
"Mem",
"ReadPerSecond",
"WritePerSecond",
"TotalRead",
"TotalWrite",
"User",
"Mem%",
"R/s",
"Read",
"Rps",
"W/s",
"Write",
"Wps",
"T.Read",
"TWrite",
"T.Write",
"TRead",
"State",
"User",
"Time",
"GpuMem",
"GpuUtil"
"GMem",
"GMem%",
"GPU%"
]
},
"ProcessesConfig": {
@ -638,7 +649,7 @@
"description": "A list of process widget columns.",
"type": "array",
"items": {
"$ref": "#/definitions/ProcWidgetColumn"
"$ref": "#/definitions/ProcColumn"
}
}
}

View File

@ -5,6 +5,8 @@
import argparse
import toml
import jsonschema_rs
import re
import traceback
def main():
@ -17,6 +19,12 @@ def main():
parser.add_argument(
"-s", "--schema", type=str, required=True, help="The schema to use."
)
parser.add_argument(
"--uncomment",
required=False,
action="store_true",
help="Uncomment the settings inside the file.",
)
parser.add_argument(
"--should_fail",
required=False,
@ -28,22 +36,38 @@ def main():
file = args.file
schema = args.schema
should_fail = args.should_fail
uncomment = args.uncomment
with open(file) as f, open(schema) as s:
try:
validator = jsonschema_rs.JSONSchema.from_str(s.read())
except:
print("Coudln't create validator.")
print("Couldn't create validator.")
exit()
is_valid = validator.is_valid(toml.load(f))
if is_valid:
if uncomment:
read_file = f.read()
read_file = re.sub(r"^#([a-zA-Z\[])", r"\1", read_file, flags=re.MULTILINE)
read_file = re.sub(
r"^#(\s\s+)([a-zA-Z\[])", r"\2", read_file, flags=re.MULTILINE
)
print(f"uncommented file: \n{read_file}")
toml_str = toml.loads(read_file)
else:
toml_str = toml.load(f)
try:
validator.validate(toml_str)
if should_fail:
print("Fail!")
print("Fail! Should have errored.")
exit(1)
else:
print("All good!")
else:
except jsonschema_rs.ValidationError as err:
print(f"Caught error: `{err}`")
print(traceback.format_exc())
if should_fail:
print("Caught error, good!")
else:

View File

@ -545,13 +545,13 @@ pub const CONFIG_TEXT: &str = r#"# This is a default config file for bottom. Al
#whole_word = false
# Whether to make process searching use regex by default.
#regex = false
# Defaults to Celsius. Temperature is one of:
#temperature_type = "k"
#temperature_type = "f"
# The temperature unit. One of the following, defaults to "c" for Celsius:
#temperature_type = "c"
#temperature_type = "kelvin"
#temperature_type = "fahrenheit"
#temperature_type = "celsius"
##temperature_type = "k"
##temperature_type = "f"
##temperature_type = "kelvin"
##temperature_type = "fahrenheit"
##temperature_type = "celsius"
# The default time interval (in milliseconds).
#default_time_value = "60s"
# The time delta on each zoom in/out action (in milliseconds).
@ -606,7 +606,7 @@ pub const CONFIG_TEXT: &str = r#"# This is a default config file for bottom. Al
#[processes]
# The columns shown by the process widget. The following columns are supported:
# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMEM%", "GPU%"]
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"]
# CPU widget configuration
#[cpu]

View File

@ -278,28 +278,47 @@ fn create_collection_thread(
})
}
#[cfg(feature = "generate_schema")]
fn generate_schema() -> anyhow::Result<()> {
let mut schema = schemars::schema_for!(crate::options::config::ConfigV1);
{
use itertools::Itertools;
use strum::VariantArray;
let proc_columns = schema.definitions.get_mut("ProcColumn").unwrap();
match proc_columns {
schemars::schema::Schema::Object(proc_columns) => {
let enums = proc_columns.enum_values.as_mut().unwrap();
*enums = options::config::process::ProcColumn::VARIANTS
.iter()
.flat_map(|var| var.get_schema_names())
.map(|v| serde_json::Value::String(v.to_string()))
.dedup()
.collect();
}
_ => anyhow::bail!("missing proc columns definition"),
}
}
let metadata = schema.schema.metadata.as_mut().unwrap();
metadata.id = Some(
"https://github.com/ClementTsang/bottom/blob/main/schema/nightly/bottom.json".to_string(),
);
metadata.description =
Some("https://clementtsang.github.io/bottom/nightly/configuration/config-file".to_string());
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
Ok(())
}
fn main() -> anyhow::Result<()> {
// let _profiler = dhat::Profiler::new_heap();
let args = args::get_args();
#[cfg(feature = "generate_schema")]
{
if args.other.generate_schema {
let mut schema = schemars::schema_for!(crate::options::config::ConfigV1);
let metadata = schema.schema.metadata.as_mut().unwrap();
metadata.id = Some(
"https://github.com/ClementTsang/bottom/blob/main/schema/nightly/bottom.json"
.to_string(),
);
metadata.description = Some(
"https://clementtsang.github.io/bottom/nightly/configuration/config-file"
.to_string(),
);
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
return Ok(());
}
if args.other.generate_schema {
return generate_schema();
}
#[cfg(feature = "logging")]

View File

@ -183,7 +183,9 @@ pub fn init_app(
if cfg.columns.is_empty() {
None
} else {
Some(IndexSet::from_iter(cfg.columns.clone()))
Some(IndexSet::from_iter(
cfg.columns.iter().map(ProcWidgetColumn::from),
))
}
})
};

View File

@ -2,9 +2,11 @@ use std::borrow::Cow;
use serde::{Deserialize, Serialize};
/// Colour configuration.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
pub struct ColoursConfig {
// TODO: Make these an enum instead.
pub table_header_color: Option<Cow<'static, str>>,
pub all_cpu_color: Option<Cow<'static, str>>,
pub avg_cpu_color: Option<Cow<'static, str>>,

View File

@ -12,7 +12,7 @@ pub enum CpuDefault {
Average,
}
/// Process column settings.
/// CPU column settings.
#[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
pub struct CpuConfig {

View File

@ -8,12 +8,121 @@ use crate::widgets::ProcWidgetColumn;
pub struct ProcessesConfig {
/// A list of process widget columns.
#[serde(default)]
pub columns: Vec<ProcWidgetColumn>,
pub columns: Vec<ProcColumn>,
}
/// A column in the process widget.
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "generate_schema",
derive(schemars::JsonSchema, strum::VariantArray)
)]
pub enum ProcColumn {
Pid,
Count,
Name,
Command,
CpuPercent,
Mem,
MemPercent,
Read,
Write,
TotalRead,
TotalWrite,
State,
User,
Time,
#[cfg(feature = "gpu")]
GpuMem,
#[cfg(feature = "gpu")]
GpuPercent,
}
impl ProcColumn {
/// An ugly hack to generate the JSON schema.
#[cfg(feature = "generate_schema")]
pub fn get_schema_names(&self) -> &[&'static str] {
match self {
ProcColumn::Pid => &["PID"],
ProcColumn::Count => &["Count"],
ProcColumn::Name => &["Name"],
ProcColumn::Command => &["Command"],
ProcColumn::CpuPercent => &["CPU%"],
ProcColumn::Mem => &["Mem"],
ProcColumn::MemPercent => &["Mem%"],
ProcColumn::Read => &["R/s", "Read", "Rps"],
ProcColumn::Write => &["W/s", "Write", "Wps"],
ProcColumn::TotalRead => &["T.Read", "TWrite"],
ProcColumn::TotalWrite => &["T.Write", "TRead"],
ProcColumn::State => &["State"],
ProcColumn::User => &["User"],
ProcColumn::Time => &["Time"],
#[cfg(feature = "gpu")]
ProcColumn::GpuMem => &["GMem", "GMem%"],
#[cfg(feature = "gpu")]
ProcColumn::GpuPercent => &["GPU%"],
}
}
}
impl<'de> Deserialize<'de> for ProcColumn {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?.to_lowercase();
match value.as_str() {
"cpu%" => Ok(ProcColumn::CpuPercent),
"mem" => Ok(ProcColumn::Mem),
"mem%" => Ok(ProcColumn::Mem),
"pid" => Ok(ProcColumn::Pid),
"count" => Ok(ProcColumn::Count),
"name" => Ok(ProcColumn::Name),
"command" => Ok(ProcColumn::Command),
"read" | "r/s" | "rps" => Ok(ProcColumn::Read),
"write" | "w/s" | "wps" => Ok(ProcColumn::Write),
"tread" | "t.read" => Ok(ProcColumn::TotalRead),
"twrite" | "t.write" => Ok(ProcColumn::TotalWrite),
"state" => Ok(ProcColumn::State),
"user" => Ok(ProcColumn::User),
"time" => Ok(ProcColumn::Time),
#[cfg(feature = "gpu")]
"gmem" | "gmem%" => Ok(ProcColumn::GpuMem),
#[cfg(feature = "gpu")]
"gpu%" => Ok(ProcColumn::GpuPercent),
_ => Err(serde::de::Error::custom("doesn't match any column type")),
}
}
}
impl From<&ProcColumn> for ProcWidgetColumn {
fn from(value: &ProcColumn) -> Self {
match value {
ProcColumn::Pid => ProcWidgetColumn::PidOrCount,
ProcColumn::Count => ProcWidgetColumn::PidOrCount,
ProcColumn::Name => ProcWidgetColumn::ProcNameOrCommand,
ProcColumn::Command => ProcWidgetColumn::ProcNameOrCommand,
ProcColumn::CpuPercent => ProcWidgetColumn::Cpu,
ProcColumn::Mem => ProcWidgetColumn::Mem,
ProcColumn::MemPercent => ProcWidgetColumn::Mem,
ProcColumn::Read => ProcWidgetColumn::ReadPerSecond,
ProcColumn::Write => ProcWidgetColumn::WritePerSecond,
ProcColumn::TotalRead => ProcWidgetColumn::TotalRead,
ProcColumn::TotalWrite => ProcWidgetColumn::TotalWrite,
ProcColumn::State => ProcWidgetColumn::State,
ProcColumn::User => ProcWidgetColumn::User,
ProcColumn::Time => ProcWidgetColumn::Time,
#[cfg(feature = "gpu")]
ProcColumn::GpuMem => ProcWidgetColumn::GpuMem,
#[cfg(feature = "gpu")]
ProcColumn::GpuPercent => ProcWidgetColumn::GpuUtil,
}
}
}
#[cfg(test)]
mod test {
use super::ProcessesConfig;
use super::{ProcColumn, ProcessesConfig};
use crate::widgets::ProcWidgetColumn;
#[test]
@ -23,6 +132,13 @@ mod test {
assert!(generated.columns.is_empty());
}
fn to_columns(columns: Vec<ProcColumn>) -> Vec<ProcWidgetColumn> {
columns
.iter()
.map(ProcWidgetColumn::from)
.collect::<Vec<_>>()
}
#[test]
fn process_column_settings() {
let config = r#"
@ -31,7 +147,7 @@ mod test {
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
assert_eq!(
generated.columns,
to_columns(generated.columns),
vec![
ProcWidgetColumn::Cpu,
ProcWidgetColumn::PidOrCount,
@ -58,18 +174,30 @@ mod test {
fn process_column_settings_3() {
let config = r#"columns = ["Twrite", "T.Write"]"#;
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
assert_eq!(generated.columns, vec![ProcWidgetColumn::TotalWrite; 2]);
assert_eq!(
to_columns(generated.columns),
vec![ProcWidgetColumn::TotalWrite; 2]
);
let config = r#"columns = ["Tread", "T.read"]"#;
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
assert_eq!(generated.columns, vec![ProcWidgetColumn::TotalRead; 2]);
assert_eq!(
to_columns(generated.columns),
vec![ProcWidgetColumn::TotalRead; 2]
);
let config = r#"columns = ["read", "rps", "r/s"]"#;
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
assert_eq!(generated.columns, vec![ProcWidgetColumn::ReadPerSecond; 3]);
assert_eq!(
to_columns(generated.columns),
vec![ProcWidgetColumn::ReadPerSecond; 3]
);
let config = r#"columns = ["write", "wps", "w/s"]"#;
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
assert_eq!(generated.columns, vec![ProcWidgetColumn::WritePerSecond; 3]);
assert_eq!(
to_columns(generated.columns),
vec![ProcWidgetColumn::WritePerSecond; 3]
);
}
}

View File

@ -9,7 +9,6 @@ use indexmap::IndexSet;
use itertools::Itertools;
pub use proc_widget_column::*;
pub use proc_widget_data::*;
use serde::{de::Error, Deserialize};
use sort_table::SortTableColumn;
use crate::{
@ -111,7 +110,6 @@ pub struct ProcTableConfig {
/// A hacky workaround for now.
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
pub enum ProcWidgetColumn {
PidOrCount,
ProcNameOrCommand,
@ -130,36 +128,6 @@ pub enum ProcWidgetColumn {
GpuUtil,
}
impl<'de> Deserialize<'de> for ProcWidgetColumn {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?.to_lowercase();
match value.as_str() {
"cpu%" => Ok(ProcWidgetColumn::Cpu),
"mem" => Ok(ProcWidgetColumn::Mem),
"mem%" => Ok(ProcWidgetColumn::Mem),
"pid" => Ok(ProcWidgetColumn::PidOrCount),
"count" => Ok(ProcWidgetColumn::PidOrCount),
"name" => Ok(ProcWidgetColumn::ProcNameOrCommand),
"command" => Ok(ProcWidgetColumn::ProcNameOrCommand),
"read" | "r/s" | "rps" => Ok(ProcWidgetColumn::ReadPerSecond),
"write" | "w/s" | "wps" => Ok(ProcWidgetColumn::WritePerSecond),
"tread" | "t.read" => Ok(ProcWidgetColumn::TotalRead),
"twrite" | "t.write" => Ok(ProcWidgetColumn::TotalWrite),
"state" => Ok(ProcWidgetColumn::State),
"user" => Ok(ProcWidgetColumn::User),
"time" => Ok(ProcWidgetColumn::Time),
#[cfg(feature = "gpu")]
"gmem" | "gmem%" => Ok(ProcWidgetColumn::GpuMem),
#[cfg(feature = "gpu")]
"gpu%" => Ok(ProcWidgetColumn::GpuUtil),
_ => Err(Error::custom("doesn't match any column type")),
}
}
}
// This is temporary. Switch back to `ProcColumn` later!
pub struct ProcWidgetState {