From 9c68aad89c6839f69d509ae3975640283ead6e94 Mon Sep 17 00:00:00 2001
From: joshmc <josh-mcc@tiscali.co.uk>
Date: Fri, 4 Dec 2020 16:56:21 +0000
Subject: [PATCH] ISSUE-151 - Accept Readme Path and Section Name as
 parameters: * Update args to accept `--readme-path` and `section-name` values
 * Add logic to allow custom section names and custom README files * Add
 integration test for existing readme cases * Create new snapshots for README
 test cases

Signed-off-by: joshmc <josh-mcc@tiscali.co.uk>
---
 cargo-geiger/src/args.rs                      |  28 ++-
 cargo-geiger/src/format.rs                    |   1 +
 cargo-geiger/src/format/emoji_symbols.rs      |   1 +
 cargo-geiger/src/main.rs                      |  11 +-
 cargo-geiger/src/readme.rs                    | 202 ++++++++++++++++--
 .../tests/readme_integration_tests.rs         |  72 +++++++
 ...ts__test1_package_with_no_deps.readme.snap |  22 ++
 ...est2_package_with_shallow_deps.readme.snap |  24 +++
 ...test3_package_with_nested_deps.readme.snap |  36 ++++
 ...rkspace_with_top_level_package.readme.snap |  29 +++
 ...__test6_cargo_lock_out_of_date.readme.snap |  30 +++
 ...test7_package_with_patched_dep.readme.snap |  24 +++
 .../test1_package_with_no_deps/README.md      |   0
 .../test2_package_with_shallow_deps/README.md |   0
 .../test3_package_with_nested_deps/README.md  |  11 +
 .../README.md                                 |   6 +
 .../README_DIFFERENT_NAME.md                  |   0
 .../README_DIFFERENT_NAME.md                  |   0
 18 files changed, 459 insertions(+), 38 deletions(-)
 create mode 100644 cargo-geiger/tests/readme_integration_tests.rs
 create mode 100644 cargo-geiger/tests/snapshots/readme_integration_tests__test1_package_with_no_deps.readme.snap
 create mode 100644 cargo-geiger/tests/snapshots/readme_integration_tests__test2_package_with_shallow_deps.readme.snap
 create mode 100644 cargo-geiger/tests/snapshots/readme_integration_tests__test3_package_with_nested_deps.readme.snap
 create mode 100644 cargo-geiger/tests/snapshots/readme_integration_tests__test4_workspace_with_top_level_package.readme.snap
 create mode 100644 cargo-geiger/tests/snapshots/readme_integration_tests__test6_cargo_lock_out_of_date.readme.snap
 create mode 100644 cargo-geiger/tests/snapshots/readme_integration_tests__test7_package_with_patched_dep.readme.snap
 create mode 100644 test_crates/test1_package_with_no_deps/README.md
 create mode 100644 test_crates/test2_package_with_shallow_deps/README.md
 create mode 100644 test_crates/test3_package_with_nested_deps/README.md
 create mode 100644 test_crates/test4_workspace_with_top_level_package/README.md
 create mode 100644 test_crates/test6_cargo_lock_out_of_date/README_DIFFERENT_NAME.md
 create mode 100644 test_crates/test7_package_with_patched_dep/README_DIFFERENT_NAME.md

diff --git a/cargo-geiger/src/args.rs b/cargo-geiger/src/args.rs
index b98ed3ee..32cd51fd 100644
--- a/cargo-geiger/src/args.rs
+++ b/cargo-geiger/src/args.rs
@@ -34,9 +34,12 @@ OPTIONS:
     --format <FORMAT>             Format string used for printing dependencies
                                   [default: {p}].
     --json                        Output in JSON format.
-    --update-readme               Writes output to README.md. Looks for a Safety
+    --update-readme               Writes output to ./README.md. Looks for a Safety
                                   Report section, replaces if found, adds if not.
                                   Throws an error if no README.md exists.
+        --readme-path <PATH>      Path of README.md file to be written to.
+        --section-name <NAME>     The section name in the README.md to be written
+                                  to.
     -v, --verbose                 Use verbose output (-vv very verbose/build.rs
                                   output).
     -q, --quiet                   No output printed to stdout other than the
@@ -46,7 +49,7 @@ OPTIONS:
         --locked                  Require Cargo.lock is up to date.
         --offline                 Run without accessing the network.
     -Z \"<FLAG>...\"                Unstable (nightly-only) flags to Cargo.
-        --include-tests           Count unsafe usage in tests..
+        --include-tests           Count unsafe usage in tests.
         --build-dependencies      Also analyze build dependencies.
         --dev-dependencies        Also analyze dev dependencies.
         --all-dependencies        Analyze all dependencies, including build and
@@ -81,9 +84,9 @@ pub struct Args {
     pub package: Option<String>,
     pub prefix_depth: bool,
     pub quiet: bool,
+    pub readme_args: ReadmeArgs,
     pub target_args: TargetArgs,
     pub unstable_flags: Vec<String>,
-    pub update_readme: bool,
     pub verbose: u32,
     pub version: bool,
     pub output_format: Option<OutputFormat>,
@@ -133,6 +136,11 @@ impl Args {
             package: raw_args.opt_value_from_str("--manifest-path")?,
             prefix_depth: raw_args.contains("--prefix-depth"),
             quiet: raw_args.contains(["-q", "--quiet"]),
+            readme_args: ReadmeArgs {
+                readme_path: raw_args.opt_value_from_str("--readme-path")?,
+                section_name: raw_args.opt_value_from_str("--section-name")?,
+                update_readme: raw_args.contains("--update-readme"),
+            },
             target_args: TargetArgs {
                 all_targets: raw_args.contains("--all-targets"),
                 target: raw_args.opt_value_from_str("--target")?,
@@ -141,7 +149,6 @@ impl Args {
                 .opt_value_from_str("-Z")?
                 .map(|s: String| s.split(' ').map(|s| s.to_owned()).collect())
                 .unwrap_or_else(Vec::new),
-            update_readme: raw_args.contains("--update-readme"),
             verbose: match (
                 raw_args.contains("-vv"),
                 raw_args.contains(["-v", "--verbose"]),
@@ -195,26 +202,33 @@ impl Args {
     }
 }
 
-#[derive(Default)]
+#[derive(Debug, Default)]
 pub struct DepsArgs {
     pub all_deps: bool,
     pub build_deps: bool,
     pub dev_deps: bool,
 }
 
-#[derive(Default)]
+#[derive(Debug, Default)]
 pub struct FeaturesArgs {
     pub all_features: bool,
     pub features: Vec<String>,
     pub no_default_features: bool,
 }
 
-#[derive(Default)]
+#[derive(Debug, Default)]
 pub struct TargetArgs {
     pub all_targets: bool,
     pub target: Option<String>,
 }
 
+#[derive(Debug, Default)]
+pub struct ReadmeArgs {
+    pub readme_path: Option<PathBuf>,
+    pub section_name: Option<String>,
+    pub update_readme: bool,
+}
+
 fn parse_features(raw_features: Option<String>) -> Vec<String> {
     raw_features
         .as_ref()
diff --git a/cargo-geiger/src/format.rs b/cargo-geiger/src/format.rs
index fd38ed42..9333529f 100644
--- a/cargo-geiger/src/format.rs
+++ b/cargo-geiger/src/format.rs
@@ -14,6 +14,7 @@ use strum_macros::EnumIter;
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub enum Charset {
     Ascii,
+    GithubMarkDown,
     Utf8,
 }
 
diff --git a/cargo-geiger/src/format/emoji_symbols.rs b/cargo-geiger/src/format/emoji_symbols.rs
index b4398013..f5d2f8c7 100644
--- a/cargo-geiger/src/format/emoji_symbols.rs
+++ b/cargo-geiger/src/format/emoji_symbols.rs
@@ -17,6 +17,7 @@ impl EmojiSymbols {
             Box::new(self.fallbacks[idx].clone())
         }
     }
+
     pub fn new(charset: Charset) -> EmojiSymbols {
         Self {
             charset,
diff --git a/cargo-geiger/src/main.rs b/cargo-geiger/src/main.rs
index dba50003..a183378c 100644
--- a/cargo-geiger/src/main.rs
+++ b/cargo-geiger/src/main.rs
@@ -16,9 +16,7 @@ use cargo_geiger::args::{Args, HELP};
 use cargo_geiger::cli::{get_cargo_metadata, get_krates, get_workspace};
 use cargo_geiger::graph::build_graph;
 use cargo_geiger::mapping::{CargoMetadataParameters, QueryResolve};
-use cargo_geiger::readme::{
-    create_or_replace_section_in_readme, README_FILENAME,
-};
+use cargo_geiger::readme::create_or_replace_section_in_readme;
 use cargo_geiger::scan::{scan, FoundWarningsError, ScanResult};
 
 use cargo::core::shell::Shell;
@@ -94,12 +92,9 @@ fn real_main(args: &Args, config: &mut Config) -> CliResult {
         &workspace,
     )?;
 
-    if args.update_readme {
-        let mut current_dir_path_buf = std::env::current_dir().unwrap();
-        current_dir_path_buf.push(README_FILENAME);
-
+    if args.readme_args.update_readme {
         create_or_replace_section_in_readme(
-            current_dir_path_buf,
+            &args.readme_args,
             &scan_output_lines,
         )?;
     } else {
diff --git a/cargo-geiger/src/readme.rs b/cargo-geiger/src/readme.rs
index 9757d5ea..e533253f 100644
--- a/cargo-geiger/src/readme.rs
+++ b/cargo-geiger/src/readme.rs
@@ -1,3 +1,5 @@
+use crate::args::ReadmeArgs;
+
 use cargo::{CliError, CliResult};
 use regex::Regex;
 use std::fs::File;
@@ -14,26 +16,29 @@ const CARGO_GEIGER_SAFETY_REPORT_SECTION_HEADER: &str =
 /// of a scan, either create a section containing the scan result if one does not exist, or replace
 /// the section if it already exists
 pub fn create_or_replace_section_in_readme(
-    readme_file_path: PathBuf,
+    readme_args: &ReadmeArgs,
     scan_output_lines: &[String],
 ) -> CliResult {
-    if !readme_file_path.exists() {
+    let readme_path_buf =
+        get_readme_path_buf_from_arguments_or_default(readme_args);
+
+    if !readme_path_buf.exists() {
         eprintln!(
             "File {} does not exist. To construct a Cargo Geiger Safety Report section, please first create a README.",
-            readme_file_path.to_str().unwrap()
+            readme_path_buf.to_str().unwrap()
         );
         return CliResult::Err(CliError::code(1));
     }
 
     let mut readme_content =
-        BufReader::new(File::open(readme_file_path.clone()).unwrap())
+        BufReader::new(File::open(readme_path_buf.clone()).unwrap())
             .lines()
             .map(|l| l.unwrap())
             .collect::<Vec<String>>();
 
-    update_readme_content(&mut readme_content, scan_output_lines);
+    update_readme_content(readme_args, &mut readme_content, scan_output_lines);
 
-    let mut readme_file = File::create(readme_file_path).unwrap();
+    let mut readme_file = File::create(readme_path_buf).unwrap();
 
     for line in readme_content {
         writeln!(readme_file, "{}", line).unwrap();
@@ -46,13 +51,14 @@ pub fn create_or_replace_section_in_readme(
 /// the Section is not present, -1 is returned for both values, and if the Section is the last
 /// section present, then the last index is -1
 fn find_start_and_end_lines_of_safety_report_section(
+    readme_args: &ReadmeArgs,
     readme_content: &[String],
 ) -> (i32, i32) {
     let mut start_line_number = -1;
     let mut end_line_number = -1;
 
     let start_line_pattern =
-        Regex::new("#+\\sCargo\\sGeiger\\sSafety\\sReport\\s*").unwrap();
+        construct_regex_expression_for_section_header(readme_args);
 
     let end_line_pattern = Regex::new("#+.*").unwrap();
 
@@ -71,21 +77,70 @@ fn find_start_and_end_lines_of_safety_report_section(
     (start_line_number, end_line_number)
 }
 
+/// Constructs a regex expression for the Section Name if provided as an argument,
+/// otherwise returns a regex expression for the default Section Name
+fn construct_regex_expression_for_section_header(
+    readme_args: &ReadmeArgs,
+) -> Regex {
+    match &readme_args.section_name {
+        Some(section_name) => {
+            let mut regex_string = String::from("#+\\s");
+            regex_string.push_str(&section_name.replace(' ', "\\s"));
+            regex_string.push_str("\\s*");
+
+            Regex::new(&regex_string).unwrap()
+        }
+        None => {
+            Regex::new("#+\\sCargo\\sGeiger\\sSafety\\sReport\\s*").unwrap()
+        }
+    }
+}
+
+/// Returns the `PathBuf` passed in as an argument value if one exists, otherwise
+/// returns the `PathBuf` to a file `README.md` in the current directory
+fn get_readme_path_buf_from_arguments_or_default(
+    readme_args: &ReadmeArgs,
+) -> PathBuf {
+    match &readme_args.readme_path {
+        Some(readme_path) => readme_path.to_path_buf(),
+        None => {
+            let mut current_dir_path_buf = std::env::current_dir().unwrap();
+            current_dir_path_buf.push(README_FILENAME);
+            current_dir_path_buf
+        }
+    }
+}
+
 /// Update the content of a README.md with a Scan Result. When the section doesn't exist, it will
 /// be created with an `h2` level header, otherwise it will preserve the level of the existing
 /// header
 fn update_readme_content(
+    readme_args: &ReadmeArgs,
     readme_content: &mut Vec<String>,
     scan_result: &[String],
 ) {
     let (start_line_number, end_line_number) =
-        find_start_and_end_lines_of_safety_report_section(&readme_content);
+        find_start_and_end_lines_of_safety_report_section(
+            readme_args,
+            &readme_content,
+        );
 
     if start_line_number == -1 {
         // When Cargo Geiger Safety Report isn't present in README, add an
         // h2 headed section at the end of the README.md containing the report
-        readme_content
-            .push(CARGO_GEIGER_SAFETY_REPORT_SECTION_HEADER.to_string());
+        match &readme_args.section_name {
+            Some(section_name) => {
+                let mut section_name_string = String::from("## ");
+                section_name_string.push_str(section_name);
+
+                readme_content.push(section_name_string);
+            }
+            None => {
+                readme_content.push(
+                    CARGO_GEIGER_SAFETY_REPORT_SECTION_HEADER.to_string(),
+                );
+            }
+        }
         for scan_result_line in scan_result {
             readme_content.push(scan_result_line.to_string())
         }
@@ -119,11 +174,17 @@ mod readme_tests {
     #[rstest]
     fn create_or_replace_section_test_readme_doesnt_exist() {
         let temp_dir = tempdir().unwrap();
-        let readme_file_path = temp_dir.path().join("README.md");
+        let readme_path = temp_dir.path().join("README.md");
+
+        let readme_args = ReadmeArgs {
+            readme_path: Some(readme_path),
+            ..Default::default()
+        };
+
         let scan_result = vec![];
 
         let result =
-            create_or_replace_section_in_readme(readme_file_path, &scan_result);
+            create_or_replace_section_in_readme(&readme_args, &scan_result);
 
         assert!(result.is_err());
     }
@@ -131,8 +192,14 @@ mod readme_tests {
     #[rstest]
     fn create_or_replace_section_test_reademe_doesnt_contain_section() {
         let temp_dir = tempdir().unwrap();
-        let readme_file_path = temp_dir.path().join("README.md");
-        let mut readme_file = File::create(readme_file_path.clone()).unwrap();
+        let readme_path = temp_dir.path().join("README.md");
+
+        let readme_args = ReadmeArgs {
+            readme_path: Some(readme_path.clone()),
+            ..Default::default()
+        };
+
+        let mut readme_file = File::create(readme_path.clone()).unwrap();
         let scan_result = vec![
             String::from("First safety report line"),
             String::from("Second safety report line"),
@@ -144,15 +211,13 @@ mod readme_tests {
             "# Readme Header\nSome text\nAnother line\n## Another header\nMore text"
         ).unwrap();
 
-        let result = create_or_replace_section_in_readme(
-            readme_file_path.clone(),
-            &scan_result,
-        );
+        let result =
+            create_or_replace_section_in_readme(&readme_args, &scan_result);
 
         assert!(result.is_ok());
 
         let updated_file_content =
-            BufReader::new(File::open(readme_file_path).unwrap())
+            BufReader::new(File::open(readme_path).unwrap())
                 .lines()
                 .map(|l| l.unwrap())
                 .collect::<Vec<String>>();
@@ -172,6 +237,37 @@ mod readme_tests {
         assert_eq!(updated_file_content, expected_readme_content)
     }
 
+    #[rstest(
+        input_readme_args,
+        expected_regex_expression,
+        case(
+            ReadmeArgs{
+                section_name: None,
+                ..Default::default()
+            },
+            Regex::new("#+\\sCargo\\sGeiger\\sSafety\\sReport\\s*").unwrap()
+        ),
+        case(
+            ReadmeArgs{
+                section_name: Some(String::from("Test Section Name")),
+                ..Default::default()
+            },
+            Regex::new("#+\\sTest\\sSection\\sName\\s*").unwrap()
+        )
+    )]
+    fn construct_regex_expression_for_section_header_test(
+        input_readme_args: ReadmeArgs,
+        expected_regex_expression: Regex,
+    ) {
+        let regex_expression =
+            construct_regex_expression_for_section_header(&input_readme_args);
+
+        assert_eq!(
+            regex_expression.as_str(),
+            expected_regex_expression.as_str()
+        );
+    }
+
     #[rstest(
         input_readme_content,
         expected_start_line_number,
@@ -234,8 +330,11 @@ mod readme_tests {
         expected_start_line_number: i32,
         expected_end_line_number: i32,
     ) {
+        let readme_args = ReadmeArgs::default();
+
         let (start_line_number, end_line_number) =
             find_start_and_end_lines_of_safety_report_section(
+                &readme_args,
                 &input_readme_content,
             );
 
@@ -244,7 +343,58 @@ mod readme_tests {
     }
 
     #[rstest]
-    fn update_readme_content_test_no_safety_report_present() {
+    fn get_readme_path_buf_from_arguments_or_default_test_none() {
+        let mut path_buf = std::env::current_dir().unwrap();
+        path_buf.push(README_FILENAME);
+
+        let readme_args = ReadmeArgs {
+            readme_path: None,
+            ..Default::default()
+        };
+
+        let readme_path_buf =
+            get_readme_path_buf_from_arguments_or_default(&readme_args);
+
+        assert_eq!(readme_path_buf, path_buf)
+    }
+
+    #[rstest]
+    fn get_readme_path_buf_from_arguments_or_default_test_some() {
+        let path_buf = PathBuf::from("/test/path");
+
+        let readme_args = ReadmeArgs {
+            readme_path: Some(path_buf.clone()),
+            ..Default::default()
+        };
+
+        let readme_path_buf =
+            get_readme_path_buf_from_arguments_or_default(&readme_args);
+
+        assert_eq!(readme_path_buf, path_buf);
+    }
+
+    #[rstest(
+        input_readme_args,
+        expected_section_header,
+        case(
+            ReadmeArgs{
+                section_name: None,
+                ..Default::default()
+            },
+            CARGO_GEIGER_SAFETY_REPORT_SECTION_HEADER.to_string()
+        ),
+        case(
+            ReadmeArgs{
+                section_name: Some(String::from("Test Section Name")),
+                ..Default::default()
+            },
+            String::from("## Test Section Name")
+        )
+    )]
+    fn update_readme_content_test_no_safety_report_present(
+        input_readme_args: ReadmeArgs,
+        expected_section_header: String,
+    ) {
         let mut readme_content = vec![
             String::from("# readme header"),
             String::from("line of text"),
@@ -258,14 +408,18 @@ mod readme_tests {
             String::from("third line of scan result"),
         ];
 
-        update_readme_content(&mut readme_content, &scan_result);
+        update_readme_content(
+            &input_readme_args,
+            &mut readme_content,
+            &scan_result,
+        );
 
         let expected_readme_content = vec![
             String::from("# readme header"),
             String::from("line of text"),
             String::from("another line of text"),
             String::from("## another header"),
-            CARGO_GEIGER_SAFETY_REPORT_SECTION_HEADER.to_string(),
+            expected_section_header,
             String::from("first line of scan result"),
             String::from("second line of scan result"),
             String::from("third line of scan result"),
@@ -276,6 +430,8 @@ mod readme_tests {
 
     #[rstest]
     fn update_readme_content_test_safety_report_present_in_middle_of_readme() {
+        let readme_args = ReadmeArgs::default();
+
         let mut readme_content = vec![
             String::from("# readme header"),
             String::from("line of text"),
@@ -293,7 +449,7 @@ mod readme_tests {
             String::from("third line of scan result"),
         ];
 
-        update_readme_content(&mut readme_content, &scan_result);
+        update_readme_content(&readme_args, &mut readme_content, &scan_result);
 
         let expected_readme_content = vec![
             String::from("# readme header"),
diff --git a/cargo-geiger/tests/readme_integration_tests.rs b/cargo-geiger/tests/readme_integration_tests.rs
new file mode 100644
index 00000000..79eb72fd
--- /dev/null
+++ b/cargo-geiger/tests/readme_integration_tests.rs
@@ -0,0 +1,72 @@
+#![forbid(unsafe_code)]
+#![forbid(warnings)]
+
+mod context;
+mod run;
+
+use self::run::run_geiger_with;
+
+use insta::assert_snapshot;
+use rstest::rstest;
+use std::fs::read_to_string;
+
+#[rstest(
+    input_crate_name,
+    input_arg_vec,
+    input_readme_filename,
+    case(
+        "test1_package_with_no_deps",
+        vec!["--update-readme"],
+        "README.md"
+    ),
+    case(
+        "test2_package_with_shallow_deps",
+        vec!["--update-readme"],
+        "README.md"
+    ),
+    case(
+        "test3_package_with_nested_deps",
+        vec!["--update-readme"],
+        "README.md"
+    ),
+    case(
+        "test4_workspace_with_top_level_package",
+        vec!["--update-readme", "--section-name", "Test Section Name"],
+        "README.md"
+    ),
+    case(
+        "test6_cargo_lock_out_of_date",
+        vec![
+            "--update-readme",
+            "--readme-path",
+            "README_DIFFERENT_NAME.md"
+        ],
+        "README_DIFFERENT_NAME.md"
+    ),
+    case(
+        "test7_package_with_patched_dep",
+        vec![
+            "--update-readme",
+            "--section-name",
+            "Test Section Name",
+            "--readme-path",
+            "README_DIFFERENT_NAME.md"
+        ],
+        "README_DIFFERENT_NAME.md"
+    )
+)]
+fn test_package_update_readme(
+    input_crate_name: &str,
+    input_arg_vec: Vec<&str>,
+    input_readme_filename: &str,
+) {
+    let (_, context) = run_geiger_with(input_crate_name, input_arg_vec);
+
+    let readme_snapshot_filename = format!("{}.readme", input_crate_name);
+
+    let crate_location = context.crate_dir(input_crate_name);
+    let readme_location = crate_location.join(input_readme_filename);
+
+    let readme_content = read_to_string(readme_location).unwrap();
+    assert_snapshot!(readme_snapshot_filename, readme_content);
+}
diff --git a/cargo-geiger/tests/snapshots/readme_integration_tests__test1_package_with_no_deps.readme.snap b/cargo-geiger/tests/snapshots/readme_integration_tests__test1_package_with_no_deps.readme.snap
new file mode 100644
index 00000000..4102bfde
--- /dev/null
+++ b/cargo-geiger/tests/snapshots/readme_integration_tests__test1_package_with_no_deps.readme.snap
@@ -0,0 +1,22 @@
+---
+source: cargo-geiger/tests/readme_integration_tests.rs
+expression: readme_content
+---
+## Cargo Geiger Safety Report
+
+Metric output format: x/y
+    x = unsafe code used by the build
+    y = total unsafe code found in the crate
+
+Symbols: 
+    :) = No `unsafe` usage found, declares #![forbid(unsafe_code)]
+    ?  = No `unsafe` usage found, missing #![forbid(unsafe_code)]
+    !  = `unsafe` usage found
+
+Functions  Expressions  Impls  Traits  Methods  Dependency
+
+1/1        2/2          0/0    0/0     0/0      !  test1_package_with_no_deps 0.1.0
+
+1/1        2/2          0/0    0/0     0/0    
+
+
diff --git a/cargo-geiger/tests/snapshots/readme_integration_tests__test2_package_with_shallow_deps.readme.snap b/cargo-geiger/tests/snapshots/readme_integration_tests__test2_package_with_shallow_deps.readme.snap
new file mode 100644
index 00000000..8b6e4437
--- /dev/null
+++ b/cargo-geiger/tests/snapshots/readme_integration_tests__test2_package_with_shallow_deps.readme.snap
@@ -0,0 +1,24 @@
+---
+source: cargo-geiger/tests/readme_integration_tests.rs
+expression: readme_content
+---
+## Cargo Geiger Safety Report
+
+Metric output format: x/y
+    x = unsafe code used by the build
+    y = total unsafe code found in the crate
+
+Symbols: 
+    :) = No `unsafe` usage found, declares #![forbid(unsafe_code)]
+    ?  = No `unsafe` usage found, missing #![forbid(unsafe_code)]
+    !  = `unsafe` usage found
+
+Functions  Expressions  Impls  Traits  Methods  Dependency
+
+1/1        4/4          0/0    0/0     0/0      !  test2_package_with_shallow_deps 0.1.0
+0/0        2/2          0/0    0/0     0/0      !  |-- ref_slice 1.1.1
+1/1        2/2          0/0    0/0     0/0      !  `-- test1_package_with_no_deps 0.1.0
+
+2/2        8/8          0/0    0/0     0/0    
+
+
diff --git a/cargo-geiger/tests/snapshots/readme_integration_tests__test3_package_with_nested_deps.readme.snap b/cargo-geiger/tests/snapshots/readme_integration_tests__test3_package_with_nested_deps.readme.snap
new file mode 100644
index 00000000..36c4eafc
--- /dev/null
+++ b/cargo-geiger/tests/snapshots/readme_integration_tests__test3_package_with_nested_deps.readme.snap
@@ -0,0 +1,36 @@
+---
+source: cargo-geiger/tests/readme_integration_tests.rs
+expression: readme_content
+---
+# README Title
+Some text
+
+## First Section Header
+Some more text
+
+## Cargo Geiger Safety Report
+
+Metric output format: x/y
+    x = unsafe code used by the build
+    y = total unsafe code found in the crate
+
+Symbols: 
+    :) = No `unsafe` usage found, declares #![forbid(unsafe_code)]
+    ?  = No `unsafe` usage found, missing #![forbid(unsafe_code)]
+    !  = `unsafe` usage found
+
+Functions  Expressions  Impls  Traits  Methods  Dependency
+
+0/0        1/1          0/0    0/0     0/0      !  test3_package_with_nested_deps 0.1.0
+0/0        0/0          0/0    0/0     0/0      ?  |-- doc-comment 0.3.1
+0/0        0/72         0/3    0/1     0/3      ?  |-- itertools 0.8.0
+0/0        0/0          0/0    0/0     0/0      ?  |   `-- either 1.5.2
+1/1        4/4          0/0    0/0     0/0      !  `-- test2_package_with_shallow_deps 0.1.0
+0/0        2/2          0/0    0/0     0/0      !      |-- ref_slice 1.1.1
+1/1        2/2          0/0    0/0     0/0      !      `-- test1_package_with_no_deps 0.1.0
+
+2/2        9/81         0/3    0/1     0/3    
+
+## Second Section Header
+Some more text
+
diff --git a/cargo-geiger/tests/snapshots/readme_integration_tests__test4_workspace_with_top_level_package.readme.snap b/cargo-geiger/tests/snapshots/readme_integration_tests__test4_workspace_with_top_level_package.readme.snap
new file mode 100644
index 00000000..e4ea9096
--- /dev/null
+++ b/cargo-geiger/tests/snapshots/readme_integration_tests__test4_workspace_with_top_level_package.readme.snap
@@ -0,0 +1,29 @@
+---
+source: cargo-geiger/tests/readme_integration_tests.rs
+expression: readme_content
+---
+# README Title
+Some text
+
+## Another Title
+Some more text
+
+## Test Section Name
+
+Metric output format: x/y
+    x = unsafe code used by the build
+    y = total unsafe code found in the crate
+
+Symbols: 
+    :) = No `unsafe` usage found, declares #![forbid(unsafe_code)]
+    ?  = No `unsafe` usage found, missing #![forbid(unsafe_code)]
+    !  = `unsafe` usage found
+
+Functions  Expressions  Impls  Traits  Methods  Dependency
+
+0/0        0/1          0/0    0/0     0/0      ?  test4_workspace_with_top_level_package 0.1.0
+1/1        2/2          0/0    0/0     0/0      !  `-- test1_package_with_no_deps 0.1.0
+
+1/1        2/3          0/0    0/0     0/0    
+
+
diff --git a/cargo-geiger/tests/snapshots/readme_integration_tests__test6_cargo_lock_out_of_date.readme.snap b/cargo-geiger/tests/snapshots/readme_integration_tests__test6_cargo_lock_out_of_date.readme.snap
new file mode 100644
index 00000000..41a14f17
--- /dev/null
+++ b/cargo-geiger/tests/snapshots/readme_integration_tests__test6_cargo_lock_out_of_date.readme.snap
@@ -0,0 +1,30 @@
+---
+source: cargo-geiger/tests/readme_integration_tests.rs
+expression: readme_content
+---
+## Cargo Geiger Safety Report
+
+Metric output format: x/y
+    x = unsafe code used by the build
+    y = total unsafe code found in the crate
+
+Symbols: 
+    :) = No `unsafe` usage found, declares #![forbid(unsafe_code)]
+    ?  = No `unsafe` usage found, missing #![forbid(unsafe_code)]
+    !  = `unsafe` usage found
+
+Functions  Expressions  Impls  Traits  Methods  Dependency
+
+0/0        0/0          0/0    0/0     0/0      :) test6_cargo_lock_out_of_date 0.1.0
+0/0        0/0          0/0    0/0     0/0      :) |-- generational-arena 0.2.2
+0/0        0/0          0/0    0/0     0/0      ?  |   `-- cfg-if 0.1.9
+0/0        1/1          0/0    0/0     0/0      !  `-- idna 0.1.5
+0/0        0/0          0/0    0/0     0/0      ?      |-- matches 0.1.8
+0/0        0/0          0/0    0/0     0/0      :)     |-- unicode-bidi 0.3.4
+0/0        0/0          0/0    0/0     0/0      ?      |   `-- matches 0.1.8
+0/0        20/20        0/0    0/0     0/0      !      `-- unicode-normalization 0.1.8
+2/2        354/354      4/4    1/1     13/13    !          `-- smallvec 0.6.9
+
+2/2        375/375      4/4    1/1     13/13  
+
+
diff --git a/cargo-geiger/tests/snapshots/readme_integration_tests__test7_package_with_patched_dep.readme.snap b/cargo-geiger/tests/snapshots/readme_integration_tests__test7_package_with_patched_dep.readme.snap
new file mode 100644
index 00000000..0755e4d8
--- /dev/null
+++ b/cargo-geiger/tests/snapshots/readme_integration_tests__test7_package_with_patched_dep.readme.snap
@@ -0,0 +1,24 @@
+---
+source: cargo-geiger/tests/readme_integration_tests.rs
+expression: readme_content
+---
+## Test Section Name
+
+Metric output format: x/y
+    x = unsafe code used by the build
+    y = total unsafe code found in the crate
+
+Symbols: 
+    :) = No `unsafe` usage found, declares #![forbid(unsafe_code)]
+    ?  = No `unsafe` usage found, missing #![forbid(unsafe_code)]
+    !  = `unsafe` usage found
+
+Functions  Expressions  Impls  Traits  Methods  Dependency
+
+0/0        0/0          0/0    0/0     0/0      :) test7_package_with_patched_dep 0.1.0
+0/0        0/0          0/0    0/0     0/0      ?  `-- num_cpus 1.10.1
+1/1        2/2          0/0    0/0     0/0      !      `-- test1_package_with_no_deps 0.1.0
+
+1/1        2/2          0/0    0/0     0/0    
+
+
diff --git a/test_crates/test1_package_with_no_deps/README.md b/test_crates/test1_package_with_no_deps/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/test_crates/test2_package_with_shallow_deps/README.md b/test_crates/test2_package_with_shallow_deps/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/test_crates/test3_package_with_nested_deps/README.md b/test_crates/test3_package_with_nested_deps/README.md
new file mode 100644
index 00000000..5dcfc13a
--- /dev/null
+++ b/test_crates/test3_package_with_nested_deps/README.md
@@ -0,0 +1,11 @@
+# README Title
+Some text
+
+## First Section Header
+Some more text
+
+## Cargo Geiger Safety Report
+Old safety report
+
+## Second Section Header
+Some more text
diff --git a/test_crates/test4_workspace_with_top_level_package/README.md b/test_crates/test4_workspace_with_top_level_package/README.md
new file mode 100644
index 00000000..91723eec
--- /dev/null
+++ b/test_crates/test4_workspace_with_top_level_package/README.md
@@ -0,0 +1,6 @@
+# README Title
+Some text
+
+## Another Title
+Some more text
+
diff --git a/test_crates/test6_cargo_lock_out_of_date/README_DIFFERENT_NAME.md b/test_crates/test6_cargo_lock_out_of_date/README_DIFFERENT_NAME.md
new file mode 100644
index 00000000..e69de29b
diff --git a/test_crates/test7_package_with_patched_dep/README_DIFFERENT_NAME.md b/test_crates/test7_package_with_patched_dep/README_DIFFERENT_NAME.md
new file mode 100644
index 00000000..e69de29b