use std::fs; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicUsize, Ordering}; use nix_style_linter::RunStatus; static TEST_DIRECTORY_ID: AtomicUsize = AtomicUsize::new(0); #[test] fn malformed_file_reports_parse_error() { let workspace = TestWorkspace::new("malformed_file_reports_parse_error"); let path = workspace.write("broken.nix", "let x = ; in x\n"); let output = run_linter(&[path]); assert_eq!(output.status, RunStatus::Diagnostics); assert_eq!(output.stderr, ""); assert!(output.stdout.starts_with(&format!( "{}:1:9: parse-error: unexpected TOKEN_SEMICOLON at 8..9", workspace.path("broken.nix").display() ))); assert!(!output.stdout.contains("let-order")); } #[test] fn forward_reference_reports_let_order() { let workspace = TestWorkspace::new("forward_reference_reports_let_order"); let path = workspace.write( "forward.nix", "let\n app = package;\n package = \"hello\";\nin app\n", ); let output = run_linter(&[path]); assert_eq!(output.status, RunStatus::Diagnostics); assert_eq!(output.stderr, ""); assert_eq!( output.stdout, format!( "{}:2:3: let-order: `app` references later let binding `package`\n", workspace.path("forward.nix").display() ) ); } #[test] fn dependency_ordered_bindings_exit_clean() { let workspace = TestWorkspace::new("dependency_ordered_bindings_exit_clean"); let path = workspace.write( "ordered.nix", "let\n package = \"hello\";\n app = package;\nin app\n", ); let output = run_linter(&[path]); assert_eq!(output.status, RunStatus::Clean); assert_eq!(output.stdout, ""); assert_eq!(output.stderr, ""); } #[test] fn outer_scope_reference_exits_clean() { let workspace = TestWorkspace::new("outer_scope_reference_exits_clean"); let path = workspace.write( "outer.nix", "let\n package = \"hello\";\nin\nlet\n app = package;\nin app\n", ); let output = run_linter(&[path]); assert_eq!(output.status, RunStatus::Clean); assert_eq!(output.stdout, ""); assert_eq!(output.stderr, ""); } #[test] fn nested_lambda_parameter_shadows_later_binding() { let workspace = TestWorkspace::new("nested_lambda_parameter_shadows_later_binding"); let path = workspace.write( "lambda.nix", "let\n call = package: package;\n package = \"hello\";\nin call\n", ); let output = run_linter(&[path]); assert_eq!(output.status, RunStatus::Clean); assert_eq!(output.stdout, ""); assert_eq!(output.stderr, ""); } #[test] fn nested_let_binding_shadows_later_binding() { let workspace = TestWorkspace::new("nested_let_binding_shadows_later_binding"); let path = workspace.write( "nested-let.nix", "let\n app = let package = \"inner\"; in package;\n package = \"outer\";\nin app\n", ); let output = run_linter(&[path]); assert_eq!(output.status, RunStatus::Clean); assert_eq!(output.stdout, ""); assert_eq!(output.stderr, ""); } #[test] fn earlier_sibling_constant_shared_by_multiple_bindings_exits_clean() { let workspace = TestWorkspace::new("earlier_sibling_constant_shared_by_multiple_bindings"); let path = workspace.write( "shared.nix", "let\n package = \"hello\";\n app = package;\n shell = package;\nin [ app shell ]\n", ); let output = run_linter(&[path]); assert_eq!(output.status, RunStatus::Clean); assert_eq!(output.stdout, ""); assert_eq!(output.stderr, ""); } #[test] fn previous_line_suppression_disables_let_order() { let workspace = TestWorkspace::new("previous_line_suppression_disables_let_order"); let path = workspace.write( "suppressed.nix", "let\n # nix-style-linter: disable-next-line let-order -- intentional recursion\n app = package;\n package = app;\nin app\n", ); let output = run_linter(&[path]); assert_eq!(output.status, RunStatus::Clean); assert_eq!(output.stdout, ""); assert_eq!(output.stderr, ""); } #[test] fn malformed_suppression_does_not_disable_let_order() { let workspace = TestWorkspace::new("malformed_suppression_does_not_disable_let_order"); let path = workspace.write( "unsuppressed.nix", "let\n # nix-style-linter: disable-next-line let-order\n app = package;\n package = \"hello\";\nin app\n", ); let output = run_linter(&[path]); assert_eq!(output.status, RunStatus::Diagnostics); assert_eq!( output.stdout, format!( "{}:3:3: let-order: `app` references later let binding `package`\n", workspace.path("unsuppressed.nix").display() ) ); assert_eq!(output.stderr, ""); } #[test] fn unsupported_binding_syntax_is_skipped() { let workspace = TestWorkspace::new("unsupported_binding_syntax_is_skipped"); let path = workspace.write( "unsupported.nix", "let\n attrs.value = package;\n inherit package;\n package = \"hello\";\nin attrs\n", ); let output = run_linter(&[path]); assert_eq!(output.status, RunStatus::Clean); assert_eq!(output.stdout, ""); assert_eq!(output.stderr, ""); } #[test] fn valid_file_exits_clean() { let workspace = TestWorkspace::new("valid_file_exits_clean"); let path = workspace.write("valid.nix", "let x = 1; in x\n"); let output = run_linter(&[path]); assert_eq!(output.status, RunStatus::Clean); assert_eq!(output.stdout, ""); assert_eq!(output.stderr, ""); } #[test] fn missing_path_reports_tool_error() { let workspace = TestWorkspace::new("missing_path_reports_tool_error"); let path = workspace.path("missing.nix"); let output = run_linter(&[path.clone()]); assert_eq!(output.status, RunStatus::ToolError); assert_eq!(output.stdout, ""); assert!(output.stderr.starts_with(&format!( "error: failed to inspect {}:", path.display() ))); } #[test] fn directory_inputs_recurse_through_nix_files() { let workspace = TestWorkspace::new("directory_inputs_recurse_through_nix_files"); workspace.write("ignored.txt", "let x = ; in x\n"); workspace.write("nested/broken.nix", "let\n x = 1;\n y = ;\nin y\n"); let output = run_linter(&[workspace.root.clone()]); assert_eq!(output.status, RunStatus::Diagnostics); assert_eq!(output.stderr, ""); assert!(output.stdout.starts_with(&format!( "{}:3:7: parse-error: unexpected TOKEN_SEMICOLON at 19..20", workspace.path("nested/broken.nix").display() ))); assert!(!output.stdout.contains("ignored.txt")); } #[test] fn empty_invocation_reports_tool_error() { let output = run_linter(&[]); assert_eq!(output.status, RunStatus::ToolError); assert_eq!(output.stdout, ""); assert_eq!(output.stderr, "error: expected at least one path\n"); } struct LinterOutput { status: RunStatus, stdout: String, stderr: String, } fn run_linter(paths: &[PathBuf]) -> LinterOutput { let mut stdout = Vec::new(); let mut stderr = Vec::new(); let status = nix_style_linter::run(paths, &mut stdout, &mut stderr).unwrap(); LinterOutput { status, stdout: String::from_utf8(stdout).unwrap(), stderr: String::from_utf8(stderr).unwrap(), } } struct TestWorkspace { root: PathBuf, } impl TestWorkspace { fn new(name: &str) -> Self { let id = TEST_DIRECTORY_ID.fetch_add(1, Ordering::Relaxed); let root = std::env::temp_dir().join(format!( "nix-style-linter-{process}-{name}-{id}", process = std::process::id() )); if root.exists() { fs::remove_dir_all(&root).unwrap(); } fs::create_dir_all(&root).unwrap(); Self { root } } fn path(&self, relative: impl AsRef) -> PathBuf { self.root.join(relative) } fn write(&self, relative: impl AsRef, contents: &str) -> PathBuf { let path = self.path(relative); if let Some(parent) = path.parent() { fs::create_dir_all(parent).unwrap(); } fs::write(&path, contents).unwrap(); path } } impl Drop for TestWorkspace { fn drop(&mut self) { fs::remove_dir_all(&self.root).unwrap(); } }