From ec67809303b1dc409c46f600ca7e4f56aeeee417 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 11 Jan 2025 09:29:32 -0600 Subject: [PATCH] Add in hidden doctests for our format API. --- lexical-util/src/format_builder.rs | 775 ++++++++++++++++++++++++++++- scripts/docs.py | 96 +++- 2 files changed, 858 insertions(+), 13 deletions(-) diff --git a/lexical-util/src/format_builder.rs b/lexical-util/src/format_builder.rs index b1e72b73..121af294 100644 --- a/lexical-util/src/format_builder.rs +++ b/lexical-util/src/format_builder.rs @@ -553,6 +553,11 @@ impl NumberFormatBuilder { // GETTERS + // NOTE: This contains a lot of tests for our tables that would spam our + // documentation, so we hide them internally. See `scripts/docs.py` for + // how the tests are generated and run. This assumes the `format` and + // `radix` features are enabled. + /// Get the digit separator for the number format. /// /// Digit separators are frequently used in number literals to group @@ -791,7 +796,7 @@ impl NumberFormatBuilder { /// | `.` | ✔️ | /// | `e10` | ✔️ | /// | `.e10` | ✔️ | - /// | `` | ❌ | + /// | | ❌ | /// /// # Used For /// @@ -1414,6 +1419,26 @@ impl NumberFormatBuilder { /// /// - Parse Float /// - Parse Integer + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn digit_separator(mut self, character: OptionU8) -> Self { @@ -1455,6 +1480,50 @@ impl NumberFormatBuilder { /// - Parse Integer /// - Write Float /// - Write Integer + /// + /// #[inline(always)] #[cfg(feature = "power-of-two")] pub const fn mantissa_radix(mut self, radix: u8) -> Self { @@ -1499,6 +1568,43 @@ impl NumberFormatBuilder { /// /// - Parse Float /// - Parse Integer + /// + /// #[inline(always)] #[cfg(feature = "power-of-two")] pub const fn exponent_radix(mut self, radix: OptionU8) -> Self { @@ -1529,6 +1635,25 @@ impl NumberFormatBuilder { /// /// - Parse Float /// - Parse Integer + /// + /// #[inline(always)] #[cfg(all(feature = "power-of-two", feature = "format"))] pub const fn base_prefix(mut self, base_prefix: OptionU8) -> Self { @@ -1557,6 +1682,25 @@ impl NumberFormatBuilder { /// /// - Parse Float /// - Parse Integer + /// + /// #[inline(always)] #[cfg(all(feature = "power-of-two", feature = "format"))] pub const fn base_suffix(mut self, base_suffix: OptionU8) -> Self { @@ -1576,10 +1720,25 @@ impl NumberFormatBuilder { /// | `0.1` | ✔️ | /// | `1` | ✔️ | /// | `.1` | ❌ | + /// | `1.` | ❌ | + /// | | ❌ | /// - /// # Used For + /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn required_integer_digits(mut self, flag: bool) -> Self { @@ -1597,12 +1756,28 @@ impl NumberFormatBuilder { /// | Input | Valid? | /// |:-:|:-:| /// | `1.1` | ✔️ | + /// | `0.1` | ✔️ | /// | `1` | ✔️ | + /// | `.1` | ✔️ | /// | `1.` | ❌ | + /// | | ❌ | /// /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn required_fraction_digits(mut self, flag: bool) -> Self { @@ -1627,6 +1802,18 @@ impl NumberFormatBuilder { /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn required_exponent_digits(mut self, flag: bool) -> Self { @@ -1645,13 +1832,26 @@ impl NumberFormatBuilder { /// |:-:|:-:| /// | `1.1` | ✔️ | /// | `.` | ✔️ | - /// | `e10` | ✔️ | - /// | `.e10` | ✔️ | - /// | `` | ❌ | + /// | `e10` | ❌ | + /// | `.e10` | ❌ | + /// | | ❌ | /// /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn required_mantissa_digits(mut self, flag: bool) -> Self { @@ -1670,16 +1870,33 @@ impl NumberFormatBuilder { /// |:-:|:-:| /// | `1.1` | ✔️ | /// | `1.1e3` | ✔️ | + /// | `1.1e` | ✔️ | /// | `0.1` | ✔️ | /// | `.1` | ❌ | /// | `1.` | ❌ | /// | `e10` | ❌ | /// | `.1e10` | ❌ | - /// | `` | ❌ | + /// | | ❌ | /// /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn required_digits(mut self, flag: bool) -> Self { @@ -1707,6 +1924,21 @@ impl NumberFormatBuilder { /// - Parse Float /// - Parse Integer /// - Write Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn no_positive_mantissa_sign(mut self, flag: bool) -> Self { @@ -1731,6 +1963,29 @@ impl NumberFormatBuilder { /// - Parse Float /// - Parse Integer /// - Write Float + /// - Write Integer + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn required_mantissa_sign(mut self, flag: bool) -> Self { @@ -1755,6 +2010,25 @@ impl NumberFormatBuilder { /// /// - Parse Float /// - Write Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn no_exponent_notation(mut self, flag: bool) -> Self { @@ -1778,6 +2052,21 @@ impl NumberFormatBuilder { /// /// - Parse Float /// - Write Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn no_positive_exponent_sign(mut self, flag: bool) -> Self { @@ -1801,6 +2090,21 @@ impl NumberFormatBuilder { /// /// - Parse Float /// - Write Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn required_exponent_sign(mut self, flag: bool) -> Self { @@ -1824,6 +2128,19 @@ impl NumberFormatBuilder { /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn no_exponent_without_fraction(mut self, flag: bool) -> Self { @@ -1842,11 +2159,28 @@ impl NumberFormatBuilder { /// | `NaN` | ❌ | /// | `inf` | ❌ | /// | `-Infinity` | ❌ | - /// | `1.1e` | ✔️ | + /// | `1.1e3` | ✔️ | /// /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn no_special(mut self, flag: bool) -> Self { @@ -1859,9 +2193,37 @@ impl NumberFormatBuilder { /// If set to [`true`], then `NaN` and `nan` are treated as the same value /// ([Not a Number][f64::NAN]). Defaults to [`false`]. /// + /// # Examples + /// + /// | Input | Valid? | + /// |:-:|:-:| + /// | `nan` | ❌ | + /// | `NaN` | ✔️ | + /// | `inf` | ✔️ | + /// | `Inf` | ❌ | + /// /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn case_sensitive_special(mut self, flag: bool) -> Self { @@ -1884,6 +2246,23 @@ impl NumberFormatBuilder { /// # Used For /// /// - Parse Integer + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn no_integer_leading_zeros(mut self, flag: bool) -> Self { @@ -1910,6 +2289,25 @@ impl NumberFormatBuilder { /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn no_float_leading_zeros(mut self, flag: bool) -> Self { @@ -1934,6 +2332,25 @@ impl NumberFormatBuilder { /// /// - Parse Float /// - Write Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn required_exponent_notation(mut self, flag: bool) -> Self { @@ -1946,9 +2363,28 @@ impl NumberFormatBuilder { /// If set to [`true`], then the exponent character `e` would be considered /// the different from `E`. Defaults to [`false`]. /// + /// # Examples + /// + /// | Input | Valid? | + /// |:-:|:-:| + /// | `1.1` | ✔️ | + /// | `1.1e3` | ✔️ | + /// | `1.1E3` | ❌ | + /// /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn case_sensitive_exponent(mut self, flag: bool) -> Self { @@ -1961,10 +2397,39 @@ impl NumberFormatBuilder { /// If set to [`true`], then the base prefix `x` would be considered the /// different from `X`. Defaults to [`false`]. /// + /// # Examples + /// + /// Using a base prefix of `x`. + /// + /// | Input | Valid? | + /// |:-:|:-:| + /// | `0x1` | ✔️ | + /// | `0X1` | ❌ | + /// | `1` | ✔️ | + /// | `1x` | ❌ | + /// /// # Used For /// /// - Parse Float /// - Parse Integer + /// + /// #[inline(always)] #[cfg(all(feature = "power-of-two", feature = "format"))] pub const fn case_sensitive_base_prefix(mut self, flag: bool) -> Self { @@ -1977,10 +2442,39 @@ impl NumberFormatBuilder { /// If set to [`true`], then the base suffix `x` would be considered the /// different from `X`. Defaults to [`false`]. /// + /// # Examples + /// + /// Using a base prefix of `x`. + /// + /// | Input | Valid? | + /// |:-:|:-:| + /// | `1` | ✔️ | + /// | `1x` | ✔️ | + /// | `1X` | ❌ | + /// | `1d` | ❌ | + /// /// # Used For /// /// - Parse Float /// - Parse Integer + /// + /// #[inline(always)] #[cfg(all(feature = "power-of-two", feature = "format"))] pub const fn case_sensitive_base_suffix(mut self, flag: bool) -> Self { @@ -2010,6 +2504,26 @@ impl NumberFormatBuilder { /// /// - Parse Float /// - Parse Integer + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn integer_internal_digit_separator(mut self, flag: bool) -> Self { @@ -2038,6 +2552,20 @@ impl NumberFormatBuilder { /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn fraction_internal_digit_separator(mut self, flag: bool) -> Self { @@ -2066,6 +2594,20 @@ impl NumberFormatBuilder { /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn exponent_internal_digit_separator(mut self, flag: bool) -> Self { @@ -2114,6 +2656,26 @@ impl NumberFormatBuilder { /// /// - Parse Float /// - Parse Integer + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn integer_leading_digit_separator(mut self, flag: bool) -> Self { @@ -2133,7 +2695,7 @@ impl NumberFormatBuilder { /// | Input | Valid? | /// |:-:|:-:| /// | `1.1` | ✔️ | - /// | `1._` | ❌ | + /// | `1._` | ✔️ | /// | `1.1_1` | ❌ | /// | `1.1_` | ❌ | /// | `1._1` | ✔️ | @@ -2141,6 +2703,20 @@ impl NumberFormatBuilder { /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn fraction_leading_digit_separator(mut self, flag: bool) -> Self { @@ -2168,6 +2744,20 @@ impl NumberFormatBuilder { /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn exponent_leading_digit_separator(mut self, flag: bool) -> Self { @@ -2216,6 +2806,26 @@ impl NumberFormatBuilder { /// /// - Parse Float /// - Parse Integer + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn integer_trailing_digit_separator(mut self, flag: bool) -> Self { @@ -2234,7 +2844,7 @@ impl NumberFormatBuilder { /// | Input | Valid? | /// |:-:|:-:| /// | `1.1` | ✔️ | - /// | `1._` | ❌ | + /// | `1._` | ✔️ | /// | `1.1_1` | ❌ | /// | `1.1_` | ✔️ | /// | `1._1` | ❌ | @@ -2242,6 +2852,20 @@ impl NumberFormatBuilder { /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn fraction_trailing_digit_separator(mut self, flag: bool) -> Self { @@ -2269,6 +2893,20 @@ impl NumberFormatBuilder { /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn exponent_trailing_digit_separator(mut self, flag: bool) -> Self { @@ -2302,10 +2940,47 @@ impl NumberFormatBuilder { /// digit separators (leading, trailing, internal) are allowed in the /// integer. Defaults to [`false`]. /// + /// # Examples + /// + /// Using a digit separator of `_` with only internal integer digit + /// separators being valid. + /// + /// | Input | Valid? | + /// |:-:|:-:| + /// | `1` | ✔️ | + /// | `_` | ❌ | + /// | `1_1` | ✔️ | + /// | `1__1` | ✔️ | + /// | `1_` | ❌ | + /// | `_1` | ❌ | + /// /// # Used For /// /// - Parse Float /// - Parse Integer + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn integer_consecutive_digit_separator(mut self, flag: bool) -> Self { @@ -2319,9 +2994,39 @@ impl NumberFormatBuilder { /// digit separators (leading, trailing, internal) are allowed in the /// fraction. Defaults to [`false`]. /// + /// # Examples + /// + /// Using a digit separator of `_` with only internal fraction digit + /// separators being valid. + /// + /// | Input | Valid? | + /// |:-:|:-:| + /// | `1.1` | ✔️ | + /// | `1._` | ❌ | + /// | `1.1_1` | ✔️ | + /// | `1.1__1` | ✔️ | + /// | `1.1_` | ❌ | + /// | `1._1` | ❌ | + /// /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn fraction_consecutive_digit_separator(mut self, flag: bool) -> Self { @@ -2335,9 +3040,39 @@ impl NumberFormatBuilder { /// digit separators (leading, trailing, internal) are allowed in the /// exponent. Defaults to [`false`]. /// + /// # Examples + /// + /// Using a digit separator of `_` with only internal exponent digit + /// separators being valid. + /// + /// | Input | Valid? | + /// |:-:|:-:| + /// | `1.1e1` | ✔️ | + /// | `1.1e_` | ❌ | + /// | `1.1e1_1` | ✔️ | + /// | `1.1e1__1` | ✔️ | + /// | `1.1e1_` | ❌ | + /// | `1.1e_1` | ❌ | + /// /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn exponent_consecutive_digit_separator(mut self, flag: bool) -> Self { @@ -2369,9 +3104,31 @@ impl NumberFormatBuilder { /// separators for any special floats: for example, `N__a_N_` is considered /// the same as `NaN`. Defaults to [`false`]. /// + /// Using a digit separator of `_`. + /// + /// | Input | Valid? | + /// |:-:|:-:| + /// | `nan` | ✔️ | + /// | `na_n` | ✔️ | + /// | `na_n_` | ✔️ | + /// | `na_nx` | ❌ | + /// /// # Used For /// /// - Parse Float + /// + /// #[inline(always)] #[cfg(feature = "format")] pub const fn special_digit_separator(mut self, flag: bool) -> Self { diff --git a/scripts/docs.py b/scripts/docs.py index 88da36ae..783ad3d3 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -8,9 +8,14 @@ ''' import html.parser +import os +import re +import sys +import shutil import time import urllib.error import urllib.request +import subprocess from pathlib import Path # This is a hack for older Python versions @@ -99,10 +104,9 @@ def handle_data(self, data: str): _ = data -def main() -> None: - '''Run our validation code.''' +def validate_toml() -> None: + '''Validate our TOML files.''' - # get all our toml files for path in home_dir.rglob('**/*.toml'): # Bug fixes for Docker on Windows. We don't want dups anyway. if path.is_symlink(): @@ -116,7 +120,10 @@ def main() -> None: data = file.read() _ = tomllib.loads(data) - # get all our links + +def validate_links() -> None: + '''Validate all the links inside our build documentation.''' + parser = LinkParser() for path in target_dir.rglob('**/*.html'): # Bug fixes for Docker on Windows. We don't want dups anyway. @@ -130,5 +137,86 @@ def main() -> None: print(f'Processed and validated {len(parser.links)} links...') +cargo_toml = ''' +[package] +authors = ["Alex Huszagh "] +edition = "2021" +name = "lexical-format-doctests" +publish = false + +[workspace] +members = [] + +[dependencies.lexical-core] +path = "../../lexical-core" +features = ["format", "radix"] +''' + +test_prefix = ''' +#![allow(unused, dead_code)] + +use core::num; + +use lexical_core::*; + +const PF_OPTS: ParseFloatOptions = ParseFloatOptions::new(); +const PI_OPTS: ParseIntegerOptions = ParseIntegerOptions::new(); +const WF_OPTS: WriteFloatOptions = WriteFloatOptions::new(); +const WI_OPTS: WriteIntegerOptions = WriteIntegerOptions::new(); +''' + +test_rs = ''' +#[test] +pub fn test{index}() {{ + {test} +}} +''' + + +def validate_format() -> int: + '''Validate all the format features inside our docs.''' + + # read all our tests + with (home_dir / 'lexical-util' / 'src' / 'format_builder.rs').open(encoding='utf-8') as file: + data = file.read() + tests = [i.group(1) for i in re.finditer(r'', data, re.DOTALL)] + tests = [re.sub(r'(?:\A|[\r\n]+)\s*///?', '\n', i) for i in tests] + tests = [i.strip().removeprefix('```rust').removesuffix('```') for i in tests] + + # create a fake project inside target + proj_dir = home_dir / 'target' / 'format-doctest' + src_dir = proj_dir / 'src' + tests_dir = proj_dir / 'tests' + shutil.rmtree(proj_dir, ignore_errors=True) + proj_dir.mkdir(parents=True) + src_dir.mkdir() + tests_dir.mkdir() + + # create basic project + with (proj_dir / 'Cargo.toml').open(encoding='utf-8', mode='w') as file: + print(cargo_toml, file=file) + with (src_dir / 'lib.rs').open(encoding='utf-8', mode='w'): + pass + with (tests_dir / 'test.rs').open(encoding='utf-8', mode='w') as file: + print(test_prefix, file=file) + for index, test in enumerate(tests): + print(test_rs.format(index=index, test=test), file=file) + + # build our tests + cargo = os.environ.get('CARGO', 'cargo') + return subprocess.call(f'{cargo} test', cwd=proj_dir, shell=True) + + +def main() -> None: + '''Run our validation code.''' + + if 'SKIP_TOML' not in os.environ: + validate_toml() + if 'SKIP_LINKS' not in os.environ: + validate_links() + if 'SKIP_FORMAT' not in os.environ: + sys.exit(validate_format()) + + if __name__ == '__main__': main()