Skip to content

Commit 908fb6e

Browse files
jgreitemannthoughtpolice
authored andcommitted
config-schema: add test asserting schema defaults match default config
Extracts the `"default"` values from the schema and creates a synthetic TOML file holding all the defaults according to the schema. This is done through some `jq` magic and is not perfect but rather a best effort. If `jq` is not available, the test is skipped; in CI `jq` is required. The test then run `jj config get` in the test env for each key in that defaults file and compares the resulting value with the schema default. For a few keys, there are actually no defaults known to `jj config get`, because they are hard-coded or dynamic. These exceptions are intercepted and explained in the test.
1 parent d2c5438 commit 908fb6e

File tree

4 files changed

+167
-0
lines changed

4 files changed

+167
-0
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2025 The Jujutsu Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::process::Command;
16+
use std::process::Stdio;
17+
18+
use bstr::ByteSlice as _;
19+
use testutils::is_external_tool_installed;
20+
21+
/// Produce a synthetic TOML document containing a default config according to
22+
/// the schema.
23+
///
24+
/// Uses the `jq` program to extract "default" nodes from the schema's JSON
25+
/// document and construct a corresponding TOML file. This TOML document
26+
/// consists of a single table with fully-qualified dotted keys.
27+
///
28+
/// # Limitations
29+
/// Defaults in `"additionalProperties"` are ignored, as are those in
30+
/// `"definitions"` nodes (which might be referenced by `"$ref"`).
31+
///
32+
/// Defaults which are JSON object-valued are supported and are specified as
33+
/// multiple lines, descending into the object fields. Arrays of scalars or
34+
/// other arrays are also supported (as they have the same representation in
35+
/// JSON and TOML), but arrays of objects are not.
36+
///
37+
/// When `jq` is not available on the system, returns `None`. This allows the
38+
/// caller to decide how to handle this.
39+
///
40+
/// # Panics
41+
/// Panics for all other error conditions related to
42+
/// - process spawning,
43+
/// - errors from running the jq program,
44+
/// - non-UTF-8 encoding,
45+
/// - parsing of the TOML output from `jq`.
46+
pub fn default_toml_from_schema() -> Option<toml_edit::DocumentMut> {
47+
const JQ_PROGRAM: &str = r#"
48+
paths as $p
49+
| select($p | any(. == "default") and all(type == "string" and . != "additionalProperties" and . != "definitions"))
50+
| getpath($p)
51+
| select(type != "object")
52+
| tojson as $v
53+
| $p
54+
| map(select(. != "properties" and . != "default")
55+
| if test("^[A-Za-z0-9_-]+$") then . else "'\(.)'" end)
56+
| join(".") as $k
57+
| "\($k)=\($v)"
58+
"#;
59+
60+
if !is_external_tool_installed("jq") {
61+
return None;
62+
}
63+
64+
let output = Command::new("jq")
65+
.args(["-r", JQ_PROGRAM, "src/config-schema.json"])
66+
.stdout(Stdio::piped())
67+
.stderr(Stdio::piped())
68+
.spawn()
69+
.unwrap()
70+
.wait_with_output()
71+
.unwrap();
72+
73+
if output.status.success() {
74+
Some(
75+
toml_edit::ImDocument::parse(String::from_utf8(output.stdout).unwrap())
76+
.unwrap()
77+
.into_mut(),
78+
)
79+
} else {
80+
panic!(
81+
"failed to extract default TOML from schema using `jq`: exit code {}:\n{}",
82+
output.status,
83+
output.stderr.to_str_lossy(),
84+
);
85+
}
86+
}

cli/tests/common/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
// limitations under the License.
1414

1515
mod command_output;
16+
mod config_schema_defaults;
1617
mod test_environment;
1718

1819
pub use self::command_output::CommandOutput;
20+
pub use self::config_schema_defaults::default_toml_from_schema;
1921
pub use self::test_environment::TestEnvironment;
2022
pub use self::test_environment::TestWorkDir;
2123

cli/tests/test_config_command.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ use std::env::join_paths;
1616
use std::path::PathBuf;
1717

1818
use indoc::indoc;
19+
use itertools::Itertools as _;
1920
use regex::Regex;
2021

22+
use crate::common::default_toml_from_schema;
2123
use crate::common::fake_editor_path;
2224
use crate::common::force_interactive;
2325
use crate::common::to_toml_value;
@@ -1256,6 +1258,82 @@ fn test_config_get() {
12561258
");
12571259
}
12581260

1261+
#[test]
1262+
fn test_config_get_yields_values_consistent_with_schema_defaults() {
1263+
let test_env = TestEnvironment::default();
1264+
let get_true_default = move |key: &str| {
1265+
let output = test_env.run_jj_in(".", ["config", "get", key]).success();
1266+
let output_doc =
1267+
toml_edit::ImDocument::parse(format!("test={}", output.stdout.normalized()))
1268+
.unwrap_or_else(|_| {
1269+
// Unfortunately for this test, `config get` is "lossy" and does not print
1270+
// quoted strings. This means that e.g. `false` and `"false"` are not
1271+
// distinguishable. If value couldn't be parsed, it's probably a string, so
1272+
// let's parse its Debug string instead.
1273+
toml_edit::ImDocument::parse(format!(
1274+
"test={:?}",
1275+
output.stdout.normalized().trim()
1276+
))
1277+
.unwrap()
1278+
});
1279+
output_doc.get("test").unwrap().as_value().unwrap().clone()
1280+
};
1281+
1282+
let Some(schema_defaults) = default_toml_from_schema() else {
1283+
testutils::ensure_running_outside_ci("`jq` must be in the PATH");
1284+
eprintln!("Skipping test because jq is not installed on the system");
1285+
return;
1286+
};
1287+
1288+
for (key, schema_default) in schema_defaults.as_table().get_values() {
1289+
let key = key.iter().join(".");
1290+
match key.as_str() {
1291+
// These keys technically don't have a default value, but they exhibit a default
1292+
// behavior consistent with the value claimed by the schema. When these defaults are
1293+
// used, a hint is printed to stdout.
1294+
"ui.default-command" => insta::assert_snapshot!(schema_default, @r#""log""#),
1295+
"ui.diff-editor" => insta::assert_snapshot!(schema_default, @r#"":builtin""#),
1296+
"ui.merge-editor" => insta::assert_snapshot!(schema_default, @r#"":builtin""#),
1297+
"git.fetch" => insta::assert_snapshot!(schema_default, @r#""origin""#),
1298+
"git.push" => insta::assert_snapshot!(schema_default, @r#""origin""#),
1299+
1300+
// When no `short-prefixes` revset is explicitly configured, the revset for `log` is
1301+
// used instead, even if that has a value different from the default. The schema
1302+
// represents this behavior with a symbolic default value.
1303+
"revsets.short-prefixes" => {
1304+
insta::assert_snapshot!(schema_default, @r#""<revsets.log>""#);
1305+
}
1306+
1307+
// The default for `ui.pager` is a table; `ui.pager.command` is an array and `jj config
1308+
// get` currently cannot print that. The schema default omits the env variable
1309+
// `LESSCHARSET` and gives the default as a plain string.
1310+
"ui.pager" => insta::assert_snapshot!(schema_default, @r#""less -FRX""#),
1311+
1312+
// The `immutable_heads()` revset actually defaults to `builtin_immutable_heads()` but
1313+
// this would be a poor starting point for a custom revset, so the schema "inlines"
1314+
// `builtin_immutable_heads()`.
1315+
"revset-aliases.'immutable_heads()'" => {
1316+
let builtin_default =
1317+
get_true_default("revset-aliases.'builtin_immutable_heads()'");
1318+
assert!(
1319+
builtin_default.to_string() == schema_default.to_string(),
1320+
"{key}: the schema claims a default ({schema_default}) which is different \
1321+
from what builtin_immutable_heads() resolves to ({builtin_default})"
1322+
);
1323+
}
1324+
1325+
_ => {
1326+
let true_default = get_true_default(&key);
1327+
assert!(
1328+
true_default.to_string() == schema_default.to_string(),
1329+
"{key}: true default value ({true_default}) is not consistent with default \
1330+
claimed by schema ({schema_default})"
1331+
);
1332+
}
1333+
}
1334+
}
1335+
}
1336+
12591337
#[test]
12601338
fn test_config_path_syntax() {
12611339
let test_env = TestEnvironment::default();

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
git
8080

8181
# for schema tests
82+
jq
8283
taplo
8384
];
8485

0 commit comments

Comments
 (0)