Repository: PlasmaFAIR/fortitude (language: python)

Issue:
Snapshot testing
We can (somewhat) simplify all our testing by switching to `insta` and capturing snapshots: https://insta.rs/docs/cli/

This is how rust tests individual rules: https://github.com/astral-sh/ruff/blob/main/CONTRIBUTING.md#rule-testing-fixtures-and-snapshots

Related: #29 
Add some basic integrated tests
Closes #29 

TODO:

- [x] Test `check`
- [x] Test configuration file


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 75ef76f..76fc0d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -315,6 +315,7 @@ dependencies = [
  "strum",
  "strum_macros",
  "tempfile",
+ "test-case",
  "textwrap",
  "thiserror",
  "toml",
@@ -870,6 +871,39 @@ version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
 
+[[package]]
+name = "test-case"
+version = "3.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8"
+dependencies = [
+ "test-case-macros",
+]
+
+[[package]]
+name = "test-case-core"
+version = "3.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
+dependencies = [
+ "cfg-if",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "test-case-macros"
+version = "3.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "test-case-core",
+]
+
 [[package]]
 name = "textwrap"
 version = "0.16.1"
diff --git a/README.dev.md b/README.dev.md
index a64ad40..7eddc29 100644
--- a/README.dev.md
+++ b/README.dev.md
@@ -26,15 +26,12 @@ Unit tests can be run by calling:
 cargo test
 ```
 
-Integration testing is currently being performed manually using the file `test.f90`:
+You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests:
 
-```bash
-fortitude check test.f90
+```shell
+cargo install cargo-insta
 ```
 
-The test suite is still in need of work, and we hope to include automated integration
-tests soon.
-
 ## Linting and Formatting
 
 When contributing, please use `cargo clippy` for linting and `cargo fmt` for formatting.
@@ -54,6 +51,42 @@ the rule is intended to fix. Words such as 'forbid' should be omitted. For examp
 name for the rule that warns of overly long lines is `LineTooLong`, and not something
 like `AvoidLineTooLong` or `KeepLinesShort`.
 
+#### Rule testing: fixtures and snapshots
+
+To test rules, Fortitude uses snapshots of Fortitude's output for a given file (fixture). Generally, there
+will be one file per rule (e.g., `E402.f90`), and each file will contain all necessary examples of
+both violations and non-violations. `cargo insta review` will generate a snapshot file containing
+Fortitude's output for each fixture, which you can then commit alongside your changes.
+
+Once you've completed the code for the rule itself, you can define tests with the following steps:
+
+1. Add a Fortran file to `fortitude/resources/test/fixtures/[category]` that contains the code you
+    want to test. The file name should match the rule name (e.g., `E402.f90`), and it should include
+    examples of both violations and non-violations.
+
+1. Run Fortitude locally against your file and verify the output is as expected. Once you're satisfied
+    with the output (you see the violations you expect, and no others), proceed to the next step.
+    For example, if you're adding a new rule named `E402`, you would run:
+
+    ```shell
+    cargo run -- check fortitude/resources/test/fixtures/typing/E402.f90 --select E402
+    ```
+
+    **Note:** Only a subset of rules are enabled by default. When testing a new rule, ensure that
+    you activate it by adding `--select ${rule_code}` to the command.
+
+1. Add the test to the relevant `fortitude/src/rules/[category]/mod.rs` file. If you're contributing
+    a rule to a pre-existing set, you should be able to find a similar example to pattern-match
+    against. If you're adding a new category, you'll need to create a new `mod.rs` file (see,
+    e.g., `fortitude/src/rules/typing/mod.rs`)
+
+1. Run `cargo test`. Your test will fail, but you'll be prompted to follow-up
+    with `cargo insta review`. Run `cargo insta review`, review and accept the generated snapshot,
+    then commit the snapshot file alongside the rest of your changes.
+
+1. Run `cargo test` again to ensure that your test passes.
+
+
 ## Making New Releases
 
 To make a new release, the following steps must be completed in order:
diff --git a/fortitude/Cargo.toml b/fortitude/Cargo.toml
index 3c238f9..b038039 100644
--- a/fortitude/Cargo.toml
+++ b/fortitude/Cargo.toml
@@ -48,6 +48,7 @@ insta-cmd = "0.6.0"
 predicates = "3.1.2"
 pretty_assertions = "1.4.1"
 tempfile = "3.13.0"
+test-case = "3.3.1"
 
 [profile.dev.package]
 insta.opt-level = 3
diff --git a/fortitude/src/check.rs b/fortitude/src/check.rs
index 29e5c30..605b3aa 100644
--- a/fortitude/src/check.rs
+++ b/fortitude/src/check.rs
@@ -89,7 +89,7 @@ fn get_files<S: AsRef<str>>(files_in: &Vec<PathBuf>, extensions: &[S]) -> Vec<Pa
 }
 
 /// Parse a file, check it for issues, and return the report.
-fn check_file(
+pub(crate) fn check_file(
     path_rules: &Vec<PathRuleEnum>,
     text_rules: &Vec<TextRuleEnum>,
     ast_entrypoints: &BTreeMap<&str, Vec<AstRuleEnum>>,
@@ -133,7 +133,7 @@ fn check_file(
 /// Check that the file length is representable as `u32` so
 /// that we don't need to check when converting tree-sitter offsets
 /// (usize) into ruff offsets (u32)
-fn read_to_string(path: &Path) -> std::io::Result<String> {
+pub(crate) fn read_to_string(path: &Path) -> std::io::Result<String> {
     let metadata = path.metadata()?;
     let file_length = metadata.len();
 
@@ -148,7 +148,7 @@ fn read_to_string(path: &Path) -> std::io::Result<String> {
     std::fs::read_to_string(path)
 }
 
-fn rules_to_path_rules(rules: &[Rule]) -> Vec<PathRuleEnum> {
+pub(crate) fn rules_to_path_rules(rules: &[Rule]) -> Vec<PathRuleEnum> {
     rules
         .iter()
         .filter_map(|rule| match TryFrom::try_from(*rule) {
@@ -158,7 +158,7 @@ fn rules_to_path_rules(rules: &[Rule]) -> Vec<PathRuleEnum> {
         .collect_vec()
 }
 
-fn rules_to_text_rules(rules: &[Rule]) -> Vec<TextRuleEnum> {
+pub(crate) fn rules_to_text_rules(rules: &[Rule]) -> Vec<TextRuleEnum> {
     rules
         .iter()
         .filter_map(|rule| match TryFrom::try_from(*rule) {
@@ -169,7 +169,7 @@ fn rules_to_text_rules(rules: &[Rule]) -> Vec<TextRuleEnum> {
 }
 
 /// Create a mapping of AST entrypoints to lists of the rules and codes that operate on them.
-fn ast_entrypoint_map<'a>(rules: &[Rule]) -> BTreeMap<&'a str, Vec<AstRuleEnum>> {
+pub(crate) fn ast_entrypoint_map<'a>(rules: &[Rule]) -> BTreeMap<&'a str, Vec<AstRuleEnum>> {
     let ast_rules: Vec<AstRuleEnum> = rules
         .iter()
         .filter_map(|rule| match TryFrom::try_from(*rule) {
diff --git a/fortitude/src/lib.rs b/fortitude/src/lib.rs
index 9831009..2dca7c7 100644
--- a/fortitude/src/lib.rs
+++ b/fortitude/src/lib.rs
@@ -7,17 +7,18 @@ mod rule_redirects;
 mod rule_selector;
 mod rules;
 mod settings;
+#[cfg(test)]
+mod test;
 pub use crate::registry::clap_completion::RuleParser;
 use crate::registry::AsRule;
 pub use crate::rule_selector::clap_completion::RuleSelectorParser;
 use annotate_snippets::{Level, Renderer, Snippet};
 use ast::{parse, FortitudeNode};
 use colored::{ColoredString, Colorize};
-use ruff_diagnostics::{Applicability, Diagnostic, DiagnosticKind, Fix};
+use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
 use ruff_source_file::{OneIndexed, SourceFile, SourceLocation};
 use ruff_text_size::{Ranged, TextRange, TextSize};
 use settings::{default_settings, Settings};
-use similar_asserts::SimpleDiff;
 use std::cmp::Ordering;
 use std::fmt;
 use std::path::Path;
@@ -155,8 +156,15 @@ impl<'a> Ranged for DiagnosticMessage<'a> {
 
 impl<'a> fmt::Display for DiagnosticMessage<'a> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        let path = self.file.name().bold();
-        let code = self.code.bold().bright_red();
+        let mut path: ColoredString = self.file.name().bold();
+        let mut code: ColoredString = self.code.bold().bright_red();
+
+        // Disable colours for tests, if the user requests it via env var, or non-tty
+        if cfg!(test) || !colored::control::SHOULD_COLORIZE.should_colorize() {
+            path = path.clear();
+            code = code.clear();
+        };
+
         let message = self.kind.body.as_str();
         let suggestion = &self.kind.suggestion;
         if self.range != TextRange::default() {
@@ -243,34 +251,6 @@ fn format_violation(
     let source_block = renderer.render(snippet_with_footer);
     writeln!(f, "{source_block}")?;
 
-    // If a fix is available, show diff to the user
-    if let Some(fix) = &diagnostic.fix {
-        let mut fixed = source_code.text().to_string();
-        let mut edits = fix.edits().to_vec();
-        // Edits are sorted, and we must apply them in reverse order to avoid invalidating other
-        // edits in the stack.
-        while let Some(edit) = edits.pop() {
-            // Remove content from source
-            fixed.replace_range(
-                usize::from(edit.range().start())..usize::from(edit.range().end()),
-                "",
-            );
-            // Add in suggested content
-            if let Some(content) = edit.content() {
-                fixed.insert_str(range.start().into(), content);
-            }
-        }
-        let fix_msg = match fix.applicability() {
-            Applicability::DisplayOnly => "The following fix may correct this problem.",
-            Applicability::Unsafe => "Running with '--fix --unsafe' will apply the following fix.",
-            Applicability::Safe => "Running with '--fix' will apply the following fix.",
-        };
-        let fix_title = Level::Note.title(fix_msg);
-        writeln!(f, "\n{}", renderer.render(fix_title))?;
-
-        let diff = SimpleDiff::from_str(source_code.text(), &fixed, "original", "fixed");
-        write!(f, "{diff}")?;
-    }
     Ok(())
 }
 
diff --git a/fortitude/src/rules/modules/external_functions.rs b/fortitude/src/rules/modules/external_functions.rs
index 288306d..1db2aca 100644
--- a/fortitude/src/rules/modules/external_functions.rs
+++ b/fortitude/src/rules/modules/external_functions.rs
@@ -43,70 +43,3 @@ impl AstRule for ExternalFunction {
         vec!["function", "subroutine"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_function_not_in_module() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            integer function double(x)
-              integer, intent(in) :: x
-              double = 2 * x
-            end function
-
-            subroutine triple(x)
-              integer, intent(inout) :: x
-              x = 3 * x
-            end subroutine
-            ",
-        );
-        let expected: Vec<_> = [(1, 0, 1, 26, "function"), (6, 0, 6, 20, "subroutine")]
-            .iter()
-            .map(|(start_line, start_col, end_line, end_col, kind)| {
-                Diagnostic::from_start_end_line_col(
-                    ExternalFunction {
-                        procedure: kind.to_string(),
-                    },
-                    &source,
-                    *start_line,
-                    *start_col,
-                    *end_line,
-                    *end_col,
-                )
-            })
-            .collect();
-        let actual = ExternalFunction::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-
-    #[test]
-    fn test_function_in_module() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            module my_module
-                implicit none
-            contains
-                integer function double(x)
-                  integer, intent(in) :: x
-                  double = 2 * x
-                end function
-
-                subroutine triple(x)
-                  integer, intent(inout) :: x
-                  x = 3 * x
-                end subroutine
-            end module
-            ",
-        );
-        let expected: Vec<Diagnostic> = vec![];
-        let actual = ExternalFunction::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/modules/mod.rs b/fortitude/src/rules/modules/mod.rs
index 55e0fe3..b5a4cfd 100644
--- a/fortitude/src/rules/modules/mod.rs
+++ b/fortitude/src/rules/modules/mod.rs
@@ -4,3 +4,30 @@ pub mod use_statements;
 // TODO should be private by default, with explicit public interface
 // TODO prefer 'end module {name}'
 // TODO function is not used within a module and is not public
+
+#[cfg(test)]
+mod tests {
+    use std::convert::AsRef;
+    use std::path::Path;
+
+    use anyhow::Result;
+    use insta::assert_snapshot;
+    use test_case::test_case;
+
+    use crate::registry::Rule;
+    use crate::settings::default_settings;
+    use crate::test::test_path;
+
+    #[test_case(Rule::ExternalFunction, Path::new("M001.f90"))]
+    #[test_case(Rule::UseAll, Path::new("M011.f90"))]
+    fn rules(rule_code: Rule, path: &Path) -> Result<()> {
+        let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
+        let diagnostics = test_path(
+            Path::new("modules").join(path).as_path(),
+            &[rule_code],
+            &default_settings(),
+        )?;
+        assert_snapshot!(snapshot, diagnostics);
+        Ok(())
+    }
+}
diff --git a/fortitude/src/rules/modules/use_statements.rs b/fortitude/src/rules/modules/use_statements.rs
index 053adb3..2aedaf6 100644
--- a/fortitude/src/rules/modules/use_statements.rs
+++ b/fortitude/src/rules/modules/use_statements.rs
@@ -49,33 +49,3 @@ impl AstRule for UseAll {
         vec!["use_statement"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_use_all() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            module my_module
-                use, intrinsic :: iso_fortran_env, only: real32
-                use, intrinsic :: iso_c_binding
-            end module
-            ",
-        );
-        let expected = vec![Diagnostic::from_start_end_line_col(
-            UseAll {},
-            &source,
-            3,
-            4,
-            3,
-            35,
-        )];
-        let actual = UseAll::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/precision/double_precision.rs b/fortitude/src/rules/precision/double_precision.rs
index 0bbbce2..32fdf83 100644
--- a/fortitude/src/rules/precision/double_precision.rs
+++ b/fortitude/src/rules/precision/double_precision.rs
@@ -66,58 +66,3 @@ impl AstRule for DoublePrecision {
         vec!["intrinsic_type"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_double_precision() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            double precision function double(x)
-              double precision, intent(in) :: x
-              double = 2 * x
-            end function
-
-            subroutine triple(x)
-              double precision, intent(inout) :: x
-              x = 3 * x
-            end subroutine
-
-            function complex_mul(x, y)
-              double precision, intent(in) :: x
-              double complex, intent(in) :: y
-              double complex :: complex_mul
-              complex_mul = x * y
-            end function
-            ",
-        );
-        let expected: Vec<_> = [
-            (1, 0, 1, 16, "double precision"),
-            (2, 2, 2, 18, "double precision"),
-            (7, 2, 7, 18, "double precision"),
-            (12, 2, 12, 18, "double precision"),
-            (13, 2, 13, 16, "double complex"),
-            (14, 2, 14, 16, "double complex"),
-        ]
-        .iter()
-        .map(|(start_line, start_col, end_line, end_col, kind)| {
-            let msg = DoublePrecision::try_new(kind).unwrap();
-            Diagnostic::from_start_end_line_col(
-                msg,
-                &source,
-                *start_line,
-                *start_col,
-                *end_line,
-                *end_col,
-            )
-        })
-        .collect();
-        let actual = DoublePrecision::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/precision/implicit_kinds.rs b/fortitude/src/rules/precision/implicit_kinds.rs
index b8a9d1a..df700b4 100644
--- a/fortitude/src/rules/precision/implicit_kinds.rs
+++ b/fortitude/src/rules/precision/implicit_kinds.rs
@@ -45,51 +45,3 @@ impl AstRule for ImplicitRealKind {
         vec!["intrinsic_type"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_implicit_real_kind() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            real function my_func(a, b, c, d, e)       ! catch
-              real, intent(in) :: a                    ! catch
-              real(4), intent(in) :: b                 ! ignore
-              integer, intent(in) :: c                 ! ignore
-              complex, intent(in) :: d                 ! catch
-              complex(8), intent(in) :: e              ! ignore
-
-              myfunc = a
-            end function my_func
-            ",
-        );
-
-        let expected: Vec<_> = [
-            (1, 0, 1, 4, "real"),
-            (2, 2, 2, 6, "real"),
-            (5, 2, 5, 9, "complex"),
-        ]
-        .iter()
-        .map(|(start_line, start_col, end_line, end_col, dtype)| {
-            Diagnostic::from_start_end_line_col(
-                ImplicitRealKind {
-                    dtype: dtype.to_string(),
-                },
-                &source,
-                *start_line,
-                *start_col,
-                *end_line,
-                *end_col,
-            )
-        })
-        .collect();
-        let actual = ImplicitRealKind::apply(&source)?;
-        assert_eq!(actual, expected);
-
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/precision/kind_suffixes.rs b/fortitude/src/rules/precision/kind_suffixes.rs
index 0d00585..6057419 100644
--- a/fortitude/src/rules/precision/kind_suffixes.rs
+++ b/fortitude/src/rules/precision/kind_suffixes.rs
@@ -73,59 +73,3 @@ impl AstRule for NoRealSuffix {
         vec!["number_literal"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_no_real_suffix() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            use, intrinsic :: iso_fortran_env, only: sp => real32, dp => real64
-
-            real(sp), parameter :: x1 = 1.234567
-            real(dp), parameter :: x2 = 1.234567_dp
-            real(dp), parameter :: x3 = 1.789d3 ! rule should ignore d exponentiation
-            real(dp), parameter :: x4 = 9.876
-            real(sp), parameter :: x5 = 2.468_sp
-            real(sp), parameter :: x6 = 2.
-            real(sp), parameter :: x7 = .0
-            real(sp), parameter :: x8 = 1E2
-            real(sp), parameter :: x9 = .1e2
-            real(sp), parameter :: y1 = 1.E2
-            real(sp), parameter :: y2 = 1.2e3
-
-            ",
-        );
-        let expected: Vec<_> = [
-            (3, 28, 3, 36, "1.234567"),
-            (6, 28, 6, 33, "9.876"),
-            (8, 28, 8, 30, "2."),
-            (9, 28, 9, 30, ".0"),
-            (10, 28, 10, 31, "1E2"),
-            (11, 28, 11, 32, ".1e2"),
-            (12, 28, 12, 32, "1.E2"),
-            (13, 28, 13, 33, "1.2e3"),
-        ]
-        .iter()
-        .map(|(start_line, start_col, end_line, end_col, num)| {
-            Diagnostic::from_start_end_line_col(
-                NoRealSuffix {
-                    literal: num.to_string(),
-                },
-                &source,
-                *start_line,
-                *start_col,
-                *end_line,
-                *end_col,
-            )
-        })
-        .collect();
-        let actual = NoRealSuffix::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/precision/mod.rs b/fortitude/src/rules/precision/mod.rs
index 8dfc992..5dbf3ea 100644
--- a/fortitude/src/rules/precision/mod.rs
+++ b/fortitude/src/rules/precision/mod.rs
@@ -1,3 +1,31 @@
 pub mod double_precision;
 pub mod implicit_kinds;
 pub mod kind_suffixes;
+
+#[cfg(test)]
+mod tests {
+    use std::convert::AsRef;
+    use std::path::Path;
+
+    use anyhow::Result;
+    use insta::assert_snapshot;
+    use test_case::test_case;
+
+    use crate::registry::Rule;
+    use crate::settings::default_settings;
+    use crate::test::test_path;
+
+    #[test_case(Rule::NoRealSuffix, Path::new("P001.f90"))]
+    #[test_case(Rule::DoublePrecision, Path::new("P011.f90"))]
+    #[test_case(Rule::ImplicitRealKind, Path::new("P021.f90"))]
+    fn rules(rule_code: Rule, path: &Path) -> Result<()> {
+        let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
+        let diagnostics = test_path(
+            Path::new("precision").join(path).as_path(),
+            &[rule_code],
+            &default_settings(),
+        )?;
+        assert_snapshot!(snapshot, diagnostics);
+        Ok(())
+    }
+}
diff --git a/fortitude/src/rules/style/end_statements.rs b/fortitude/src/rules/style/end_statements.rs
index 84b774a..bebe5f4 100644
--- a/fortitude/src/rules/style/end_statements.rs
+++ b/fortitude/src/rules/style/end_statements.rs
@@ -106,110 +106,3 @@ impl AstRule for UnnamedEndStatement {
         ]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_unnamed_end_statement() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            module mymod1
-              implicit none
-              type mytype
-                integer :: x
-              end type                      ! catch this
-            contains
-              subroutine mysub1()
-                write (*,*) 'hello world'
-              end subroutine                ! catch this
-              subroutine mysub2()
-                write (*,*) 'hello world'
-              end subroutine mysub2         ! ignore this
-            end                             ! catch this
-            module mymod2
-              implicit none
-              type mytype
-                integer :: x
-              end type mytype               ! ignore this
-            contains
-              integer function myfunc1()
-                myfunc1 = 1
-              end function                  ! catch this
-              integer function myfunc2()
-                myfunc2 = 1
-              end function myfunc2          ! ignore this
-            end module                      ! catch this
-            module mymod3
-              interface
-                module function foo() result(x)
-                  integer :: x
-                end function foo            ! ignore this
-                module function bar() result(x)
-                  integer :: x
-                end function bar            ! ignore this
-                module function baz() result(x)
-                  integer :: x
-                end function baz            ! ignore this
-              end interface
-            end module mymod3
-            submodule (mymod3) mysub1
-            contains
-              module procedure foo
-                x = 1
-              end procedure                 ! catch this
-            end                             ! catch this
-            submodule (mymod3) mysub2
-            contains
-              module procedure bar
-                x = 1
-              end procedure bar             ! ignore this
-            end submodule                   ! catch this
-            submodule (mymod3) mysub3
-            contains
-              module procedure baz
-                x = 1
-              end procedure baz             ! ignore this
-            end submodule mysub3            ! ignore this
-            program myprog
-              implicit none
-              write (*,*) 'hello world'
-            end                             ! catch this
-            ",
-        );
-        let expected: Vec<_> = [
-            (5, 2, 5, 32, "type", "mytype"),
-            (9, 2, 9, 32, "subroutine", "mysub1"),
-            (13, 0, 13, 32, "module", "mymod1"),
-            (22, 2, 22, 32, "function", "myfunc1"),
-            (26, 0, 26, 32, "module", "mymod2"),
-            (44, 2, 44, 32, "procedure", "foo"),
-            (45, 0, 45, 32, "submodule", "mysub1"),
-            (51, 0, 51, 32, "submodule", "mysub2"),
-            (61, 0, 61, 32, "program", "myprog"),
-        ]
-        .iter()
-        .map(
-            |(start_line, start_col, end_line, end_col, statement, name)| {
-                Diagnostic::from_start_end_line_col(
-                    UnnamedEndStatement {
-                        statement: statement.to_string(),
-                        name: name.to_string(),
-                    },
-                    &source,
-                    *start_line,
-                    *start_col,
-                    *end_line,
-                    *end_col,
-                )
-            },
-        )
-        .collect();
-        let actual = UnnamedEndStatement::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/style/exit_labels.rs b/fortitude/src/rules/style/exit_labels.rs
index 7101ede..f135d0c 100644
--- a/fortitude/src/rules/style/exit_labels.rs
+++ b/fortitude/src/rules/style/exit_labels.rs
@@ -69,87 +69,3 @@ impl AstRule for MissingExitOrCycleLabel {
         vec!["do_loop_statement"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_missing_exit_label() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            program test
-              label1: do
-                if (.true.) then
-                  EXIT
-                end if
-              end do label1
-
-              label2: do
-                if (.true.) exit
-              end do label2
-
-              label3: do
-                exit label3
-              end do label3
-
-              label4: do
-                if (.true.) exit label4
-              end do label4
-
-              label5: do i = 1, 2
-                do j = 1, 2  ! unnamed inner loop: currently doesn't warn
-                  cycle
-                end do
-              end do label5
-
-              label6: do i = 1, 2
-                inner: do j = 1, 2
-                  if (.true.) CYCLE ! named inner loop: warns on inner loop
-                end do inner
-              end do label6
-
-              label7: do
-                cycle label7
-              end do label7
-
-              label8: do
-                if (.true.) cycle label8
-              end do label8
-
-              do
-                ! Don't warn on unnamed loops
-                exit
-              end do
-            end program test
-            ",
-        );
-        let expected: Vec<_> = [
-            (4, 6, 4, 10, "exit", "label1"),
-            (9, 16, 9, 20, "exit", "label2"),
-            (28, 18, 28, 23, "cycle", "inner"),
-        ]
-        .iter()
-        .map(
-            |(start_line, start_col, end_line, end_col, stmt_kind, label)| {
-                Diagnostic::from_start_end_line_col(
-                    MissingExitOrCycleLabel {
-                        name: stmt_kind.to_string(),
-                        label: label.to_string(),
-                    },
-                    &source,
-                    *start_line,
-                    *start_col,
-                    *end_line,
-                    *end_col,
-                )
-            },
-        )
-        .collect();
-        let actual = MissingExitOrCycleLabel::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/style/line_length.rs b/fortitude/src/rules/style/line_length.rs
index b7cf87a..2a2cec4 100644
--- a/fortitude/src/rules/style/line_length.rs
+++ b/fortitude/src/rules/style/line_length.rs
@@ -70,53 +70,3 @@ impl TextRule for LineTooLong {
         violations
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use crate::test_file;
-
-    use super::*;
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_line_too_long() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            program test
-              use some_really_long_module_name, only : integer_working_precision
-              implicit none
-              integer(integer_working_precision), parameter, dimension(1) :: a = [1]
-            end program test
-            ",
-        );
-
-        let max_length = 20;
-        let short_line_settings = Settings {
-            line_length: max_length,
-        };
-        let expected: Vec<Diagnostic> = [
-            (2, max_length, 2, 68, 68usize),
-            (4, max_length, 4, 72, 72usize),
-        ]
-        .iter()
-        .map(
-            |(start_line, start_col, end_line, end_col, actual_length)| {
-                Diagnostic::from_start_end_line_col(
-                    LineTooLong {
-                        max_length,
-                        actual_length: *actual_length,
-                    },
-                    &source,
-                    *start_line,
-                    *start_col,
-                    *end_line,
-                    *end_col,
-                )
-            },
-        )
-        .collect();
-        let actual = LineTooLong::check(&short_line_settings, &source);
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/style/mod.rs b/fortitude/src/rules/style/mod.rs
index fb4bea3..3ca2a1c 100644
--- a/fortitude/src/rules/style/mod.rs
+++ b/fortitude/src/rules/style/mod.rs
@@ -4,3 +4,51 @@ pub mod line_length;
 pub mod old_style_array_literal;
 pub mod relational_operators;
 pub mod whitespace;
+
+#[cfg(test)]
+mod tests {
+    use std::convert::AsRef;
+    use std::path::Path;
+
+    use anyhow::Result;
+    use insta::assert_snapshot;
+    use test_case::test_case;
+
+    use crate::registry::Rule;
+    use crate::settings::{default_settings, Settings};
+    use crate::test::test_path;
+
+    #[test_case(Rule::LineTooLong, Path::new("S001.f90"))]
+    #[test_case(Rule::MissingExitOrCycleLabel, Path::new("S021.f90"))]
+    #[test_case(Rule::OldStyleArrayLiteral, Path::new("S041.f90"))]
+    #[test_case(Rule::DeprecatedRelationalOperator, Path::new("S051.f90"))]
+    #[test_case(Rule::UnnamedEndStatement, Path::new("S061.f90"))]
+    #[test_case(Rule::TrailingWhitespace, Path::new("S101.f90"))]
+    fn rules(rule_code: Rule, path: &Path) -> Result<()> {
+        let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
+        let diagnostics = test_path(
+            Path::new("style").join(path).as_path(),
+            &[rule_code],
+            &default_settings(),
+        )?;
+        assert_snapshot!(snapshot, diagnostics);
+        Ok(())
+    }
+
+    #[test_case(Rule::LineTooLong, Path::new("S001_line_length_20.f90"))]
+    fn line_too_long_line_length_20(rule_code: Rule, path: &Path) -> Result<()> {
+        let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
+        #[allow(clippy::needless_update)]
+        let settings = Settings {
+            line_length: 20,
+            ..default_settings()
+        };
+        let diagnostics = test_path(
+            Path::new("style").join(path).as_path(),
+            &[rule_code],
+            &settings,
+        )?;
+        assert_snapshot!(snapshot, diagnostics);
+        Ok(())
+    }
+}
diff --git a/fortitude/src/rules/style/old_style_array_literal.rs b/fortitude/src/rules/style/old_style_array_literal.rs
index 63f1220..9b5e9a7 100644
--- a/fortitude/src/rules/style/old_style_array_literal.rs
+++ b/fortitude/src/rules/style/old_style_array_literal.rs
@@ -34,53 +34,3 @@ impl AstRule for OldStyleArrayLiteral {
         vec!["array_literal"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_old_style_array_literal() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            program test
-              integer :: a(3) = (/1, 2, 3/)
-              integer :: b(3) = (/ &
-                 1, &
-                 2, &
-                 3 &
-              /)
-             if (.true.) a = (/4, 5, 6/)
-             b(1:3) = (/ &
-                 4, &
-                 5, &
-                 6 &
-              /)
-             end program test
-            ",
-        );
-        let expected: Vec<_> = [
-            (2, 20, 2, 31),
-            (3, 20, 7, 4),
-            (8, 17, 8, 28),
-            (9, 10, 13, 4),
-        ]
-        .iter()
-        .map(|(start_line, start_col, end_line, end_col)| {
-            Diagnostic::from_start_end_line_col(
-                OldStyleArrayLiteral {},
-                &source,
-                *start_line,
-                *start_col,
-                *end_line,
-                *end_col,
-            )
-        })
-        .collect();
-        let actual = OldStyleArrayLiteral::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/style/whitespace.rs b/fortitude/src/rules/style/whitespace.rs
index 4505239..29437f1 100644
--- a/fortitude/src/rules/style/whitespace.rs
+++ b/fortitude/src/rules/style/whitespace.rs
@@ -40,48 +40,3 @@ impl TextRule for TrailingWhitespace {
         violations
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::settings::default_settings;
-    use pretty_assertions::assert_eq;
-    use ruff_source_file::SourceFileBuilder;
-
-    #[test]
-    fn test_trailing_whitespace() -> anyhow::Result<()> {
-        // Careful of trailing whitespace in this string! That's the
-        // point of the test! Also not using `dedent` here as it
-        // messes with the whitespace-only line
-        let source = "
-program test  
-  implicit none
-  integer :: a(3) = [ & 
-    1, &
-    2, &
-    3 &
-  ]    
-   
-end program test
-";
-        let file = SourceFileBuilder::new("test", source).finish();
-
-        let expected: Vec<Diagnostic> =
-            [(0, 13, 0, 15), (3, 23, 3, 24), (7, 3, 7, 7), (8, 0, 8, 3)]
-                .iter()
-                .map(|(start_line, start_col, end_line, end_col)| {
-                    Diagnostic::from_start_end_line_col(
-                        TrailingWhitespace {},
-                        &file,
-                        *start_line,
-                        *start_col,
-                        *end_line,
-                        *end_col,
-                    )
-                })
-                .collect();
-        let actual = TrailingWhitespace::check(&default_settings(), &file);
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/typing/assumed_size.rs b/fortitude/src/rules/typing/assumed_size.rs
index 4038b7f..0e1f49d 100644
--- a/fortitude/src/rules/typing/assumed_size.rs
+++ b/fortitude/src/rules/typing/assumed_size.rs
@@ -282,152 +282,3 @@ impl AstRule for DeprecatedAssumedSizeCharacter {
         vec!["assumed_size"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_assumed_size() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            subroutine assumed_size_dimension(array, n, m, l, o, p, options, thing, q)
-              integer, intent(in) :: n, m
-              integer, dimension(n, m, *), intent(in) :: array
-              integer, intent(in) :: l(*), o, p(*)
-              ! warning must be on the array part for characters
-              character(len=*), dimension(*) :: options
-              character(*) :: thing(*)
-              ! this is ok
-              character(*), intent(in) :: q
-              ! following are ok because they're parameters
-              integer, dimension(*), parameter :: param = [1, 2, 3]
-              character(*), dimension(*), parameter :: param_char = ['hello']
-            end subroutine assumed_size_dimension
-            ",
-        );
-        let expected: Vec<_> = [
-            (3, 27, 3, 28, "array"),
-            (4, 27, 4, 28, "l"),
-            (4, 36, 4, 37, "p"),
-            (6, 30, 6, 31, "options"),
-            (7, 24, 7, 25, "thing"),
-        ]
-        .iter()
-        .map(|(start_line, start_col, end_line, end_col, variable)| {
-            Diagnostic::from_start_end_line_col(
-                AssumedSize {
-                    name: variable.to_string(),
-                },
-                &source,
-                *start_line,
-                *start_col,
-                *end_line,
-                *end_col,
-            )
-        })
-        .collect();
-        let actual = AssumedSize::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-
-    #[test]
-    fn test_assumed_size_character_intent() -> anyhow::Result<()> {
-        // test case from stylist
-        let source = test_file(
-            "
-            program cases
-              ! A char array outside a function or subroutine, no exception
-              character (*) :: autochar_glob
-            contains
-              subroutine char_input(autochar_in, autochar_inout, autochar_out, fixedchar)
-                ! A char array with proper intent, no exception
-                character(*), intent(in)       :: autochar_in
-                ! A char array with disallowed intent, exception
-                character(*), intent(inout)    :: autochar_inout
-                ! A char array with disallowed intent, exception
-                character(len=*), intent(out)  :: autochar_out
-                ! A char array not passed as a parameter, no exception
-                character(*)                   :: autochar_var
-                ! A char array with fixed length, no exception
-                character(len=10), intent(out) :: fixedchar
-                ! A declaration with non-intent attribute, no exception
-                character(len=*), parameter :: alt_attr = 'sample'
-              end subroutine char_input
-            end program cases
-            ",
-        );
-        let expected: Vec<_> = [
-            (3, 13, 3, 14, "autochar_glob"),
-            (9, 14, 9, 15, "autochar_inout"),
-            (11, 18, 11, 19, "autochar_out"),
-            (13, 14, 13, 15, "autochar_var"),
-        ]
-        .iter()
-        .map(|(start_line, start_col, end_line, end_col, variable)| {
-            Diagnostic::from_start_end_line_col(
-                AssumedSizeCharacterIntent {
-                    name: variable.to_string(),
-                },
-                &source,
-                *start_line,
-                *start_col,
-                *end_line,
-                *end_col,
-            )
-        })
-        .collect();
-        let actual = AssumedSizeCharacterIntent::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-
-    #[test]
-    fn test_deprecated_assumed_size_character() -> anyhow::Result<()> {
-        // test case from stylist
-        let source = test_file(
-            "
-            program cases
-            contains
-              subroutine char_input(a, b, c, d, e, f)
-                character * ( * ), intent(in) :: a
-                character*(*), intent(in) :: b
-                character*(len=*), intent(in) :: c
-                character*(3), intent(in) :: d
-                character*(MAX_LEN), intent(in) :: e
-                ! these are ok
-                character(*, kind) :: f
-                character(len=*, kind=4) :: g
-              end subroutine char_input
-            end program cases
-            ",
-        );
-        let expected: Vec<_> = [
-            (4, 14, 4, 15, "a"),
-            (5, 13, 5, 14, "b"),
-            (6, 13, 6, 14, "c"),
-            (7, 13, 7, 14, "d"),
-            (8, 13, 8, 14, "e"),
-        ]
-        .iter()
-        .map(|(start_line, start_col, end_line, end_col, variable)| {
-            Diagnostic::from_start_end_line_col(
-                DeprecatedAssumedSizeCharacter {
-                    name: variable.to_string(),
-                },
-                &source,
-                *start_line,
-                *start_col,
-                *end_line,
-                *end_col,
-            )
-        })
-        .collect();
-        let actual = DeprecatedAssumedSizeCharacter::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/typing/implicit_typing.rs b/fortitude/src/rules/typing/implicit_typing.rs
index f558d86..57901f6 100644
--- a/fortitude/src/rules/typing/implicit_typing.rs
+++ b/fortitude/src/rules/typing/implicit_typing.rs
@@ -149,264 +149,3 @@ impl AstRule for SuperfluousImplicitNone {
         vec!["implicit_statement"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-    use ruff_diagnostics::Edit;
-    use ruff_text_size::{TextRange, TextSize};
-
-    #[test]
-    fn test_implicit_typing() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            module my_module
-                parameter(N = 1)
-            end module
-
-            program my_program
-                write(*,*) 42
-            end program
-            ",
-        );
-        let expected: Vec<_> = [(0, 1, 0, 17, "module"), (5, 0, 5, 18, "program")]
-            .iter()
-            .map(|(start_line, start_col, end_line, end_col, kind)| {
-                Diagnostic::from_start_end_line_col(
-                    ImplicitTyping {
-                        entity: kind.to_string(),
-                    },
-                    &source,
-                    *start_line,
-                    *start_col,
-                    *end_line,
-                    *end_col,
-                )
-            })
-            .collect();
-        let actual = ImplicitTyping::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-
-    #[test]
-    fn test_implicit_none() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            module my_module
-                implicit none
-            contains
-                integer function double(x)
-                  integer, intent(in) :: x
-                  double = 2 * x
-                end function
-            end module
-
-            program my_program
-                implicit none
-                integer, paramter :: x = 2
-                write(*,*) x
-            end program
-            ",
-        );
-        let expected: Vec<Diagnostic> = vec![];
-        let actual = ImplicitTyping::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-
-    #[test]
-    fn test_interface_implicit_typing() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            module my_module
-                implicit none
-                interface
-                    integer function myfunc(x)
-                        integer, intent(in) :: x
-                    end function
-                end interface
-            end module
-
-            program my_program
-                implicit none
-                interface
-                    subroutine myfunc2(x)
-                        integer, intent(inout) :: x
-                    end subroutine
-                end interface
-                write(*,*) 42
-            end program
-            ",
-        );
-        let expected: Vec<_> = [(4, 8, 4, 34, "function"), (13, 8, 13, 29, "subroutine")]
-            .iter()
-            .map(|(start_line, start_col, end_line, end_col, kind)| {
-                Diagnostic::from_start_end_line_col(
-                    InterfaceImplicitTyping {
-                        name: kind.to_string(),
-                    },
-                    &source,
-                    *start_line,
-                    *start_col,
-                    *end_line,
-                    *end_col,
-                )
-            })
-            .collect();
-        let actual = InterfaceImplicitTyping::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-
-    #[test]
-    fn test_interface_implicit_none() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            module my_module
-                implicit none
-                interface
-                    integer function myfunc(x)
-                        implicit none
-                        integer, intent(in) :: x
-                    end function
-                end interface
-            end module
-
-            program my_program
-                implicit none
-                interface
-                    subroutine mysub(x)
-                        implicit none
-                        integer, intent(inout) :: x
-                    end subroutine
-                end interface
-                write(*,*) 42
-            end program
-            ",
-        );
-        let expected: Vec<Diagnostic> = vec![];
-        let actual = InterfaceImplicitTyping::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-
-    #[test]
-    fn test_superfluous_implicit_none() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            module my_module
-                implicit none
-            contains
-                integer function myfunc(x)
-                    implicit none
-                    integer, intent(in) :: x
-                    myfunc = x * 2
-                end function
-                subroutine mysub(x)
-                    implicit none
-                    integer, intent(inout) :: x
-                    x = x * 2
-                end subroutine
-            end module
-
-            program my_program
-                implicit none
-
-                write(*,*) 42
-
-            contains
-                integer function myfunc2(x)
-                    implicit none
-                    integer, intent(in) :: x
-                    myfunc2 = x * 2
-                end function
-                subroutine mysub2(x)
-                    implicit none
-                    integer, intent(inout) :: x
-                    x = x * 2
-                end subroutine
-            end program
-            ",
-        );
-        let expected: Vec<_> = [
-            (5, 8, 5, 21, 76u32, 98u32, "module"),
-            (10, 8, 10, 21, 195u32, 217u32, "module"),
-            (23, 8, 23, 21, 400u32, 422u32, "program"),
-            (28, 8, 28, 21, 521u32, 543u32, "program"),
-        ]
-        .iter()
-        .map(
-            |(start_line, start_col, end_line, end_col, start_byte, end_byte, kind)| {
-                Diagnostic::from_start_end_line_col(
-                    SuperfluousImplicitNone {
-                        entity: kind.to_string(),
-                    },
-                    &source,
-                    *start_line,
-                    *start_col,
-                    *end_line,
-                    *end_col,
-                )
-                .with_fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
-                    TextSize::from(*start_byte),
-                    TextSize::from(*end_byte),
-                ))))
-            },
-        )
-        .collect();
-        let actual = SuperfluousImplicitNone::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-
-    #[test]
-    fn test_no_superfluous_implicit_none() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            module my_module
-                implicit none
-
-                interface
-                    integer function interfunc(x)
-                        implicit none
-                        integer, intent(in) :: x
-                    end function
-                end interface
-
-            contains
-                integer function myfunc(x)
-                    integer, intent(in) :: x
-                    myfunc = x * 2
-                end function
-                subroutine mysub(x)
-                    integer, intent(inout) :: x
-                    x = x * 2
-                end subroutine
-            end module
-
-            program my_program
-                implicit none
-
-                write(*,*) 42
-
-            contains
-                integer function myfunc2(x)
-                    integer, intent(in) :: x
-                    myfunc2 = x * 2
-                end function
-                subroutine mysub2(x)
-                    integer, intent(inout) :: x
-                    x = x * 2
-                end subroutine
-            end program
-            ",
-        );
-        let expected: Vec<Diagnostic> = vec![];
-        let actual = SuperfluousImplicitNone::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/typing/init_decls.rs b/fortitude/src/rules/typing/init_decls.rs
index 293e021..edcaa86 100644
--- a/fortitude/src/rules/typing/init_decls.rs
+++ b/fortitude/src/rules/typing/init_decls.rs
@@ -107,62 +107,3 @@ impl AstRule for InitialisationInDeclaration {
         vec!["init_declarator"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_init_decl() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            module test
-              integer :: global = 0  ! Ok at module level
-
-              type :: my_type
-                integer :: component = 1  ! Ok in types
-              end type
-            contains
-
-            subroutine init_decl1()
-              integer :: foo = 1
-            end subroutine init_decl1
-
-            subroutine init_decl2()
-              integer, save :: foo = 1  ! Ok with explicit save
-              integer, parameter :: bar = 2  ! Ok as parameter
-            end subroutine init_decl2
-
-            subroutine init_decl3()
-              integer :: foo, bar = 1, quazz, zapp = 2
-            end subroutine no_init_decl3
-
-            end module test
-            ",
-        );
-        let expected: Vec<_> = [
-            (10, 13, 10, 20, "foo"),
-            (19, 18, 19, 25, "bar"),
-            (19, 34, 19, 42, "zapp"),
-        ]
-        .iter()
-        .map(|(start_line, start_col, end_line, end_col, variable)| {
-            Diagnostic::from_start_end_line_col(
-                InitialisationInDeclaration {
-                    name: variable.to_string(),
-                },
-                &source,
-                *start_line,
-                *start_col,
-                *end_line,
-                *end_col,
-            )
-        })
-        .collect();
-        let actual = InitialisationInDeclaration::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/typing/intent.rs b/fortitude/src/rules/typing/intent.rs
index 0062b23..11117dd 100644
--- a/fortitude/src/rules/typing/intent.rs
+++ b/fortitude/src/rules/typing/intent.rs
@@ -113,54 +113,3 @@ impl AstRule for MissingIntent {
         vec!["function_statement", "subroutine_statement"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_missing_intent() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            integer function foo(a, b, c)
-              use mod
-              integer :: a, c(2), f
-              integer, dimension(:), intent(in) :: b
-            end function
-
-            subroutine bar(d, e, f)
-              integer, pointer :: d
-              integer, allocatable :: e(:, :)
-              type(integer(kind=int64)), intent(inout) :: f
-              integer :: g
-            end subroutine
-            ",
-        );
-        let expected: Vec<_> = [
-            (3, 13, 3, 14, "function", "a"),
-            (3, 16, 3, 20, "function", "c"),
-            (8, 22, 8, 23, "subroutine", "d"),
-            (9, 26, 9, 33, "subroutine", "e"),
-        ]
-        .iter()
-        .map(|(start_line, start_col, end_line, end_col, entity, arg)| {
-            Diagnostic::from_start_end_line_col(
-                MissingIntent {
-                    entity: entity.to_string(),
-                    name: arg.to_string(),
-                },
-                &source,
-                *start_line,
-                *start_col,
-                *end_line,
-                *end_col,
-            )
-        })
-        .collect();
-        let actual = MissingIntent::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/typing/literal_kinds.rs b/fortitude/src/rules/typing/literal_kinds.rs
index f096551..c84d306 100644
--- a/fortitude/src/rules/typing/literal_kinds.rs
+++ b/fortitude/src/rules/typing/literal_kinds.rs
@@ -184,108 +184,3 @@ impl AstRule for LiteralKindSuffix {
         vec!["number_literal"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_literal_kind() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            integer(8) function add_if(x, y, z)
-              integer :: w
-              integer(kind=2), intent(in) :: x
-              integer(i32), intent(in) :: y
-              logical(kind=4), intent(in) :: z
-
-              if (x) then
-                add_if = x + y
-              else
-                add_if = x
-              end if
-            end function
-
-            subroutine complex_mul(x, y)
-              real(8), intent(in) :: x
-              complex(4), intent(inout) :: y
-              real :: z = 0.5
-              y = y * x
-            end subroutine
-
-            complex(real64) function complex_add(x, y)
-              real(real64), intent(in) :: x
-              complex(kind=4), intent(in) :: y
-              complex_add = y + x
-            end function
-            ",
-        );
-        let expected: Vec<_> = [
-            (1, 8, 1, 9, "integer", 8),
-            (3, 15, 3, 16, "integer", 2),
-            (5, 15, 5, 16, "logical", 4),
-            (15, 7, 15, 8, "real", 8),
-            (16, 10, 16, 11, "complex", 4),
-            (23, 15, 23, 16, "complex", 4),
-        ]
-        .iter()
-        .map(
-            |(start_line, start_col, end_line, end_col, kind, literal)| {
-                Diagnostic::from_start_end_line_col(
-                    LiteralKind {
-                        dtype: kind.to_string(),
-                        literal: literal.to_string(),
-                    },
-                    &source,
-                    *start_line,
-                    *start_col,
-                    *end_line,
-                    *end_col,
-                )
-            },
-        )
-        .collect();
-        let actual = LiteralKind::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-
-    #[test]
-    fn test_literal_kind_suffix() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            use, intrinsic :: iso_fortran_env, only: sp => real32, dp => real64
-
-            real(sp), parameter :: x1 = 1.234567_4
-            real(dp), parameter :: x2 = 1.234567_dp
-            real(dp), parameter :: x3 = 1.789d3
-            real(dp), parameter :: x4 = 9.876_8
-            real(sp), parameter :: x5 = 2.468_sp
-            ",
-        );
-        let expected: Vec<_> = [
-            (3, 37, 3, 38, "1.234567_4", "4"),
-            (6, 34, 6, 35, "9.876_8", "8"),
-        ]
-        .iter()
-        .map(|(start_line, start_col, end_line, end_col, num, kind)| {
-            Diagnostic::from_start_end_line_col(
-                LiteralKindSuffix {
-                    literal: num.to_string(),
-                    suffix: kind.to_string(),
-                },
-                &source,
-                *start_line,
-                *start_col,
-                *end_line,
-                *end_col,
-            )
-        })
-        .collect();
-        let actual = LiteralKindSuffix::apply(&source)?;
-        assert_eq!(actual, expected);
-        Ok(())
-    }
-}
diff --git a/fortitude/src/rules/typing/mod.rs b/fortitude/src/rules/typing/mod.rs
index 635f7c5..82cd759 100644
--- a/fortitude/src/rules/typing/mod.rs
+++ b/fortitude/src/rules/typing/mod.rs
@@ -4,3 +4,55 @@ pub mod init_decls;
 pub mod intent;
 pub mod literal_kinds;
 pub mod star_kinds;
+
+#[cfg(test)]
+mod tests {
+    use std::convert::AsRef;
+    use std::path::Path;
+
+    use anyhow::Result;
+    use insta::assert_snapshot;
+    use test_case::test_case;
+
+    use crate::registry::Rule;
+    use crate::settings::default_settings;
+    use crate::test::test_path;
+
+    #[test_case(Rule::ImplicitTyping, Path::new("T001.f90"))]
+    #[test_case(Rule::InterfaceImplicitTyping, Path::new("T002.f90"))]
+    #[test_case(Rule::SuperfluousImplicitNone, Path::new("T003.f90"))]
+    #[test_case(Rule::LiteralKind, Path::new("T011.f90"))]
+    #[test_case(Rule::LiteralKindSuffix, Path::new("T012.f90"))]
+    #[test_case(Rule::StarKind, Path::new("T021.f90"))]
+    #[test_case(Rule::MissingIntent, Path::new("T031.f90"))]
+    #[test_case(Rule::AssumedSize, Path::new("T041.f90"))]
+    #[test_case(Rule::AssumedSizeCharacterIntent, Path::new("T042.f90"))]
+    #[test_case(Rule::DeprecatedAssumedSizeCharacter, Path::new("T043.f90"))]
+    #[test_case(Rule::InitialisationInDeclaration, Path::new("T051.f90"))]
+    fn rules(rule_code: Rule, path: &Path) -> Result<()> {
+        let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
+        let diagnostics = test_path(
+            Path::new("typing").join(path).as_path(),
+            &[rule_code],
+            &default_settings(),
+        )?;
+        assert_snapshot!(snapshot, diagnostics);
+        Ok(())
+    }
+
+    #[test_case(Rule::ImplicitTyping, Path::new("T001_ok.f90"))]
+    #[test_case(Rule::InterfaceImplicitTyping, Path::new("T002_ok.f90"))]
+    #[test_case(Rule::SuperfluousImplicitNone, Path::new("T003_ok.f90"))]
+    fn rules_pass(rule_code: Rule, path: &Path) -> Result<()> {
+        let diagnostics = test_path(
+            Path::new("typing").join(path).as_path(),
+            &[rule_code],
+            &default_settings(),
+        )?;
+        assert!(
+            diagnostics.is_empty(),
+            "Test source has no warnings, but some were raised:\n{diagnostics}"
+        );
+        Ok(())
+    }
+}
diff --git a/fortitude/src/rules/typing/star_kinds.rs b/fortitude/src/rules/typing/star_kinds.rs
index 72e5c87..854024b 100644
--- a/fortitude/src/rules/typing/star_kinds.rs
+++ b/fortitude/src/rules/typing/star_kinds.rs
@@ -64,70 +64,3 @@ impl AstRule for StarKind {
         vec!["intrinsic_type"]
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{test_file, FromStartEndLineCol};
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_star_kind() -> anyhow::Result<()> {
-        let source = test_file(
-            "
-            integer*8 function add_if(x, y, z)
-              integer(kind=2), intent(in) :: x
-              integer *4, intent(in) :: y
-              logical*   4, intent(in) :: z
-              real    * &
-               8 :: t
-
-              if (x == 2) then
-                add_if = x + y
-              else
-                add_if = x
-              end if
-            end function
-
-            subroutine complex_mul(x, real)
-              real * 4, intent(in) :: x
-              complex  *  8, intent(inout) :: real
-              ! This would be a false positive with purely regexp based linting
-              real = real * 8
-            end subroutine
-            ",
-        );
-
-        let expected: Vec<_> = [
-            (1, 7, 1, 9, "integer", "*8", "8"),
-            (3, 10, 3, 12, "integer", "*4", "4"),
-            (4, 9, 4, 14, "logical", "*4", "4"),
-            (5, 10, 6, 4, "real", "*8", "8"),
-            (16, 7, 16, 10, "real", "*4", "4"),
-            (17, 11, 17, 15, "complex", "*8", "8"),
-        ]
-        .iter()
-        .map(
-            |(start_line, start_col, end_line, end_col, dtype, size, kind)| {
-                Diagnostic::from_start_end_line_col(
-                    StarKind {
-                        dtype: dtype.to_string(),
-                        size: size.to_string(),
-                        kind: kind.to_string(),
-                    },
-                    &source,
-                    *start_line,
-                    *start_col,
-                    *end_line,
-                    *end_col,
-                )
-            },
-        )
-        .collect();
-
-        let actual = StarKind::apply(&source)?;
-        assert_eq!(actual, expected);
-
-        Ok(())
-    }
-}


Test patch:
diff --git a/fortitude/resources/test/fixtures/modules/M001.f90 b/fortitude/resources/test/fixtures/modules/M001.f90
new file mode 100644
index 0000000..910a82a
--- /dev/null
+++ b/fortitude/resources/test/fixtures/modules/M001.f90
@@ -0,0 +1,23 @@
+integer function double(x)
+  integer, intent(in) :: x
+  double = 2 * x
+end function double
+
+subroutine triple(x)
+  integer, intent(inout) :: x
+  x = 3 * x
+end subroutine triple
+
+module my_module
+  implicit none
+contains
+  integer function double(x)
+    integer, intent(in) :: x
+    double = 2 * x
+  end function double
+
+  subroutine triple(x)
+    integer, intent(inout) :: x
+    x = 3 * x
+  end subroutine triple
+end module my_module
diff --git a/fortitude/resources/test/fixtures/modules/M011.f90 b/fortitude/resources/test/fixtures/modules/M011.f90
new file mode 100644
index 0000000..d881c4b
--- /dev/null
+++ b/fortitude/resources/test/fixtures/modules/M011.f90
@@ -0,0 +1,4 @@
+module my_module
+  use, intrinsic :: iso_fortran_env, only: real32
+  use, intrinsic :: iso_c_binding
+end module my_module
diff --git a/fortitude/resources/test/fixtures/precision/P001.f90 b/fortitude/resources/test/fixtures/precision/P001.f90
new file mode 100644
index 0000000..2d7d112
--- /dev/null
+++ b/fortitude/resources/test/fixtures/precision/P001.f90
@@ -0,0 +1,15 @@
+program test
+  use, intrinsic :: iso_fortran_env, only: sp => real32, dp => real64
+
+  real(sp), parameter :: x1 = 1.234567
+  real(dp), parameter :: x2 = 1.234567_dp
+  real(dp), parameter :: x3 = 1.789d3 ! rule should ignore d exponentiation
+  real(dp), parameter :: x4 = 9.876
+  real(sp), parameter :: x5 = 2.468_sp
+  real(sp), parameter :: x6 = 2.
+  real(sp), parameter :: x7 = .0
+  real(sp), parameter :: x8 = 1E2
+  real(sp), parameter :: x9 = .1e2
+  real(sp), parameter :: y1 = 1.E2
+  real(sp), parameter :: y2 = 1.2e3
+end program test
diff --git a/fortitude/resources/test/fixtures/precision/P011.f90 b/fortitude/resources/test/fixtures/precision/P011.f90
new file mode 100644
index 0000000..5886150
--- /dev/null
+++ b/fortitude/resources/test/fixtures/precision/P011.f90
@@ -0,0 +1,16 @@
+double precision function double(x)
+  double precision, intent(in) :: x
+  double = 2 * x
+end function double
+
+subroutine triple(x)
+  double precision, intent(inout) :: x
+  x = 3 * x
+end subroutine triple
+
+function complex_mul(x, y)
+  double precision, intent(in) :: x
+  double complex, intent(in) :: y
+  double complex :: complex_mul
+  complex_mul = x * y
+end function complex_mul
diff --git a/fortitude/resources/test/fixtures/precision/P021.f90 b/fortitude/resources/test/fixtures/precision/P021.f90
new file mode 100644
index 0000000..d87b743
--- /dev/null
+++ b/fortitude/resources/test/fixtures/precision/P021.f90
@@ -0,0 +1,9 @@
+real function my_func(a, b, c, d, e)       ! catch
+  real, intent(in) :: a                    ! catch
+  real(4), intent(in) :: b                 ! ignore
+  integer, intent(in) :: c                 ! ignore
+  complex, intent(in) :: d                 ! catch
+  complex(8), intent(in) :: e              ! ignore
+
+  myfunc = a
+end function my_func
diff --git a/fortitude/resources/test/fixtures/style/S001.f90 b/fortitude/resources/test/fixtures/style/S001.f90
new file mode 100644
index 0000000..6746d8b
--- /dev/null
+++ b/fortitude/resources/test/fixtures/style/S001.f90
@@ -0,0 +1,5 @@
+program test
+  use some_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_really_long_module_name, only : integer_working_precision
+  implicit none
+  integer(integer_working_precision), parameter, dimension(1) :: a = [1]
+end program test
diff --git a/fortitude/resources/test/fixtures/style/S001_line_length_20.f90 b/fortitude/resources/test/fixtures/style/S001_line_length_20.f90
new file mode 100644
index 0000000..2911cd8
--- /dev/null
+++ b/fortitude/resources/test/fixtures/style/S001_line_length_20.f90
@@ -0,0 +1,5 @@
+program test
+  use some_really_long_module_name, only : integer_working_precision
+  implicit none
+  integer(integer_working_precision), parameter, dimension(1) :: a = [1]
+end program test
diff --git a/fortitude/resources/test/fixtures/style/S021.f90 b/fortitude/resources/test/fixtures/style/S021.f90
new file mode 100644
index 0000000..a7e632f
--- /dev/null
+++ b/fortitude/resources/test/fixtures/style/S021.f90
@@ -0,0 +1,44 @@
+program test
+  label1: do
+    if (.true.) then
+      EXIT
+    end if
+  end do label1
+
+  label2: do
+    if (.true.) exit
+  end do label2
+
+  label3: do
+    exit label3
+  end do label3
+
+  label4: do
+    if (.true.) exit label4
+  end do label4
+
+  label5: do i = 1, 2
+    do j = 1, 2  ! unnamed inner loop: currently doesn't warn
+      cycle
+    end do
+  end do label5
+
+  label6: do i = 1, 2
+    inner: do j = 1, 2
+      if (.true.) CYCLE ! named inner loop: warns on inner loop
+    end do inner
+  end do label6
+
+  label7: do
+    cycle label7
+  end do label7
+
+  label8: do
+    if (.true.) cycle label8
+  end do label8
+
+  do
+    ! Don't warn on unnamed loops
+    exit
+  end do
+end program test
diff --git a/fortitude/resources/test/fixtures/style/S041.f90 b/fortitude/resources/test/fixtures/style/S041.f90
new file mode 100644
index 0000000..b1daab4
--- /dev/null
+++ b/fortitude/resources/test/fixtures/style/S041.f90
@@ -0,0 +1,14 @@
+program test
+  integer :: a(3) = (/1, 2, 3/)
+  integer :: b(3) = (/ &
+       1, &
+       2, &
+       3 &
+       /)
+  if (.true.) a = (/4, 5, 6/)
+  b(1:3) = (/ &
+       4, &
+       5, &
+       6 &
+       /)
+end program test
diff --git a/fortitude/resources/test/fixtures/style/S051.f90 b/fortitude/resources/test/fixtures/style/S051.f90
new file mode 100644
index 0000000..76173a5
--- /dev/null
+++ b/fortitude/resources/test/fixtures/style/S051.f90
@@ -0,0 +1,7 @@
+program test
+  if (0 .gt. 1) error stop
+  if (1 .le. 0) error stop
+  if (a.eq.b.and.a.ne.b) error stop
+  if (1 == 2) error stop  ! OK
+  if (2 /= 2) error stop  ! OK
+end program test
diff --git a/fortitude/resources/test/fixtures/style/S061.f90 b/fortitude/resources/test/fixtures/style/S061.f90
new file mode 100644
index 0000000..8ded261
--- /dev/null
+++ b/fortitude/resources/test/fixtures/style/S061.f90
@@ -0,0 +1,61 @@
+module mymod1
+  implicit none
+  type mytype
+    integer :: x
+  end type                      ! catch this
+contains
+  subroutine mysub1()
+    write (*,*) 'hello world'
+  end subroutine                ! catch this
+  subroutine mysub2()
+    write (*,*) 'hello world'
+  end subroutine mysub2         ! ignore this
+end                             ! catch this
+module mymod2
+  implicit none
+  type mytype
+    integer :: x
+  end type mytype               ! ignore this
+contains
+  integer function myfunc1()
+    myfunc1 = 1
+  end function                  ! catch this
+  integer function myfunc2()
+    myfunc2 = 1
+  end function myfunc2          ! ignore this
+end module                      ! catch this
+module mymod3
+  interface
+    module function foo() result(x)
+      integer :: x
+    end function foo            ! ignore this
+    module function bar() result(x)
+      integer :: x
+    end function bar            ! ignore this
+    module function baz() result(x)
+      integer :: x
+    end function baz            ! ignore this
+  end interface
+end module mymod3
+submodule (mymod3) mysub1
+contains
+  module procedure foo
+    x = 1
+  end procedure                 ! catch this
+end                             ! catch this
+submodule (mymod3) mysub2
+contains
+  module procedure bar
+    x = 1
+  end procedure bar             ! ignore this
+end submodule                   ! catch this
+submodule (mymod3) mysub3
+contains
+  module procedure baz
+    x = 1
+  end procedure baz             ! ignore this
+end submodule mysub3            ! ignore this
+program myprog
+  implicit none
+  write (*,*) 'hello world'
+end                             ! catch this
diff --git a/fortitude/resources/test/fixtures/style/S101.f90 b/fortitude/resources/test/fixtures/style/S101.f90
new file mode 100644
index 0000000..1b7521a
--- /dev/null
+++ b/fortitude/resources/test/fixtures/style/S101.f90
@@ -0,0 +1,9 @@
+program test  
+  implicit none
+  integer :: a(3) = [ & 
+    1, &
+    2, &
+    3 &
+  ]    
+   
+end program test
diff --git a/fortitude/resources/test/fixtures/typing/T001.f90 b/fortitude/resources/test/fixtures/typing/T001.f90
new file mode 100644
index 0000000..eca3155
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T001.f90
@@ -0,0 +1,7 @@
+module my_module
+  parameter(N = 1)
+end module my_module
+
+program my_program
+  write(*,*) 42
+end program my_program
diff --git a/fortitude/resources/test/fixtures/typing/T001_ok.f90 b/fortitude/resources/test/fixtures/typing/T001_ok.f90
new file mode 100644
index 0000000..473911a
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T001_ok.f90
@@ -0,0 +1,14 @@
+module my_module
+  implicit none
+contains
+  integer function double(x)
+    integer, intent(in) :: x
+    double = 2 * x
+  end function double
+end module my_module
+
+program my_program
+  implicit none
+  integer, paramter :: x = 2
+  write(*,*) x
+end program my_program
diff --git a/fortitude/resources/test/fixtures/typing/T002.f90 b/fortitude/resources/test/fixtures/typing/T002.f90
new file mode 100644
index 0000000..fbd05db
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T002.f90
@@ -0,0 +1,18 @@
+module my_module
+  implicit none
+  interface
+    integer function myfunc(x)
+      integer, intent(in) :: x
+    end function myfunc
+  end interface
+end module my_module
+
+program my_program
+  implicit none
+  interface
+    subroutine myfunc2(x)
+      integer, intent(inout) :: x
+    end subroutine myfunc2
+  end interface
+  write(*,*) 42
+end program my_program
diff --git a/fortitude/resources/test/fixtures/typing/T002_ok.f90 b/fortitude/resources/test/fixtures/typing/T002_ok.f90
new file mode 100644
index 0000000..8e1d3a5
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T002_ok.f90
@@ -0,0 +1,20 @@
+module my_module
+  implicit none
+  interface
+    integer function myfunc(x)
+      implicit none
+      integer, intent(in) :: x
+    end function myfunc
+  end interface
+end module my_module
+
+program my_program
+  implicit none
+  interface
+    subroutine mysub(x)
+      implicit none
+      integer, intent(inout) :: x
+    end subroutine mysub
+  end interface
+  write(*,*) 42
+end program my_program
diff --git a/fortitude/resources/test/fixtures/typing/T003.f90 b/fortitude/resources/test/fixtures/typing/T003.f90
new file mode 100644
index 0000000..1bcaa2c
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T003.f90
@@ -0,0 +1,32 @@
+module my_module
+  implicit none
+contains
+  integer function myfunc(x)
+    implicit none
+    integer, intent(in) :: x
+    myfunc = x * 2
+  end function myfunc
+  subroutine mysub(x)
+    implicit none
+    integer, intent(inout) :: x
+    x = x * 2
+  end subroutine mysub
+end module my_module
+
+program my_program
+  implicit none
+
+  write(*,*) 42
+
+contains
+  integer function myfunc2(x)
+    implicit none
+    integer, intent(in) :: x
+    myfunc2 = x * 2
+  end function myfunc2
+  subroutine mysub2(x)
+    implicit none
+    integer, intent(inout) :: x
+    x = x * 2
+  end subroutine mysub2
+end program my_program
diff --git a/fortitude/resources/test/fixtures/typing/T003_ok.f90 b/fortitude/resources/test/fixtures/typing/T003_ok.f90
new file mode 100644
index 0000000..d6ecb15
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T003_ok.f90
@@ -0,0 +1,36 @@
+module my_module
+  implicit none
+
+  interface
+    integer function interfunc(x)
+      implicit none
+      integer, intent(in) :: x
+    end function interfunc
+  end interface
+
+contains
+  integer function myfunc(x)
+    integer, intent(in) :: x
+    myfunc = x * 2
+  end function myfunc
+  subroutine mysub(x)
+    integer, intent(inout) :: x
+    x = x * 2
+  end subroutine mysub
+end module my_module
+
+program my_program
+  implicit none
+
+  write(*,*) 42
+
+contains
+  integer function myfunc2(x)
+    integer, intent(in) :: x
+    myfunc2 = x * 2
+  end function myfunc2
+  subroutine mysub2(x)
+    integer, intent(inout) :: x
+    x = x * 2
+  end subroutine mysub2
+end program my_program
diff --git a/fortitude/resources/test/fixtures/typing/T011.f90 b/fortitude/resources/test/fixtures/typing/T011.f90
new file mode 100644
index 0000000..d3ebb89
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T011.f90
@@ -0,0 +1,25 @@
+integer(8) function add_if(x, y, z)
+  integer :: w
+  integer(kind=2), intent(in) :: x
+  integer(i32), intent(in) :: y
+  logical(kind=4), intent(in) :: z
+
+  if (x) then
+    add_if = x + y
+  else
+    add_if = x
+  end if
+end function add_if
+
+subroutine complex_mul(x, y)
+  real(8), intent(in) :: x
+  complex(4), intent(inout) :: y
+  real :: z = 0.5
+  y = y * x
+end subroutine complex_mul
+
+complex(real64) function complex_add(x, y)
+  real(real64), intent(in) :: x
+  complex(kind=4), intent(in) :: y
+  complex_add = y + x
+end function complex_add
diff --git a/fortitude/resources/test/fixtures/typing/T012.f90 b/fortitude/resources/test/fixtures/typing/T012.f90
new file mode 100644
index 0000000..3ff8169
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T012.f90
@@ -0,0 +1,9 @@
+program test
+  use, intrinsic :: iso_fortran_env, only: sp => real32, dp => real64
+
+  real(sp), parameter :: x1 = 1.234567_4
+  real(dp), parameter :: x2 = 1.234567_dp
+  real(dp), parameter :: x3 = 1.789d3
+  real(dp), parameter :: x4 = 9.876_8
+  real(sp), parameter :: x5 = 2.468_sp
+end program
diff --git a/fortitude/resources/test/fixtures/typing/T021.f90 b/fortitude/resources/test/fixtures/typing/T021.f90
new file mode 100644
index 0000000..bf62075
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T021.f90
@@ -0,0 +1,20 @@
+integer*8 function add_if(x, y, z)
+  integer(kind=2), intent(in) :: x
+  integer *4, intent(in) :: y
+  logical*   4, intent(in) :: z
+  real    * &
+       8 :: t
+
+  if (x == 2) then
+    add_if = x + y
+  else
+    add_if = x
+  end if
+end function add_if
+
+subroutine complex_mul(x, real)
+  real * 4, intent(in) :: x
+  complex  *  8, intent(inout) :: real
+  ! This would be a false positive with purely regexp based linting
+  real = real * 8
+end subroutine complex_mul
diff --git a/fortitude/resources/test/fixtures/typing/T031.f90 b/fortitude/resources/test/fixtures/typing/T031.f90
new file mode 100644
index 0000000..7b748d4
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T031.f90
@@ -0,0 +1,12 @@
+integer function foo(a, b, c)
+  use mod
+  integer :: a, c(2), f
+  integer, dimension(:), intent(in) :: b
+end function foo
+
+subroutine bar(d, e, f)
+  integer, pointer :: d
+  integer, allocatable :: e(:, :)
+  type(integer(kind=int64)), intent(inout) :: f
+  integer :: g
+end subroutine bar
diff --git a/fortitude/resources/test/fixtures/typing/T041.f90 b/fortitude/resources/test/fixtures/typing/T041.f90
new file mode 100644
index 0000000..c9efdda
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T041.f90
@@ -0,0 +1,13 @@
+subroutine assumed_size_dimension(array, n, m, l, o, p, options, thing, q)
+  integer, intent(in) :: n, m
+  integer, dimension(n, m, *), intent(in) :: array
+  integer, intent(in) :: l(*), o, p(*)
+  ! warning must be on the array part for characters
+  character(len=*), dimension(*) :: options
+  character(*) :: thing(*)
+  ! this is ok
+  character(*), intent(in) :: q
+  ! following are ok because they're parameters
+  integer, dimension(*), parameter :: param = [1, 2, 3]
+  character(*), dimension(*), parameter :: param_char = ['hello']
+end subroutine assumed_size_dimension
diff --git a/fortitude/resources/test/fixtures/typing/T042.f90 b/fortitude/resources/test/fixtures/typing/T042.f90
new file mode 100644
index 0000000..624beb2
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T042.f90
@@ -0,0 +1,19 @@
+program cases
+  ! A char array outside a function or subroutine, no exception
+  character (*) :: autochar_glob
+contains
+  subroutine char_input(autochar_in, autochar_inout, autochar_out, fixedchar)
+    ! A char array with proper intent, no exception
+    character(*), intent(in)       :: autochar_in
+    ! A char array with disallowed intent, exception
+    character(*), intent(inout)    :: autochar_inout
+    ! A char array with disallowed intent, exception
+    character(len=*), intent(out)  :: autochar_out
+    ! A char array not passed as a parameter, no exception
+    character(*)                   :: autochar_var
+    ! A char array with fixed length, no exception
+    character(len=10), intent(out) :: fixedchar
+    ! A declaration with non-intent attribute, no exception
+    character(len=*), parameter :: alt_attr = 'sample'
+  end subroutine char_input
+end program cases
diff --git a/fortitude/resources/test/fixtures/typing/T043.f90 b/fortitude/resources/test/fixtures/typing/T043.f90
new file mode 100644
index 0000000..7da0a50
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T043.f90
@@ -0,0 +1,13 @@
+program cases
+contains
+  subroutine char_input(a, b, c, d, e, f)
+    character * ( * ), intent(in) :: a
+    character*(*), intent(in) :: b
+    character*(len=*), intent(in) :: c
+    character*(3), intent(in) :: d
+    character*(MAX_LEN), intent(in) :: e
+    ! these are ok
+    character(*, kind) :: f
+    character(len=*, kind=4) :: g
+  end subroutine char_input
+end program cases
diff --git a/fortitude/resources/test/fixtures/typing/T051.f90 b/fortitude/resources/test/fixtures/typing/T051.f90
new file mode 100644
index 0000000..860aaf1
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T051.f90
@@ -0,0 +1,22 @@
+module test
+  integer :: global = 0  ! Ok at module level
+
+  type :: my_type
+    integer :: component = 1  ! Ok in types
+  end type my_type
+contains
+
+  subroutine init_decl1()
+    integer :: foo = 1
+  end subroutine init_decl1
+
+  subroutine init_decl2()
+    integer, save :: foo = 1  ! Ok with explicit save
+    integer, parameter :: bar = 2  ! Ok as parameter
+  end subroutine init_decl2
+
+  subroutine init_decl3()
+    integer :: foo, bar = 1, quazz, zapp = 2
+  end subroutine init_decl3
+
+end module test
diff --git a/fortitude/src/rules/modules/snapshots/fortitude__rules__modules__tests__external-function_M001.f90.snap b/fortitude/src/rules/modules/snapshots/fortitude__rules__modules__tests__external-function_M001.f90.snap
new file mode 100644
index 0000000..20781f9
--- /dev/null
+++ b/fortitude/src/rules/modules/snapshots/fortitude__rules__modules__tests__external-function_M001.f90.snap
@@ -0,0 +1,22 @@
+---
+source: fortitude/src/rules/modules/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/modules/M001.f90:1:1: M001 function not contained within (sub)module or program
+  |
+1 | integer function double(x)
+  | ^^^^^^^^^^^^^^^^^^^^^^^^^^ M001
+2 |   integer, intent(in) :: x
+3 |   double = 2 * x
+  |
+
+warning: ./resources/test/fixtures/modules/M001.f90:6:1: M001 subroutine not contained within (sub)module or program
+  |
+4 | end function double
+5 | 
+6 | subroutine triple(x)
+  | ^^^^^^^^^^^^^^^^^^^^ M001
+7 |   integer, intent(inout) :: x
+8 |   x = 3 * x
+  |
diff --git a/fortitude/src/rules/modules/snapshots/fortitude__rules__modules__tests__use-all_M011.f90.snap b/fortitude/src/rules/modules/snapshots/fortitude__rules__modules__tests__use-all_M011.f90.snap
new file mode 100644
index 0000000..dbb7260
--- /dev/null
+++ b/fortitude/src/rules/modules/snapshots/fortitude__rules__modules__tests__use-all_M011.f90.snap
@@ -0,0 +1,13 @@
+---
+source: fortitude/src/rules/modules/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/modules/M011.f90:3:3: M011 'use' statement missing 'only' clause
+  |
+1 | module my_module
+2 |   use, intrinsic :: iso_fortran_env, only: real32
+3 |   use, intrinsic :: iso_c_binding
+  |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ M011
+4 | end module my_module
+  |
diff --git a/fortitude/src/rules/precision/snapshots/fortitude__rules__precision__tests__double-precision_P011.f90.snap b/fortitude/src/rules/precision/snapshots/fortitude__rules__precision__tests__double-precision_P011.f90.snap
new file mode 100644
index 0000000..3676530
--- /dev/null
+++ b/fortitude/src/rules/precision/snapshots/fortitude__rules__precision__tests__double-precision_P011.f90.snap
@@ -0,0 +1,59 @@
+---
+source: fortitude/src/rules/precision/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/precision/P011.f90:1:1: P011 prefer 'real(real64)' to 'double precision' (see 'iso_fortran_env')
+  |
+1 | double precision function double(x)
+  | ^^^^^^^^^^^^^^^^ P011
+2 |   double precision, intent(in) :: x
+3 |   double = 2 * x
+  |
+
+warning: ./resources/test/fixtures/precision/P011.f90:2:3: P011 prefer 'real(real64)' to 'double precision' (see 'iso_fortran_env')
+  |
+1 | double precision function double(x)
+2 |   double precision, intent(in) :: x
+  |   ^^^^^^^^^^^^^^^^ P011
+3 |   double = 2 * x
+4 | end function double
+  |
+
+warning: ./resources/test/fixtures/precision/P011.f90:7:3: P011 prefer 'real(real64)' to 'double precision' (see 'iso_fortran_env')
+  |
+6 | subroutine triple(x)
+7 |   double precision, intent(inout) :: x
+  |   ^^^^^^^^^^^^^^^^ P011
+8 |   x = 3 * x
+9 | end subroutine triple
+  |
+
+warning: ./resources/test/fixtures/precision/P011.f90:12:3: P011 prefer 'real(real64)' to 'double precision' (see 'iso_fortran_env')
+   |
+11 | function complex_mul(x, y)
+12 |   double precision, intent(in) :: x
+   |   ^^^^^^^^^^^^^^^^ P011
+13 |   double complex, intent(in) :: y
+14 |   double complex :: complex_mul
+   |
+
+warning: ./resources/test/fixtures/precision/P011.f90:13:3: P011 prefer 'complex(real64)' to 'double complex' (see 'iso_fortran_env')
+   |
+11 | function complex_mul(x, y)
+12 |   double precision, intent(in) :: x
+13 |   double complex, intent(in) :: y
+   |   ^^^^^^^^^^^^^^ P011
+14 |   double complex :: complex_mul
+15 |   complex_mul = x * y
+   |
+
+warning: ./resources/test/fixtures/precision/P011.f90:14:3: P011 prefer 'complex(real64)' to 'double complex' (see 'iso_fortran_env')
+   |
+12 |   double precision, intent(in) :: x
+13 |   double complex, intent(in) :: y
+14 |   double complex :: complex_mul
+   |   ^^^^^^^^^^^^^^ P011
+15 |   complex_mul = x * y
+16 | end function complex_mul
+   |
diff --git a/fortitude/src/rules/precision/snapshots/fortitude__rules__precision__tests__implicit-real-kind_P021.f90.snap b/fortitude/src/rules/precision/snapshots/fortitude__rules__precision__tests__implicit-real-kind_P021.f90.snap
new file mode 100644
index 0000000..85e19c3
--- /dev/null
+++ b/fortitude/src/rules/precision/snapshots/fortitude__rules__precision__tests__implicit-real-kind_P021.f90.snap
@@ -0,0 +1,30 @@
+---
+source: fortitude/src/rules/precision/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/precision/P021.f90:1:1: P021 real has implicit kind
+  |
+1 | real function my_func(a, b, c, d, e)       ! catch
+  | ^^^^ P021
+2 |   real, intent(in) :: a                    ! catch
+3 |   real(4), intent(in) :: b                 ! ignore
+  |
+
+warning: ./resources/test/fixtures/precision/P021.f90:2:3: P021 real has implicit kind
+  |
+1 | real function my_func(a, b, c, d, e)       ! catch
+2 |   real, intent(in) :: a                    ! catch
+  |   ^^^^ P021
+3 |   real(4), intent(in) :: b                 ! ignore
+4 |   integer, intent(in) :: c                 ! ignore
+  |
+
+warning: ./resources/test/fixtures/precision/P021.f90:5:3: P021 complex has implicit kind
+  |
+3 |   real(4), intent(in) :: b                 ! ignore
+4 |   integer, intent(in) :: c                 ! ignore
+5 |   complex, intent(in) :: d                 ! catch
+  |   ^^^^^^^ P021
+6 |   complex(8), intent(in) :: e              ! ignore
+  |
diff --git a/fortitude/src/rules/precision/snapshots/fortitude__rules__precision__tests__no-real-suffix_P001.f90.snap b/fortitude/src/rules/precision/snapshots/fortitude__rules__precision__tests__no-real-suffix_P001.f90.snap
new file mode 100644
index 0000000..c56eb3d
--- /dev/null
+++ b/fortitude/src/rules/precision/snapshots/fortitude__rules__precision__tests__no-real-suffix_P001.f90.snap
@@ -0,0 +1,83 @@
+---
+source: fortitude/src/rules/precision/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/precision/P001.f90:4:31: P001 real literal 1.234567 missing kind suffix
+  |
+2 |   use, intrinsic :: iso_fortran_env, only: sp => real32, dp => real64
+3 | 
+4 |   real(sp), parameter :: x1 = 1.234567
+  |                               ^^^^^^^^ P001
+5 |   real(dp), parameter :: x2 = 1.234567_dp
+6 |   real(dp), parameter :: x3 = 1.789d3 ! rule should ignore d exponentiation
+  |
+
+warning: ./resources/test/fixtures/precision/P001.f90:7:31: P001 real literal 9.876 missing kind suffix
+  |
+5 |   real(dp), parameter :: x2 = 1.234567_dp
+6 |   real(dp), parameter :: x3 = 1.789d3 ! rule should ignore d exponentiation
+7 |   real(dp), parameter :: x4 = 9.876
+  |                               ^^^^^ P001
+8 |   real(sp), parameter :: x5 = 2.468_sp
+9 |   real(sp), parameter :: x6 = 2.
+  |
+
+warning: ./resources/test/fixtures/precision/P001.f90:9:31: P001 real literal 2. missing kind suffix
+   |
+ 7 |   real(dp), parameter :: x4 = 9.876
+ 8 |   real(sp), parameter :: x5 = 2.468_sp
+ 9 |   real(sp), parameter :: x6 = 2.
+   |                               ^^ P001
+10 |   real(sp), parameter :: x7 = .0
+11 |   real(sp), parameter :: x8 = 1E2
+   |
+
+warning: ./resources/test/fixtures/precision/P001.f90:10:31: P001 real literal .0 missing kind suffix
+   |
+ 8 |   real(sp), parameter :: x5 = 2.468_sp
+ 9 |   real(sp), parameter :: x6 = 2.
+10 |   real(sp), parameter :: x7 = .0
+   |                               ^^ P001
+11 |   real(sp), parameter :: x8 = 1E2
+12 |   real(sp), parameter :: x9 = .1e2
+   |
+
+warning: ./resources/test/fixtures/precision/P001.f90:11:31: P001 real literal 1E2 missing kind suffix
+   |
+ 9 |   real(sp), parameter :: x6 = 2.
+10 |   real(sp), parameter :: x7 = .0
+11 |   real(sp), parameter :: x8 = 1E2
+   |                               ^^^ P001
+12 |   real(sp), parameter :: x9 = .1e2
+13 |   real(sp), parameter :: y1 = 1.E2
+   |
+
+warning: ./resources/test/fixtures/precision/P001.f90:12:31: P001 real literal .1e2 missing kind suffix
+   |
+10 |   real(sp), parameter :: x7 = .0
+11 |   real(sp), parameter :: x8 = 1E2
+12 |   real(sp), parameter :: x9 = .1e2
+   |                               ^^^^ P001
+13 |   real(sp), parameter :: y1 = 1.E2
+14 |   real(sp), parameter :: y2 = 1.2e3
+   |
+
+warning: ./resources/test/fixtures/precision/P001.f90:13:31: P001 real literal 1.E2 missing kind suffix
+   |
+11 |   real(sp), parameter :: x8 = 1E2
+12 |   real(sp), parameter :: x9 = .1e2
+13 |   real(sp), parameter :: y1 = 1.E2
+   |                               ^^^^ P001
+14 |   real(sp), parameter :: y2 = 1.2e3
+15 | end program test
+   |
+
+warning: ./resources/test/fixtures/precision/P001.f90:14:31: P001 real literal 1.2e3 missing kind suffix
+   |
+12 |   real(sp), parameter :: x9 = .1e2
+13 |   real(sp), parameter :: y1 = 1.E2
+14 |   real(sp), parameter :: y2 = 1.2e3
+   |                               ^^^^^ P001
+15 | end program test
+   |
diff --git a/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__deprecated-relational-operator_S051.f90.snap b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__deprecated-relational-operator_S051.f90.snap
new file mode 100644
index 0000000..66f05aa
--- /dev/null
+++ b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__deprecated-relational-operator_S051.f90.snap
@@ -0,0 +1,43 @@
+---
+source: fortitude/src/rules/style/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/style/S051.f90:2:9: S051 deprecated relational operator '.gt.', prefer '>' instead
+  |
+1 | program test
+2 |   if (0 .gt. 1) error stop
+  |         ^^^^ S051
+3 |   if (1 .le. 0) error stop
+4 |   if (a.eq.b.and.a.ne.b) error stop
+  |
+
+warning: ./resources/test/fixtures/style/S051.f90:3:9: S051 deprecated relational operator '.le.', prefer '<=' instead
+  |
+1 | program test
+2 |   if (0 .gt. 1) error stop
+3 |   if (1 .le. 0) error stop
+  |         ^^^^ S051
+4 |   if (a.eq.b.and.a.ne.b) error stop
+5 |   if (1 == 2) error stop  ! OK
+  |
+
+warning: ./resources/test/fixtures/style/S051.f90:4:8: S051 deprecated relational operator '.eq.', prefer '==' instead
+  |
+2 |   if (0 .gt. 1) error stop
+3 |   if (1 .le. 0) error stop
+4 |   if (a.eq.b.and.a.ne.b) error stop
+  |        ^^^^ S051
+5 |   if (1 == 2) error stop  ! OK
+6 |   if (2 /= 2) error stop  ! OK
+  |
+
+warning: ./resources/test/fixtures/style/S051.f90:4:19: S051 deprecated relational operator '.ne.', prefer '/=' instead
+  |
+2 |   if (0 .gt. 1) error stop
+3 |   if (1 .le. 0) error stop
+4 |   if (a.eq.b.and.a.ne.b) error stop
+  |                   ^^^^ S051
+5 |   if (1 == 2) error stop  ! OK
+6 |   if (2 /= 2) error stop  ! OK
+  |
diff --git a/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__line-too-long_S001.f90.snap b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__line-too-long_S001.f90.snap
new file mode 100644
index 0000000..06e00ab
--- /dev/null
+++ b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__line-too-long_S001.f90.snap
@@ -0,0 +1,13 @@
+---
+source: fortitude/src/rules/style/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/style/S001.f90:2:101: S001 line length of 222, exceeds maximum 100
+  |
+1 | ...
+2 | ...ally_really_really_really_really_really_really_really_really_really_really_long_module_name, only : integer_working_precision
+  |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S001
+3 | ...
+4 | ...
+  |
diff --git a/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__line-too-long_S001_line_length_20.f90.snap b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__line-too-long_S001_line_length_20.f90.snap
new file mode 100644
index 0000000..cabb07e
--- /dev/null
+++ b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__line-too-long_S001_line_length_20.f90.snap
@@ -0,0 +1,22 @@
+---
+source: fortitude/src/rules/style/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/style/S001_line_length_20.f90:2:21: S001 line length of 68, exceeds maximum 20
+  |
+1 | program test
+2 |   use some_really_long_module_name, only : integer_working_precision
+  |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S001
+3 |   implicit none
+4 |   integer(integer_working_precision), parameter, dimension(1) :: a = [1]
+  |
+
+warning: ./resources/test/fixtures/style/S001_line_length_20.f90:4:21: S001 line length of 72, exceeds maximum 20
+  |
+2 |   use some_really_long_module_name, only : integer_working_precision
+3 |   implicit none
+4 |   integer(integer_working_precision), parameter, dimension(1) :: a = [1]
+  |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S001
+5 | end program test
+  |
diff --git a/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__missing-exit-or-cycle-label_S021.f90.snap b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__missing-exit-or-cycle-label_S021.f90.snap
new file mode 100644
index 0000000..df3825b
--- /dev/null
+++ b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__missing-exit-or-cycle-label_S021.f90.snap
@@ -0,0 +1,32 @@
+---
+source: fortitude/src/rules/style/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/style/S021.f90:4:7: S021 'exit' statement in named 'do' loop missing label 'label1'
+  |
+2 |   label1: do
+3 |     if (.true.) then
+4 |       EXIT
+  |       ^^^^ S021
+5 |     end if
+6 |   end do label1
+  |
+
+warning: ./resources/test/fixtures/style/S021.f90:9:17: S021 'exit' statement in named 'do' loop missing label 'label2'
+   |
+ 8 |   label2: do
+ 9 |     if (.true.) exit
+   |                 ^^^^ S021
+10 |   end do label2
+   |
+
+warning: ./resources/test/fixtures/style/S021.f90:28:19: S021 'cycle' statement in named 'do' loop missing label 'inner'
+   |
+26 |   label6: do i = 1, 2
+27 |     inner: do j = 1, 2
+28 |       if (.true.) CYCLE ! named inner loop: warns on inner loop
+   |                   ^^^^^ S021
+29 |     end do inner
+30 |   end do label6
+   |
diff --git a/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__old-style-array-literal_S041.f90.snap b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__old-style-array-literal_S041.f90.snap
new file mode 100644
index 0000000..35e596d
--- /dev/null
+++ b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__old-style-array-literal_S041.f90.snap
@@ -0,0 +1,52 @@
+---
+source: fortitude/src/rules/style/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/style/S041.f90:2:21: S041 Array literal uses old-style syntax: prefer `[...]`
+  |
+1 | program test
+2 |   integer :: a(3) = (/1, 2, 3/)
+  |                     ^^^^^^^^^^^ S041
+3 |   integer :: b(3) = (/ &
+4 |        1, &
+  |
+
+warning: ./resources/test/fixtures/style/S041.f90:3:21: S041 Array literal uses old-style syntax: prefer `[...]`
+  |
+1 |   program test
+2 |     integer :: a(3) = (/1, 2, 3/)
+3 |     integer :: b(3) = (/ &
+  |  _____________________^
+4 | |        1, &
+5 | |        2, &
+6 | |        3 &
+7 | |        /)
+  | |_________^ S041
+8 |     if (.true.) a = (/4, 5, 6/)
+9 |     b(1:3) = (/ &
+  |
+
+warning: ./resources/test/fixtures/style/S041.f90:8:19: S041 Array literal uses old-style syntax: prefer `[...]`
+   |
+ 6 |        3 &
+ 7 |        /)
+ 8 |   if (.true.) a = (/4, 5, 6/)
+   |                   ^^^^^^^^^^^ S041
+ 9 |   b(1:3) = (/ &
+10 |        4, &
+   |
+
+warning: ./resources/test/fixtures/style/S041.f90:9:12: S041 Array literal uses old-style syntax: prefer `[...]`
+   |
+ 7 |          /)
+ 8 |     if (.true.) a = (/4, 5, 6/)
+ 9 |     b(1:3) = (/ &
+   |  ____________^
+10 | |        4, &
+11 | |        5, &
+12 | |        6 &
+13 | |        /)
+   | |_________^ S041
+14 |   end program test
+   |
diff --git a/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__trailing-whitespace_S101.f90.snap b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__trailing-whitespace_S101.f90.snap
new file mode 100644
index 0000000..39ad346
--- /dev/null
+++ b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__trailing-whitespace_S101.f90.snap
@@ -0,0 +1,41 @@
+---
+source: fortitude/src/rules/style/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/style/S101.f90:1:13: S101 trailing whitespace
+  |
+1 | program test  
+  |             ^^ S101
+2 |   implicit none
+3 |   integer :: a(3) = [ & 
+  |
+
+warning: ./resources/test/fixtures/style/S101.f90:3:24: S101 trailing whitespace
+  |
+1 | program test  
+2 |   implicit none
+3 |   integer :: a(3) = [ & 
+  |                        ^ S101
+4 |     1, &
+5 |     2, &
+  |
+
+warning: ./resources/test/fixtures/style/S101.f90:7:4: S101 trailing whitespace
+  |
+5 |     2, &
+6 |     3 &
+7 |   ]    
+  |    ^^^^ S101
+8 |    
+9 | end program test
+  |
+
+warning: ./resources/test/fixtures/style/S101.f90:8:1: S101 trailing whitespace
+  |
+6 |     3 &
+7 |   ]    
+8 |    
+  | ^^^ S101
+9 | end program test
+  |
diff --git a/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__unnamed-end-statement_S061.f90.snap b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__unnamed-end-statement_S061.f90.snap
new file mode 100644
index 0000000..5fbd084
--- /dev/null
+++ b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__unnamed-end-statement_S061.f90.snap
@@ -0,0 +1,92 @@
+---
+source: fortitude/src/rules/style/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/style/S061.f90:5:3: S061 end statement should read 'end type mytype'
+  |
+3 |   type mytype
+4 |     integer :: x
+5 |   end type                      ! catch this
+  |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
+6 | contains
+7 |   subroutine mysub1()
+  |
+
+warning: ./resources/test/fixtures/style/S061.f90:9:3: S061 end statement should read 'end subroutine mysub1'
+   |
+ 7 |   subroutine mysub1()
+ 8 |     write (*,*) 'hello world'
+ 9 |   end subroutine                ! catch this
+   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
+10 |   subroutine mysub2()
+11 |     write (*,*) 'hello world'
+   |
+
+warning: ./resources/test/fixtures/style/S061.f90:13:1: S061 end statement should read 'end module mymod1'
+   |
+11 |     write (*,*) 'hello world'
+12 |   end subroutine mysub2         ! ignore this
+13 | end                             ! catch this
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
+14 | module mymod2
+15 |   implicit none
+   |
+
+warning: ./resources/test/fixtures/style/S061.f90:22:3: S061 end statement should read 'end function myfunc1'
+   |
+20 |   integer function myfunc1()
+21 |     myfunc1 = 1
+22 |   end function                  ! catch this
+   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
+23 |   integer function myfunc2()
+24 |     myfunc2 = 1
+   |
+
+warning: ./resources/test/fixtures/style/S061.f90:26:1: S061 end statement should read 'end module mymod2'
+   |
+24 |     myfunc2 = 1
+25 |   end function myfunc2          ! ignore this
+26 | end module                      ! catch this
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
+27 | module mymod3
+28 |   interface
+   |
+
+warning: ./resources/test/fixtures/style/S061.f90:44:3: S061 end statement should read 'end procedure foo'
+   |
+42 |   module procedure foo
+43 |     x = 1
+44 |   end procedure                 ! catch this
+   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
+45 | end                             ! catch this
+46 | submodule (mymod3) mysub2
+   |
+
+warning: ./resources/test/fixtures/style/S061.f90:45:1: S061 end statement should read 'end submodule mysub1'
+   |
+43 |     x = 1
+44 |   end procedure                 ! catch this
+45 | end                             ! catch this
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
+46 | submodule (mymod3) mysub2
+47 | contains
+   |
+
+warning: ./resources/test/fixtures/style/S061.f90:51:1: S061 end statement should read 'end submodule mysub2'
+   |
+49 |     x = 1
+50 |   end procedure bar             ! ignore this
+51 | end submodule                   ! catch this
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
+52 | submodule (mymod3) mysub3
+53 | contains
+   |
+
+warning: ./resources/test/fixtures/style/S061.f90:61:1: S061 end statement should read 'end program myprog'
+   |
+59 |   implicit none
+60 |   write (*,*) 'hello world'
+61 | end                             ! catch this
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
+   |
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__assumed-size-character-intent_T042.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__assumed-size-character-intent_T042.f90.snap
new file mode 100644
index 0000000..52ecfa0
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__assumed-size-character-intent_T042.f90.snap
@@ -0,0 +1,44 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/typing/T042.f90:3:14: T042 character 'autochar_glob' has assumed size but does not have `intent(in)`
+  |
+1 | program cases
+2 |   ! A char array outside a function or subroutine, no exception
+3 |   character (*) :: autochar_glob
+  |              ^ T042
+4 | contains
+5 |   subroutine char_input(autochar_in, autochar_inout, autochar_out, fixedchar)
+  |
+
+warning: ./resources/test/fixtures/typing/T042.f90:9:15: T042 character 'autochar_inout' has assumed size but does not have `intent(in)`
+   |
+ 7 |     character(*), intent(in)       :: autochar_in
+ 8 |     ! A char array with disallowed intent, exception
+ 9 |     character(*), intent(inout)    :: autochar_inout
+   |               ^ T042
+10 |     ! A char array with disallowed intent, exception
+11 |     character(len=*), intent(out)  :: autochar_out
+   |
+
+warning: ./resources/test/fixtures/typing/T042.f90:11:19: T042 character 'autochar_out' has assumed size but does not have `intent(in)`
+   |
+ 9 |     character(*), intent(inout)    :: autochar_inout
+10 |     ! A char array with disallowed intent, exception
+11 |     character(len=*), intent(out)  :: autochar_out
+   |                   ^ T042
+12 |     ! A char array not passed as a parameter, no exception
+13 |     character(*)                   :: autochar_var
+   |
+
+warning: ./resources/test/fixtures/typing/T042.f90:13:15: T042 character 'autochar_var' has assumed size but does not have `intent(in)`
+   |
+11 |     character(len=*), intent(out)  :: autochar_out
+12 |     ! A char array not passed as a parameter, no exception
+13 |     character(*)                   :: autochar_var
+   |               ^ T042
+14 |     ! A char array with fixed length, no exception
+15 |     character(len=10), intent(out) :: fixedchar
+   |
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__assumed-size_T041.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__assumed-size_T041.f90.snap
new file mode 100644
index 0000000..ac26166
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__assumed-size_T041.f90.snap
@@ -0,0 +1,54 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/typing/T041.f90:3:28: T041 'array' has assumed size
+  |
+1 | subroutine assumed_size_dimension(array, n, m, l, o, p, options, thing, q)
+2 |   integer, intent(in) :: n, m
+3 |   integer, dimension(n, m, *), intent(in) :: array
+  |                            ^ T041
+4 |   integer, intent(in) :: l(*), o, p(*)
+5 |   ! warning must be on the array part for characters
+  |
+
+warning: ./resources/test/fixtures/typing/T041.f90:4:28: T041 'l' has assumed size
+  |
+2 |   integer, intent(in) :: n, m
+3 |   integer, dimension(n, m, *), intent(in) :: array
+4 |   integer, intent(in) :: l(*), o, p(*)
+  |                            ^ T041
+5 |   ! warning must be on the array part for characters
+6 |   character(len=*), dimension(*) :: options
+  |
+
+warning: ./resources/test/fixtures/typing/T041.f90:4:37: T041 'p' has assumed size
+  |
+2 |   integer, intent(in) :: n, m
+3 |   integer, dimension(n, m, *), intent(in) :: array
+4 |   integer, intent(in) :: l(*), o, p(*)
+  |                                     ^ T041
+5 |   ! warning must be on the array part for characters
+6 |   character(len=*), dimension(*) :: options
+  |
+
+warning: ./resources/test/fixtures/typing/T041.f90:6:31: T041 'options' has assumed size
+  |
+4 |   integer, intent(in) :: l(*), o, p(*)
+5 |   ! warning must be on the array part for characters
+6 |   character(len=*), dimension(*) :: options
+  |                               ^ T041
+7 |   character(*) :: thing(*)
+8 |   ! this is ok
+  |
+
+warning: ./resources/test/fixtures/typing/T041.f90:7:25: T041 'thing' has assumed size
+  |
+5 |   ! warning must be on the array part for characters
+6 |   character(len=*), dimension(*) :: options
+7 |   character(*) :: thing(*)
+  |                         ^ T041
+8 |   ! this is ok
+9 |   character(*), intent(in) :: q
+  |
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__deprecated-assumed-size-character_T043.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__deprecated-assumed-size-character_T043.f90.snap
new file mode 100644
index 0000000..ec34756
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__deprecated-assumed-size-character_T043.f90.snap
@@ -0,0 +1,54 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/typing/T043.f90:4:15: T043 character 'a' uses deprecated syntax for assumed size
+  |
+2 | contains
+3 |   subroutine char_input(a, b, c, d, e, f)
+4 |     character * ( * ), intent(in) :: a
+  |               ^ T043
+5 |     character*(*), intent(in) :: b
+6 |     character*(len=*), intent(in) :: c
+  |
+
+warning: ./resources/test/fixtures/typing/T043.f90:5:14: T043 character 'b' uses deprecated syntax for assumed size
+  |
+3 |   subroutine char_input(a, b, c, d, e, f)
+4 |     character * ( * ), intent(in) :: a
+5 |     character*(*), intent(in) :: b
+  |              ^ T043
+6 |     character*(len=*), intent(in) :: c
+7 |     character*(3), intent(in) :: d
+  |
+
+warning: ./resources/test/fixtures/typing/T043.f90:6:14: T043 character 'c' uses deprecated syntax for assumed size
+  |
+4 |     character * ( * ), intent(in) :: a
+5 |     character*(*), intent(in) :: b
+6 |     character*(len=*), intent(in) :: c
+  |              ^ T043
+7 |     character*(3), intent(in) :: d
+8 |     character*(MAX_LEN), intent(in) :: e
+  |
+
+warning: ./resources/test/fixtures/typing/T043.f90:7:14: T043 character 'd' uses deprecated syntax for assumed size
+  |
+5 |     character*(*), intent(in) :: b
+6 |     character*(len=*), intent(in) :: c
+7 |     character*(3), intent(in) :: d
+  |              ^ T043
+8 |     character*(MAX_LEN), intent(in) :: e
+9 |     ! these are ok
+  |
+
+warning: ./resources/test/fixtures/typing/T043.f90:8:14: T043 character 'e' uses deprecated syntax for assumed size
+   |
+ 6 |     character*(len=*), intent(in) :: c
+ 7 |     character*(3), intent(in) :: d
+ 8 |     character*(MAX_LEN), intent(in) :: e
+   |              ^ T043
+ 9 |     ! these are ok
+10 |     character(*, kind) :: f
+   |
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__implicit-typing_T001.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__implicit-typing_T001.f90.snap
new file mode 100644
index 0000000..82f54c7
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__implicit-typing_T001.f90.snap
@@ -0,0 +1,22 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/typing/T001.f90:1:1: T001 module missing 'implicit none'
+  |
+1 | module my_module
+  | ^^^^^^^^^^^^^^^^ T001
+2 |   parameter(N = 1)
+3 | end module my_module
+  |
+
+warning: ./resources/test/fixtures/typing/T001.f90:5:1: T001 program missing 'implicit none'
+  |
+3 | end module my_module
+4 | 
+5 | program my_program
+  | ^^^^^^^^^^^^^^^^^^ T001
+6 |   write(*,*) 42
+7 | end program my_program
+  |
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__initialisation-in-declaration_T051.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__initialisation-in-declaration_T051.f90.snap
new file mode 100644
index 0000000..57a91a4
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__initialisation-in-declaration_T051.f90.snap
@@ -0,0 +1,28 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/typing/T051.f90:10:16: T051 'foo' is initialised in its declaration and has no explicit `save` or `parameter` attribute
+   |
+ 9 |   subroutine init_decl1()
+10 |     integer :: foo = 1
+   |                ^^^^^^^ T051
+11 |   end subroutine init_decl1
+   |
+
+warning: ./resources/test/fixtures/typing/T051.f90:19:21: T051 'bar' is initialised in its declaration and has no explicit `save` or `parameter` attribute
+   |
+18 |   subroutine init_decl3()
+19 |     integer :: foo, bar = 1, quazz, zapp = 2
+   |                     ^^^^^^^ T051
+20 |   end subroutine init_decl3
+   |
+
+warning: ./resources/test/fixtures/typing/T051.f90:19:37: T051 'zapp' is initialised in its declaration and has no explicit `save` or `parameter` attribute
+   |
+18 |   subroutine init_decl3()
+19 |     integer :: foo, bar = 1, quazz, zapp = 2
+   |                                     ^^^^^^^^ T051
+20 |   end subroutine init_decl3
+   |
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__interface-implicit-typing_T002.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__interface-implicit-typing_T002.f90.snap
new file mode 100644
index 0000000..64cf160
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__interface-implicit-typing_T002.f90.snap
@@ -0,0 +1,24 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/typing/T002.f90:4:5: T002 interface 'function' missing 'implicit none'
+  |
+2 |   implicit none
+3 |   interface
+4 |     integer function myfunc(x)
+  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^ T002
+5 |       integer, intent(in) :: x
+6 |     end function myfunc
+  |
+
+warning: ./resources/test/fixtures/typing/T002.f90:13:5: T002 interface 'subroutine' missing 'implicit none'
+   |
+11 |   implicit none
+12 |   interface
+13 |     subroutine myfunc2(x)
+   |     ^^^^^^^^^^^^^^^^^^^^^ T002
+14 |       integer, intent(inout) :: x
+15 |     end subroutine myfunc2
+   |
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__literal-kind-suffix_T012.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__literal-kind-suffix_T012.f90.snap
new file mode 100644
index 0000000..cc29696
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__literal-kind-suffix_T012.f90.snap
@@ -0,0 +1,24 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/typing/T012.f90:4:40: T012 '1.234567_4' has literal suffix '4', use 'iso_fortran_env' parameter
+  |
+2 |   use, intrinsic :: iso_fortran_env, only: sp => real32, dp => real64
+3 | 
+4 |   real(sp), parameter :: x1 = 1.234567_4
+  |                                        ^ T012
+5 |   real(dp), parameter :: x2 = 1.234567_dp
+6 |   real(dp), parameter :: x3 = 1.789d3
+  |
+
+warning: ./resources/test/fixtures/typing/T012.f90:7:37: T012 '9.876_8' has literal suffix '8', use 'iso_fortran_env' parameter
+  |
+5 |   real(dp), parameter :: x2 = 1.234567_dp
+6 |   real(dp), parameter :: x3 = 1.789d3
+7 |   real(dp), parameter :: x4 = 9.876_8
+  |                                     ^ T012
+8 |   real(sp), parameter :: x5 = 2.468_sp
+9 | end program
+  |
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__literal-kind_T011.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__literal-kind_T011.f90.snap
new file mode 100644
index 0000000..9d6044d
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__literal-kind_T011.f90.snap
@@ -0,0 +1,61 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/typing/T011.f90:1:9: T011 integer kind set with number literal '8', use 'iso_fortran_env' parameter
+  |
+1 | integer(8) function add_if(x, y, z)
+  |         ^ T011
+2 |   integer :: w
+3 |   integer(kind=2), intent(in) :: x
+  |
+
+warning: ./resources/test/fixtures/typing/T011.f90:3:16: T011 integer kind set with number literal '2', use 'iso_fortran_env' parameter
+  |
+1 | integer(8) function add_if(x, y, z)
+2 |   integer :: w
+3 |   integer(kind=2), intent(in) :: x
+  |                ^ T011
+4 |   integer(i32), intent(in) :: y
+5 |   logical(kind=4), intent(in) :: z
+  |
+
+warning: ./resources/test/fixtures/typing/T011.f90:5:16: T011 logical kind set with number literal '4', use 'iso_fortran_env' parameter
+  |
+3 |   integer(kind=2), intent(in) :: x
+4 |   integer(i32), intent(in) :: y
+5 |   logical(kind=4), intent(in) :: z
+  |                ^ T011
+6 | 
+7 |   if (x) then
+  |
+
+warning: ./resources/test/fixtures/typing/T011.f90:15:8: T011 real kind set with number literal '8', use 'iso_fortran_env' parameter
+   |
+14 | subroutine complex_mul(x, y)
+15 |   real(8), intent(in) :: x
+   |        ^ T011
+16 |   complex(4), intent(inout) :: y
+17 |   real :: z = 0.5
+   |
+
+warning: ./resources/test/fixtures/typing/T011.f90:16:11: T011 complex kind set with number literal '4', use 'iso_fortran_env' parameter
+   |
+14 | subroutine complex_mul(x, y)
+15 |   real(8), intent(in) :: x
+16 |   complex(4), intent(inout) :: y
+   |           ^ T011
+17 |   real :: z = 0.5
+18 |   y = y * x
+   |
+
+warning: ./resources/test/fixtures/typing/T011.f90:23:16: T011 complex kind set with number literal '4', use 'iso_fortran_env' parameter
+   |
+21 | complex(real64) function complex_add(x, y)
+22 |   real(real64), intent(in) :: x
+23 |   complex(kind=4), intent(in) :: y
+   |                ^ T011
+24 |   complex_add = y + x
+25 | end function complex_add
+   |
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__missing-intent_T031.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__missing-intent_T031.f90.snap
new file mode 100644
index 0000000..08b234d
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__missing-intent_T031.f90.snap
@@ -0,0 +1,43 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/typing/T031.f90:3:14: T031 function argument 'a' missing 'intent' attribute
+  |
+1 | integer function foo(a, b, c)
+2 |   use mod
+3 |   integer :: a, c(2), f
+  |              ^ T031
+4 |   integer, dimension(:), intent(in) :: b
+5 | end function foo
+  |
+
+warning: ./resources/test/fixtures/typing/T031.f90:3:17: T031 function argument 'c' missing 'intent' attribute
+  |
+1 | integer function foo(a, b, c)
+2 |   use mod
+3 |   integer :: a, c(2), f
+  |                 ^^^^ T031
+4 |   integer, dimension(:), intent(in) :: b
+5 | end function foo
+  |
+
+warning: ./resources/test/fixtures/typing/T031.f90:8:23: T031 subroutine argument 'd' missing 'intent' attribute
+   |
+ 7 | subroutine bar(d, e, f)
+ 8 |   integer, pointer :: d
+   |                       ^ T031
+ 9 |   integer, allocatable :: e(:, :)
+10 |   type(integer(kind=int64)), intent(inout) :: f
+   |
+
+warning: ./resources/test/fixtures/typing/T031.f90:9:27: T031 subroutine argument 'e' missing 'intent' attribute
+   |
+ 7 | subroutine bar(d, e, f)
+ 8 |   integer, pointer :: d
+ 9 |   integer, allocatable :: e(:, :)
+   |                           ^^^^^^^ T031
+10 |   type(integer(kind=int64)), intent(inout) :: f
+11 |   integer :: g
+   |
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__star-kind_T021.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__star-kind_T021.f90.snap
new file mode 100644
index 0000000..96f8efb
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__star-kind_T021.f90.snap
@@ -0,0 +1,63 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/typing/T021.f90:1:8: T021 integer*8 is non-standard, use integer(8)
+  |
+1 | integer*8 function add_if(x, y, z)
+  |        ^^ T021
+2 |   integer(kind=2), intent(in) :: x
+3 |   integer *4, intent(in) :: y
+  |
+
+warning: ./resources/test/fixtures/typing/T021.f90:3:11: T021 integer*4 is non-standard, use integer(4)
+  |
+1 | integer*8 function add_if(x, y, z)
+2 |   integer(kind=2), intent(in) :: x
+3 |   integer *4, intent(in) :: y
+  |           ^^ T021
+4 |   logical*   4, intent(in) :: z
+5 |   real    * &
+  |
+
+warning: ./resources/test/fixtures/typing/T021.f90:4:10: T021 logical*4 is non-standard, use logical(4)
+  |
+2 |   integer(kind=2), intent(in) :: x
+3 |   integer *4, intent(in) :: y
+4 |   logical*   4, intent(in) :: z
+  |          ^^^^^ T021
+5 |   real    * &
+6 |        8 :: t
+  |
+
+warning: ./resources/test/fixtures/typing/T021.f90:5:11: T021 real*8 is non-standard, use real(8)
+  |
+3 |     integer *4, intent(in) :: y
+4 |     logical*   4, intent(in) :: z
+5 |     real    * &
+  |  ___________^
+6 | |        8 :: t
+  | |________^ T021
+7 |   
+8 |     if (x == 2) then
+  |
+
+warning: ./resources/test/fixtures/typing/T021.f90:16:8: T021 real*4 is non-standard, use real(4)
+   |
+15 | subroutine complex_mul(x, real)
+16 |   real * 4, intent(in) :: x
+   |        ^^^ T021
+17 |   complex  *  8, intent(inout) :: real
+18 |   ! This would be a false positive with purely regexp based linting
+   |
+
+warning: ./resources/test/fixtures/typing/T021.f90:17:12: T021 complex*8 is non-standard, use complex(8)
+   |
+15 | subroutine complex_mul(x, real)
+16 |   real * 4, intent(in) :: x
+17 |   complex  *  8, intent(inout) :: real
+   |            ^^^^ T021
+18 |   ! This would be a false positive with purely regexp based linting
+19 |   real = real * 8
+   |
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__superfluous-implicit-none_T003.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__superfluous-implicit-none_T003.f90.snap
new file mode 100644
index 0000000..789decd
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__superfluous-implicit-none_T003.f90.snap
@@ -0,0 +1,48 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+warning: ./resources/test/fixtures/typing/T003.f90:5:5: T003 'implicit none' set on the enclosing module
+  |
+3 | contains
+4 |   integer function myfunc(x)
+5 |     implicit none
+  |     ^^^^^^^^^^^^^ T003
+6 |     integer, intent(in) :: x
+7 |     myfunc = x * 2
+  |
+  = help: Remove unnecessary 'implicit none'
+
+warning: ./resources/test/fixtures/typing/T003.f90:10:5: T003 'implicit none' set on the enclosing module
+   |
+ 8 |   end function myfunc
+ 9 |   subroutine mysub(x)
+10 |     implicit none
+   |     ^^^^^^^^^^^^^ T003
+11 |     integer, intent(inout) :: x
+12 |     x = x * 2
+   |
+   = help: Remove unnecessary 'implicit none'
+
+warning: ./resources/test/fixtures/typing/T003.f90:23:5: T003 'implicit none' set on the enclosing program
+   |
+21 | contains
+22 |   integer function myfunc2(x)
+23 |     implicit none
+   |     ^^^^^^^^^^^^^ T003
+24 |     integer, intent(in) :: x
+25 |     myfunc2 = x * 2
+   |
+   = help: Remove unnecessary 'implicit none'
+
+warning: ./resources/test/fixtures/typing/T003.f90:28:5: T003 'implicit none' set on the enclosing program
+   |
+26 |   end function myfunc2
+27 |   subroutine mysub2(x)
+28 |     implicit none
+   |     ^^^^^^^^^^^^^ T003
+29 |     integer, intent(inout) :: x
+30 |     x = x * 2
+   |
+   = help: Remove unnecessary 'implicit none'
diff --git a/fortitude/src/test.rs b/fortitude/src/test.rs
new file mode 100644
index 0000000..40405bc
--- /dev/null
+++ b/fortitude/src/test.rs
@@ -0,0 +1,65 @@
+use std::path::Path;
+
+use anyhow::Result;
+use itertools::join;
+use ruff_source_file::{SourceFile, SourceFileBuilder};
+
+use crate::{
+    check::{
+        ast_entrypoint_map, check_file, read_to_string, rules_to_path_rules, rules_to_text_rules,
+    },
+    rules::Rule,
+    settings::Settings,
+    DiagnosticMessage,
+};
+
+pub(crate) fn test_resource_path(path: impl AsRef<Path>) -> std::path::PathBuf {
+    Path::new("./resources/test/").join(path)
+}
+
+/// Run [`check_path`] on a Fortran file in the `resources/test/fixtures` directory.
+pub(crate) fn test_path(
+    path: impl AsRef<Path>,
+    rules: &[Rule],
+    settings: &Settings,
+) -> Result<String> {
+    let path = test_resource_path("fixtures").join(path);
+    let source = read_to_string(&path)?;
+    let filename = path.to_string_lossy();
+    let file = SourceFileBuilder::new(filename.as_ref(), source.as_str()).finish();
+    Ok(test_contents(&path, &file, rules, settings))
+}
+
+pub(crate) fn test_contents(
+    path: &Path,
+    file: &SourceFile,
+    rules: &[Rule],
+    settings: &Settings,
+) -> String {
+    let path_rules = rules_to_path_rules(rules);
+    let text_rules = rules_to_text_rules(rules);
+    let ast_entrypoints = ast_entrypoint_map(rules);
+
+    match check_file(
+        &path_rules,
+        &text_rules,
+        &ast_entrypoints,
+        path,
+        file,
+        settings,
+    ) {
+        Ok(violations) => {
+            if violations.is_empty() {
+                return String::new();
+            }
+            let strings = violations
+                .into_iter()
+                .map(|v| DiagnosticMessage::from_ruff(file, v))
+                .map(|d| d.to_string());
+            join(strings, "\n")
+        }
+        Err(msg) => {
+            panic!("Failed to process: {msg}");
+        }
+    }
+}