From 635839b463c4e5bc967db2796d421882ee9e0bf4 Mon Sep 17 00:00:00 2001 From: Alex Povel Date: Sat, 9 Nov 2024 17:57:21 +0100 Subject: [PATCH] test(readme): `FIX_README` - automatic fixing functionality --- CONTRIBUTING.md | 6 --- scripts/update-readme.py | 67 ----------------------------- tests/readme.rs | 93 +++++++++++++++++++++++----------------- 3 files changed, 54 insertions(+), 112 deletions(-) delete mode 100755 scripts/update-readme.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af84c50..5895282 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,17 +90,11 @@ tested](./tests/readme.rs). The testing of the `bash` snippets is pure Rust (no binary needed), making it platform-independent. The downside is that custom parsing is used. This has known warts. Those warts have workarounds: -- the README contains the full output of `srgn --help`, which is also tested against. - Run [`./scripts/update-readme.py`](./scripts/update-readme.py) to update this README - section automatically if you updated it. - the tests contain [**hard-coded names of CLI options**](https://github.com/alexpovel/srgn/blob/8ff54ee53ac0a53cdc4791b069648ee4511c7b94/tests/readme.rs#L494-L521). This is necessary as otherwise it'd be unclear if a string `--foo` should be a flag (no argument) or an option (an argument follows, `--foo bar`). -All these could be considered plain bugs in the custom parser. They would certainly -be fixable, given the time. - ### Custom macros to work around `clap` The needs of `srgn` go slightly beyond what `clap` offers out of the box. For this, diff --git a/scripts/update-readme.py b/scripts/update-readme.py deleted file mode 100755 index b82acba..0000000 --- a/scripts/update-readme.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import re -import subprocess -from difflib import unified_diff -from pathlib import Path - -ENCODING = "utf8" - - -def diff(a: str, b: str, filename: Path) -> str: - res = "\n".join( - unified_diff( - a.splitlines(), - b.splitlines(), - fromfile=str(filename), - tofile=str(filename), - ) - ) - return res - - -def update_markdown_file(file: Path) -> None: - content = file.read_text(encoding=ENCODING) - print(f"Got contents of {file}, {len(content)} long") - pattern = re.compile( - pattern=r"(?<=```console\n\$ srgn --help\n)(.*?)(?=```$)", - flags=re.DOTALL | re.MULTILINE, - ) - args = ["cargo", "run", "--", "--help"] - result = subprocess.run( - args=args, - capture_output=True, - text=True, - check=True, - ) - print(f"Successfully ran command: {args}") - - match_ = re.search(pattern, content) - assert match_ is not None, "Bad regex/README" - print(f"Match at: {match_}") - - new_content = re.sub(pattern, result.stdout, content) - - print("Got new file contents. Diff:") - print(diff(content, new_content, filename=file)) - input("Press Enter to confirm and write to file, CTRL+C to abort...") - file.write_text(new_content, encoding=ENCODING) - print("Wrote new file contents.") - - -def main(): - parser = argparse.ArgumentParser( - description="Update Markdown console code blocks with actual command output" - ) - parser.add_argument("file", help="Path to the Markdown file to process") - args = parser.parse_args() - - file = Path(args.file) - update_markdown_file(file) - print(f"Successfully updated {file}") - print("Done") - - -if __name__ == "__main__": - main() diff --git a/tests/readme.rs b/tests/readme.rs index b56b946..39934df 100644 --- a/tests/readme.rs +++ b/tests/readme.rs @@ -15,6 +15,7 @@ mod tests { use core::{fmt, panic}; use std::cell::RefCell; use std::collections::{HashMap, VecDeque}; + use std::error::Error; use std::io::Write; use std::mem::ManuallyDrop; use std::rc::Rc; @@ -40,7 +41,8 @@ mod tests { use unescape::unescape; const PROGRAM_NAME: &str = env!("CARGO_PKG_NAME"); - const DOCUMENT: &str = include_str!("../README.md"); + const PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/", "README.md"); + const FIX_ENV_VAR: &str = "FIX_README"; /// A flag, either short or long. /// @@ -738,10 +740,10 @@ mod tests { type Snippets = HashMap; - fn get_readme_snippets() -> Snippets { + fn get_readme_snippets(doc: &str) -> Snippets { let mut snippets = HashMap::new(); - map_on_markdown_codeblocks(DOCUMENT, |ncb| { + map_on_markdown_codeblocks(doc, |ncb| { if let Some((_language, options)) = ncb.info.split_once(' ') { let mut snippet = Snippet::default(); let options: BlockOptions = options.into(); @@ -774,10 +776,10 @@ mod tests { snippets } - fn get_readme_program_pipes(snippets: &Snippets) -> Vec { + fn get_readme_program_pipes(doc: &str, snippets: &Snippets) -> Vec { let mut pipes = Vec::new(); - map_on_markdown_codeblocks(DOCUMENT, |ncb| { + map_on_markdown_codeblocks(doc, |ncb| { let (language, options): (&str, Option) = ncb .info .split_once(' ') @@ -826,11 +828,12 @@ mod tests { } #[test] - fn test_readme_code_blocks() { - let _helper = TestHinter; + fn test_readme_code_blocks() -> Result<(), Box> { + let _hinter = TestHinter; - let snippets = get_readme_snippets(); - let pipes = get_readme_program_pipes(&snippets); + let contents = std::fs::read_to_string(PATH)?; + let snippets = get_readme_snippets(&contents); + let pipes = get_readme_program_pipes(&contents, &snippets); for pipe in pipes { let mut previous_stdin = None; @@ -875,37 +878,48 @@ mod tests { } if observed_stdout != expected_stdout { - // Write to files for easier inspection - let (mut obs_f, mut exp_f) = ( - ManuallyDrop::new(NamedTempFile::new().unwrap()), - ManuallyDrop::new(NamedTempFile::new().unwrap()), - ); - - obs_f.write_all(observed_stdout.as_bytes()).unwrap(); - exp_f.write_all(expected_stdout.as_bytes()).unwrap(); - - // Now panic as usual, for the usual output - assert_eq!( - // Get some more readable output diff compared to - // `assert::Command`'s `stdout()` function, for which diffing - // whitespace is very hard. - expected_stdout, - observed_stdout, - "Output differs; for inspection see observed stdout at '{}', expected stdout at '{}'", - obs_f.path().display(), - exp_f.path().display() - ); - - // Temporary files remain, they're not dropped. - - unreachable!(); + if std::env::var(FIX_ENV_VAR).as_deref() == Ok("1") { + let old_readme = std::fs::read_to_string(PATH)?; + // We'd REALLY want the exact byte ranges here for exact + // precision, but those aren't easily available. This + // naive replacement will fail in various scenarios. + let new_readme = old_readme.replace(&expected_stdout, &observed_stdout); + + assert!(old_readme != new_readme, "No changes made to {PATH}, bug."); + + std::fs::write(PATH, new_readme)?; + } else { + // Write to files for easier inspection + let (mut obs_f, mut exp_f) = ( + ManuallyDrop::new(NamedTempFile::new().unwrap()), + ManuallyDrop::new(NamedTempFile::new().unwrap()), + ); + + obs_f.write_all(observed_stdout.as_bytes()).unwrap(); + exp_f.write_all(expected_stdout.as_bytes()).unwrap(); + + // Now panic as usual, for the usual output + assert_eq!( + // Get some more readable output diff compared to + // `assert::Command`'s `stdout()` function, for which diffing + // whitespace is very hard. + expected_stdout, + observed_stdout, + "Output differs; for inspection see observed stdout at '{}', expected stdout at '{}'", + obs_f.path().display(), + exp_f.path().display() + ); + + // Temporary files remain, they're not dropped. + } } } - // Pipe stdout to stdin of next run... previous_stdin = Some(observed_stdout); } } + + Ok(()) } fn fix_windows_output(mut input: String) -> String { @@ -966,11 +980,12 @@ mod tests { impl Drop for TestHinter { fn drop(&mut self) { if std::thread::panicking() { - println!("\n=============================================="); - println!("💡 README test failed!"); - println!("Did you update the `srgn --help` output?"); - println!("If no, run `./scripts/update-readme.py README.md` and try again."); - println!("==============================================\n"); + println!( + r#" +💡 README test failed! +Run with `{FIX_ENV_VAR}=1` env var to fix the README automatically (BEST EFFORT!). +"# + ); } } }