mirror of
https://github.com/nushell/nushell.git
synced 2025-08-15 00:02:35 +02:00
to <format>
: preserve round float numbers' type (#16016)
- fixes #16011 # Description `Display` implementation for `f64` omits the decimal part for round numbers, and by using it we did the same. This affected: - conversions to delimited formats: `csv`, `tsv` - textual formats: `html`, `md`, `text` - pretty printed `json` (`--raw` was unaffected) - how single float values are displayed in the REPL > [!TIP] > This PR fixes our existing json pretty printing implementation. > We can likely switch to using serde_json's impl using its PrettyFormatter which allows arbitrary indent strings. # User-Facing Changes - Round trips through `csv`, `tsv`, and `json` preserve the type of round floats. - It's always clear whether a number is an integer or a float in the REPL ```nushell 4 / 2 # => 2 # before: is this an int or a float? 4 / 2 # => 2.0 # after: clearly a float ``` # Tests + Formatting Adjusted tests for the new behavior. - 🟢 toolkit fmt - 🟢 toolkit clippy - 🟢 toolkit test - 🟢 toolkit test stdlib # After Submitting N/A --------- Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com>
This commit is contained in:
@ -1,201 +1,97 @@
|
||||
use fancy_regex::Regex;
|
||||
use nu_json::Value;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn txt(text: &str) -> String {
|
||||
let out = String::from_utf8_lossy(text.as_bytes());
|
||||
fn txt(text: String) -> String {
|
||||
let out = text;
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
out.replace("\r\n", "").replace('\n', "")
|
||||
out.replace("\r\n", "\n")
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
out.to_string()
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn hjson_expectations() -> PathBuf {
|
||||
nu_test_support::fs::assets().join("nu_json").into()
|
||||
// This test will fail if/when `nu_test_support::fs::assets()`'s return value changes.
|
||||
#[rstest]
|
||||
fn assert_rstest_finds_assets(#[files("../../tests/assets/nu_json")] rstest_supplied: PathBuf) {
|
||||
// rstest::files runs paths through `fs::canonicalize`, which:
|
||||
// > On Windows, this converts the path to use extended length path syntax
|
||||
// So we make sure to canonicalize both paths.
|
||||
assert_eq!(
|
||||
fs::canonicalize(rstest_supplied).unwrap(),
|
||||
fs::canonicalize(nu_test_support::fs::assets().join("nu_json")).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
fn get_test_content(name: &str) -> io::Result<String> {
|
||||
let expectations = hjson_expectations();
|
||||
|
||||
let mut p = format!("{}/{}_test.hjson", expectations.display(), name);
|
||||
|
||||
if !Path::new(&p).exists() {
|
||||
p = format!("{}/{}_test.json", expectations.display(), name);
|
||||
}
|
||||
|
||||
fs::read_to_string(&p)
|
||||
#[rstest]
|
||||
fn test_hjson_fails(#[files("../../tests/assets/nu_json/fail*_test.*")] file: PathBuf) {
|
||||
let contents = fs::read_to_string(file).unwrap();
|
||||
let data: nu_json::Result<Value> = nu_json::from_str(&contents);
|
||||
assert!(data.is_err());
|
||||
}
|
||||
|
||||
fn get_result_content(name: &str) -> io::Result<(String, String)> {
|
||||
let expectations = hjson_expectations();
|
||||
#[rstest]
|
||||
fn test_hjson(
|
||||
#[files("../../tests/assets/nu_json/*_test.*")]
|
||||
#[exclude("fail*")]
|
||||
test_file: PathBuf,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let name = test_file
|
||||
.file_stem()
|
||||
.and_then(|x| x.to_str())
|
||||
.and_then(|x| x.strip_suffix("_test"))
|
||||
.unwrap();
|
||||
|
||||
let p1 = format!("{}/{}_result.json", expectations.display(), name);
|
||||
let p2 = format!("{}/{}_result.hjson", expectations.display(), name);
|
||||
let data: Value = nu_json::from_str(fs::read_to_string(&test_file)?.as_str())?;
|
||||
|
||||
Ok((fs::read_to_string(p1)?, fs::read_to_string(p2)?))
|
||||
let r_json = get_content(get_result_path(&test_file, "json").as_deref().unwrap())?;
|
||||
// let r_hjson = get_content(get_result_path(&test_file, "hjson").as_deref().unwrap())?;
|
||||
let r_hjson = r_json.as_str();
|
||||
|
||||
let actual_json = serde_json::to_string_pretty(&data).map(get_fix(name))?;
|
||||
let actual_hjson = nu_json::to_string(&data).map(txt)?;
|
||||
|
||||
assert_eq!(r_json, actual_json);
|
||||
assert_eq!(r_hjson, actual_hjson);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
macro_rules! run_test {
|
||||
// {{ is a workaround for rust stable
|
||||
($v: ident, $list: expr, $fix: expr) => {{
|
||||
let name = stringify!($v);
|
||||
$list.push(format!("{}_test", name));
|
||||
println!("- running {}", name);
|
||||
let should_fail = name.starts_with("fail");
|
||||
let test_content = get_test_content(name).unwrap();
|
||||
let data: nu_json::Result<Value> = nu_json::from_str(&test_content);
|
||||
assert!(should_fail == data.is_err());
|
||||
fn get_result_path(test_file: &Path, ext: &str) -> Option<PathBuf> {
|
||||
let name = test_file
|
||||
.file_stem()
|
||||
.and_then(|x| x.to_str())
|
||||
.and_then(|x| x.strip_suffix("_test"))?;
|
||||
|
||||
if !should_fail {
|
||||
let udata = data.unwrap();
|
||||
let (rjson, rhjson) = get_result_content(name).unwrap();
|
||||
let rjson = txt(&rjson);
|
||||
let rhjson = txt(&rhjson);
|
||||
let actual_hjson = nu_json::to_string(&udata).unwrap();
|
||||
let actual_hjson = txt(&actual_hjson);
|
||||
let actual_json = $fix(serde_json::to_string_pretty(&udata).unwrap());
|
||||
let actual_json = txt(&actual_json);
|
||||
// nu_json::to_string now outputs json instead of hjson!
|
||||
if rjson != actual_hjson {
|
||||
println!(
|
||||
"{:?}\n---hjson expected\n{}\n---hjson actual\n{}\n---\n",
|
||||
name, rhjson, actual_hjson
|
||||
);
|
||||
}
|
||||
if rjson != actual_json {
|
||||
println!(
|
||||
"{:?}\n---json expected\n{}\n---json actual\n{}\n---\n",
|
||||
name, rjson, actual_json
|
||||
);
|
||||
}
|
||||
assert!(rjson == actual_hjson && rjson == actual_json);
|
||||
}
|
||||
}};
|
||||
Some(test_file.with_file_name(format!("{name}_result.{ext}")))
|
||||
}
|
||||
|
||||
fn get_content(file: &Path) -> io::Result<String> {
|
||||
fs::read_to_string(file).map(txt)
|
||||
}
|
||||
|
||||
// add fixes where rust's json differs from javascript
|
||||
fn get_fix(s: &str) -> fn(String) -> String {
|
||||
fn remove_negative_zero(json: String) -> String {
|
||||
json.replace(" -0,", " 0,")
|
||||
}
|
||||
|
||||
fn std_fix(json: String) -> String {
|
||||
// serde_json serializes integers with a superfluous .0 suffix
|
||||
let re = Regex::new(r"(?m)(?P<d>\d)\.0(?P<s>,?)$").unwrap();
|
||||
re.replace_all(&json, "$d$s").to_string()
|
||||
}
|
||||
fn positive_exp_add_sign(json: String) -> String {
|
||||
json.replace("1.23456789e34", "1.23456789e+34")
|
||||
.replace("2.3456789012e76", "2.3456789012e+76")
|
||||
}
|
||||
|
||||
fn fix_kan(json: String) -> String {
|
||||
std_fix(json).replace(" -0,", " 0,")
|
||||
}
|
||||
|
||||
fn fix_pass1(json: String) -> String {
|
||||
std_fix(json)
|
||||
.replace("1.23456789e34", "1.23456789e+34")
|
||||
.replace("2.3456789012e76", "2.3456789012e+76")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hjson() {
|
||||
let mut done: Vec<String> = Vec::new();
|
||||
|
||||
println!();
|
||||
run_test!(charset, done, std_fix);
|
||||
run_test!(comments, done, std_fix);
|
||||
run_test!(empty, done, std_fix);
|
||||
run_test!(failCharset1, done, std_fix);
|
||||
run_test!(failJSON02, done, std_fix);
|
||||
run_test!(failJSON05, done, std_fix);
|
||||
run_test!(failJSON06, done, std_fix);
|
||||
run_test!(failJSON07, done, std_fix);
|
||||
run_test!(failJSON08, done, std_fix);
|
||||
run_test!(failJSON10, done, std_fix);
|
||||
run_test!(failJSON11, done, std_fix);
|
||||
run_test!(failJSON12, done, std_fix);
|
||||
run_test!(failJSON13, done, std_fix);
|
||||
run_test!(failJSON14, done, std_fix);
|
||||
run_test!(failJSON15, done, std_fix);
|
||||
run_test!(failJSON16, done, std_fix);
|
||||
run_test!(failJSON17, done, std_fix);
|
||||
run_test!(failJSON19, done, std_fix);
|
||||
run_test!(failJSON20, done, std_fix);
|
||||
run_test!(failJSON21, done, std_fix);
|
||||
run_test!(failJSON22, done, std_fix);
|
||||
run_test!(failJSON23, done, std_fix);
|
||||
run_test!(failJSON24, done, std_fix);
|
||||
run_test!(failJSON26, done, std_fix);
|
||||
run_test!(failJSON28, done, std_fix);
|
||||
run_test!(failJSON29, done, std_fix);
|
||||
run_test!(failJSON30, done, std_fix);
|
||||
run_test!(failJSON31, done, std_fix);
|
||||
run_test!(failJSON32, done, std_fix);
|
||||
run_test!(failJSON33, done, std_fix);
|
||||
run_test!(failJSON34, done, std_fix);
|
||||
run_test!(failKey1, done, std_fix);
|
||||
run_test!(failKey2, done, std_fix);
|
||||
run_test!(failKey3, done, std_fix);
|
||||
run_test!(failKey4, done, std_fix);
|
||||
run_test!(failMLStr1, done, std_fix);
|
||||
run_test!(failObj1, done, std_fix);
|
||||
run_test!(failObj2, done, std_fix);
|
||||
run_test!(failObj3, done, std_fix);
|
||||
run_test!(failStr1a, done, std_fix);
|
||||
run_test!(failStr1b, done, std_fix);
|
||||
run_test!(failStr1c, done, std_fix);
|
||||
run_test!(failStr1d, done, std_fix);
|
||||
run_test!(failStr2a, done, std_fix);
|
||||
run_test!(failStr2b, done, std_fix);
|
||||
run_test!(failStr2c, done, std_fix);
|
||||
run_test!(failStr2d, done, std_fix);
|
||||
run_test!(failStr3a, done, std_fix);
|
||||
run_test!(failStr3b, done, std_fix);
|
||||
run_test!(failStr3c, done, std_fix);
|
||||
run_test!(failStr3d, done, std_fix);
|
||||
run_test!(failStr4a, done, std_fix);
|
||||
run_test!(failStr4b, done, std_fix);
|
||||
run_test!(failStr4c, done, std_fix);
|
||||
run_test!(failStr4d, done, std_fix);
|
||||
run_test!(failStr5a, done, std_fix);
|
||||
run_test!(failStr5b, done, std_fix);
|
||||
run_test!(failStr5c, done, std_fix);
|
||||
run_test!(failStr5d, done, std_fix);
|
||||
run_test!(failStr6a, done, std_fix);
|
||||
run_test!(failStr6b, done, std_fix);
|
||||
run_test!(failStr6c, done, std_fix);
|
||||
run_test!(failStr6d, done, std_fix);
|
||||
run_test!(kan, done, fix_kan);
|
||||
run_test!(keys, done, std_fix);
|
||||
run_test!(oa, done, std_fix);
|
||||
run_test!(pass1, done, fix_pass1);
|
||||
run_test!(pass2, done, std_fix);
|
||||
run_test!(pass3, done, std_fix);
|
||||
run_test!(pass4, done, std_fix);
|
||||
run_test!(passSingle, done, std_fix);
|
||||
run_test!(root, done, std_fix);
|
||||
run_test!(stringify1, done, std_fix);
|
||||
run_test!(strings, done, std_fix);
|
||||
run_test!(trail, done, std_fix);
|
||||
|
||||
// check if we include all assets
|
||||
let paths = fs::read_dir(hjson_expectations()).unwrap();
|
||||
|
||||
let all = paths
|
||||
.map(|item| String::from(item.unwrap().path().file_stem().unwrap().to_str().unwrap()))
|
||||
.filter(|x| x.contains("_test"));
|
||||
|
||||
let missing = all
|
||||
.into_iter()
|
||||
.filter(|x| !done.iter().any(|y| x == y))
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
if !missing.is_empty() {
|
||||
for item in missing {
|
||||
println!("missing: {}", item);
|
||||
}
|
||||
panic!();
|
||||
match s {
|
||||
"kan" => remove_negative_zero,
|
||||
"pass1" => positive_exp_add_sign,
|
||||
_ => std::convert::identity,
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user