diff options
Diffstat (limited to 'nixpkgs/pkgs/test/nixpkgs-check-by-name/src')
-rw-r--r-- | nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.nix | 59 | ||||
-rw-r--r-- | nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.rs | 124 | ||||
-rw-r--r-- | nixpkgs/pkgs/test/nixpkgs-check-by-name/src/main.rs | 171 | ||||
-rw-r--r-- | nixpkgs/pkgs/test/nixpkgs-check-by-name/src/references.rs | 184 | ||||
-rw-r--r-- | nixpkgs/pkgs/test/nixpkgs-check-by-name/src/structure.rs | 152 | ||||
-rw-r--r-- | nixpkgs/pkgs/test/nixpkgs-check-by-name/src/utils.rs | 72 |
6 files changed, 762 insertions, 0 deletions
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.nix b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.nix new file mode 100644 index 000000000000..7c0ae755215a --- /dev/null +++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.nix @@ -0,0 +1,59 @@ +# Takes a path to nixpkgs and a path to the json-encoded list of attributes to check. +# Returns an attribute set containing information on each requested attribute. +# If the attribute is missing from Nixpkgs it's also missing from the result. +# +# The returned information is an attribute set with: +# - call_package_path: The <path> from `<attr> = callPackage <path> { ... }`, +# or null if it's not defined as with callPackage, or if the <path> is not a path +# - is_derivation: The result of `lib.isDerivation <attr>` +{ + attrsPath, + nixpkgsPath, +}: +let + attrs = builtins.fromJSON (builtins.readFile attrsPath); + + # This overlay mocks callPackage to persist the path of the first argument + callPackageOverlay = self: super: { + callPackage = fn: args: + let + result = super.callPackage fn args; + in + if builtins.isAttrs result then + # If this was the last overlay to be applied, we could just only return the `_callPackagePath`, + # but that's not the case because stdenv has another overlays on top of user-provided ones. + # So to not break the stdenv build we need to return the mostly proper result here + result // { + _callPackagePath = fn; + } + else + # It's very rare that callPackage doesn't return an attribute set, but it can occur. + { + _callPackagePath = fn; + }; + }; + + pkgs = import nixpkgsPath { + # Don't let the users home directory influence this result + config = { }; + overlays = [ callPackageOverlay ]; + }; + + attrInfo = attr: { + # These names are used by the deserializer on the Rust side + call_package_path = + if pkgs.${attr} ? _callPackagePath && builtins.isPath pkgs.${attr}._callPackagePath then + toString pkgs.${attr}._callPackagePath + else + null; + is_derivation = pkgs.lib.isDerivation pkgs.${attr}; + }; + + attrInfos = builtins.listToAttrs (map (name: { + inherit name; + value = attrInfo name; + }) attrs); + +in +# Filter out attributes not in Nixpkgs +builtins.intersectAttrs pkgs attrInfos diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.rs new file mode 100644 index 000000000000..d084642ffe7e --- /dev/null +++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.rs @@ -0,0 +1,124 @@ +use crate::structure; +use crate::utils::ErrorWriter; +use std::path::Path; + +use anyhow::Context; +use serde::Deserialize; +use std::collections::HashMap; +use std::io; +use std::path::PathBuf; +use std::process; +use tempfile::NamedTempFile; + +/// Attribute set of this structure is returned by eval.nix +#[derive(Deserialize)] +struct AttributeInfo { + call_package_path: Option<PathBuf>, + is_derivation: bool, +} + +const EXPR: &str = include_str!("eval.nix"); + +/// Check that the Nixpkgs attribute values corresponding to the packages in pkgs/by-name are +/// of the form `callPackage <package_file> { ... }`. +/// See the `eval.nix` file for how this is achieved on the Nix side +pub fn check_values<W: io::Write>( + error_writer: &mut ErrorWriter<W>, + nixpkgs: &structure::Nixpkgs, + eval_accessible_paths: Vec<&Path>, +) -> anyhow::Result<()> { + // Write the list of packages we need to check into a temporary JSON file. + // This can then get read by the Nix evaluation. + let attrs_file = NamedTempFile::new().context("Failed to create a temporary file")?; + serde_json::to_writer(&attrs_file, &nixpkgs.package_names).context(format!( + "Failed to serialise the package names to the temporary path {}", + attrs_file.path().display() + ))?; + + // With restrict-eval, only paths in NIX_PATH can be accessed, so we explicitly specify the + // ones needed needed + + let mut command = process::Command::new("nix-instantiate"); + command + // Inherit stderr so that error messages always get shown + .stderr(process::Stdio::inherit()) + // Clear NIX_PATH to be sure it doesn't influence the result + .env_remove("NIX_PATH") + .args([ + "--eval", + "--json", + "--strict", + "--readonly-mode", + "--restrict-eval", + "--show-trace", + "--expr", + EXPR, + ]) + // Pass the path to the attrs_file as an argument and add it to the NIX_PATH so it can be + // accessed in restrict-eval mode + .args(["--arg", "attrsPath"]) + .arg(attrs_file.path()) + .arg("-I") + .arg(attrs_file.path()) + // Same for the nixpkgs to test + .args(["--arg", "nixpkgsPath"]) + .arg(&nixpkgs.path) + .arg("-I") + .arg(&nixpkgs.path); + + // Also add extra paths that need to be accessible + for path in eval_accessible_paths { + command.arg("-I"); + command.arg(path); + } + + let result = command + .output() + .context(format!("Failed to run command {command:?}"))?; + + if !result.status.success() { + anyhow::bail!("Failed to run command {command:?}"); + } + // Parse the resulting JSON value + let actual_files: HashMap<String, AttributeInfo> = serde_json::from_slice(&result.stdout) + .context(format!( + "Failed to deserialise {}", + String::from_utf8_lossy(&result.stdout) + ))?; + + for package_name in &nixpkgs.package_names { + let relative_package_file = structure::Nixpkgs::relative_file_for_package(package_name); + let absolute_package_file = nixpkgs.path.join(&relative_package_file); + + if let Some(attribute_info) = actual_files.get(package_name) { + let is_expected_file = + if let Some(call_package_path) = &attribute_info.call_package_path { + absolute_package_file == *call_package_path + } else { + false + }; + + if !is_expected_file { + error_writer.write(&format!( + "pkgs.{package_name}: This attribute is not defined as `pkgs.callPackage {} {{ ... }}`.", + relative_package_file.display() + ))?; + continue; + } + + if !attribute_info.is_derivation { + error_writer.write(&format!( + "pkgs.{package_name}: This attribute defined by {} is not a derivation", + relative_package_file.display() + ))?; + } + } else { + error_writer.write(&format!( + "pkgs.{package_name}: This attribute is not defined but it should be defined automatically as {}", + relative_package_file.display() + ))?; + continue; + } + } + Ok(()) +} diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/main.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/main.rs new file mode 100644 index 000000000000..db22e524553b --- /dev/null +++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/main.rs @@ -0,0 +1,171 @@ +mod eval; +mod references; +mod structure; +mod utils; + +use anyhow::Context; +use clap::Parser; +use colored::Colorize; +use std::io; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; +use structure::Nixpkgs; +use utils::ErrorWriter; + +/// Program to check the validity of pkgs/by-name +#[derive(Parser, Debug)] +#[command(about)] +struct Args { + /// Path to nixpkgs + nixpkgs: PathBuf, +} + +fn main() -> ExitCode { + let args = Args::parse(); + match check_nixpkgs(&args.nixpkgs, vec![], &mut io::stderr()) { + Ok(true) => { + eprintln!("{}", "Validated successfully".green()); + ExitCode::SUCCESS + } + Ok(false) => { + eprintln!("{}", "Validation failed, see above errors".yellow()); + ExitCode::from(1) + } + Err(e) => { + eprintln!("{} {:#}", "I/O error: ".yellow(), e); + ExitCode::from(2) + } + } +} + +/// Checks whether the pkgs/by-name structure in Nixpkgs is valid. +/// +/// # Arguments +/// - `nixpkgs_path`: The path to the Nixpkgs to check +/// - `eval_accessible_paths`: +/// Extra paths that need to be accessible to evaluate Nixpkgs using `restrict-eval`. +/// This is used to allow the tests to access the mock-nixpkgs.nix file +/// - `error_writer`: An `io::Write` value to write validation errors to, if any. +/// +/// # Return value +/// - `Err(e)` if an I/O-related error `e` occurred. +/// - `Ok(false)` if the structure is invalid, all the structural errors have been written to `error_writer`. +/// - `Ok(true)` if the structure is valid, nothing will have been written to `error_writer`. +pub fn check_nixpkgs<W: io::Write>( + nixpkgs_path: &Path, + eval_accessible_paths: Vec<&Path>, + error_writer: &mut W, +) -> anyhow::Result<bool> { + let nixpkgs_path = nixpkgs_path.canonicalize().context(format!( + "Nixpkgs path {} could not be resolved", + nixpkgs_path.display() + ))?; + + // Wraps the error_writer to print everything in red, and tracks whether anything was printed + // at all. Later used to figure out if the structure was valid or not. + let mut error_writer = ErrorWriter::new(error_writer); + + if !nixpkgs_path.join(structure::BASE_SUBPATH).exists() { + eprintln!( + "Given Nixpkgs path does not contain a {} subdirectory, no check necessary.", + structure::BASE_SUBPATH + ); + } else { + let nixpkgs = Nixpkgs::new(&nixpkgs_path, &mut error_writer)?; + + if error_writer.empty { + // Only if we could successfully parse the structure, we do the semantic checks + eval::check_values(&mut error_writer, &nixpkgs, eval_accessible_paths)?; + references::check_references(&mut error_writer, &nixpkgs)?; + } + } + Ok(error_writer.empty) +} + +#[cfg(test)] +mod tests { + use crate::check_nixpkgs; + use crate::structure; + use anyhow::Context; + use std::env; + use std::fs; + use std::path::Path; + use tempfile::{tempdir, tempdir_in}; + + #[test] + fn tests_dir() -> anyhow::Result<()> { + for entry in Path::new("tests").read_dir()? { + let entry = entry?; + let path = entry.path(); + let name = entry.file_name().to_string_lossy().into_owned(); + + if !path.is_dir() { + continue; + } + + let expected_errors = + fs::read_to_string(path.join("expected")).unwrap_or(String::new()); + + test_nixpkgs(&name, &path, &expected_errors)?; + } + Ok(()) + } + + // We cannot check case-conflicting files into Nixpkgs (the channel would fail to + // build), so we generate the case-conflicting file instead. + #[test] + fn test_case_sensitive() -> anyhow::Result<()> { + let temp_nixpkgs = tempdir()?; + let path = temp_nixpkgs.path(); + + if is_case_insensitive_fs(&path)? { + eprintln!("We're on a case-insensitive filesystem, skipping case-sensitivity test"); + return Ok(()); + } + + let base = path.join(structure::BASE_SUBPATH); + + fs::create_dir_all(base.join("fo/foo"))?; + fs::write(base.join("fo/foo/package.nix"), "{ someDrv }: someDrv")?; + + fs::create_dir_all(base.join("fo/foO"))?; + fs::write(base.join("fo/foO/package.nix"), "{ someDrv }: someDrv")?; + + test_nixpkgs( + "case_sensitive", + &path, + "pkgs/by-name/fo: Duplicate case-sensitive package directories \"foO\" and \"foo\".\n", + )?; + + Ok(()) + } + + fn test_nixpkgs(name: &str, path: &Path, expected_errors: &str) -> anyhow::Result<()> { + let extra_nix_path = Path::new("tests/mock-nixpkgs.nix"); + + // We don't want coloring to mess up the tests + env::set_var("NO_COLOR", "1"); + + let mut writer = vec![]; + check_nixpkgs(&path, vec![&extra_nix_path], &mut writer) + .context(format!("Failed test case {name}"))?; + + let actual_errors = String::from_utf8_lossy(&writer); + + if actual_errors != expected_errors { + panic!( + "Failed test case {name}, expected these errors:\n\n{}\n\nbut got these:\n\n{}", + expected_errors, actual_errors + ); + } + Ok(()) + } + + /// Check whether a path is in a case-insensitive filesystem + fn is_case_insensitive_fs(path: &Path) -> anyhow::Result<bool> { + let dir = tempdir_in(path)?; + let base = dir.path(); + fs::write(base.join("aaa"), "")?; + Ok(base.join("AAA").exists()) + } +} diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/references.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/references.rs new file mode 100644 index 000000000000..16dc60729c42 --- /dev/null +++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/references.rs @@ -0,0 +1,184 @@ +use crate::structure::Nixpkgs; +use crate::utils; +use crate::utils::{ErrorWriter, LineIndex}; + +use anyhow::Context; +use rnix::{Root, SyntaxKind::NODE_PATH}; +use std::ffi::OsStr; +use std::fs::read_to_string; +use std::io; +use std::path::{Path, PathBuf}; + +/// Small helper so we don't need to pass in the same arguments to all functions +struct PackageContext<'a, W: io::Write> { + error_writer: &'a mut ErrorWriter<W>, + /// The package directory relative to Nixpkgs, such as `pkgs/by-name/fo/foo` + relative_package_dir: &'a PathBuf, + /// The absolute package directory + absolute_package_dir: &'a PathBuf, +} + +/// Check that every package directory in pkgs/by-name doesn't link to outside that directory. +/// Both symlinks and Nix path expressions are checked. +pub fn check_references<W: io::Write>( + error_writer: &mut ErrorWriter<W>, + nixpkgs: &Nixpkgs, +) -> anyhow::Result<()> { + // Check the directories for each package separately + for package_name in &nixpkgs.package_names { + let relative_package_dir = Nixpkgs::relative_dir_for_package(package_name); + let mut context = PackageContext { + error_writer, + relative_package_dir: &relative_package_dir, + absolute_package_dir: &nixpkgs.path.join(&relative_package_dir), + }; + + // The empty argument here is the subpath under the package directory to check + // An empty one means the package directory itself + check_path(&mut context, Path::new("")).context(format!( + "While checking the references in package directory {}", + relative_package_dir.display() + ))?; + } + Ok(()) +} + +/// Checks for a specific path to not have references outside +fn check_path<W: io::Write>(context: &mut PackageContext<W>, subpath: &Path) -> anyhow::Result<()> { + let path = context.absolute_package_dir.join(subpath); + + if path.is_symlink() { + // Check whether the symlink resolves to outside the package directory + match path.canonicalize() { + Ok(target) => { + // No need to handle the case of it being inside the directory, since we scan through the + // entire directory recursively anyways + if let Err(_prefix_error) = target.strip_prefix(context.absolute_package_dir) { + context.error_writer.write(&format!( + "{}: Path {} is a symlink pointing to a path outside the directory of that package.", + context.relative_package_dir.display(), + subpath.display(), + ))?; + } + } + Err(e) => { + context.error_writer.write(&format!( + "{}: Path {} is a symlink which cannot be resolved: {e}.", + context.relative_package_dir.display(), + subpath.display(), + ))?; + } + } + } else if path.is_dir() { + // Recursively check each entry + for entry in utils::read_dir_sorted(&path)? { + let entry_subpath = subpath.join(entry.file_name()); + check_path(context, &entry_subpath) + .context(format!("Error while recursing into {}", subpath.display()))? + } + } else if path.is_file() { + // Only check Nix files + if let Some(ext) = path.extension() { + if ext == OsStr::new("nix") { + check_nix_file(context, subpath).context(format!( + "Error while checking Nix file {}", + subpath.display() + ))? + } + } + } else { + // This should never happen, git doesn't support other file types + anyhow::bail!("Unsupported file type for path {}", subpath.display()); + } + Ok(()) +} + +/// Check whether a nix file contains path expression references pointing outside the package +/// directory +fn check_nix_file<W: io::Write>( + context: &mut PackageContext<W>, + subpath: &Path, +) -> anyhow::Result<()> { + let path = context.absolute_package_dir.join(subpath); + let parent_dir = path.parent().context(format!( + "Could not get parent of path {}", + subpath.display() + ))?; + + let contents = + read_to_string(&path).context(format!("Could not read file {}", subpath.display()))?; + + let root = Root::parse(&contents); + if let Some(error) = root.errors().first() { + context.error_writer.write(&format!( + "{}: File {} could not be parsed by rnix: {}", + context.relative_package_dir.display(), + subpath.display(), + error, + ))?; + return Ok(()); + } + + let line_index = LineIndex::new(&contents); + + for node in root.syntax().descendants() { + // We're only interested in Path expressions + if node.kind() != NODE_PATH { + continue; + } + + let text = node.text().to_string(); + let line = line_index.line(node.text_range().start().into()); + + // Filters out ./foo/${bar}/baz + // TODO: We can just check ./foo + if node.children().count() != 0 { + context.error_writer.write(&format!( + "{}: File {} at line {line} contains the path expression \"{}\", which is not yet supported and may point outside the directory of that package.", + context.relative_package_dir.display(), + subpath.display(), + text + ))?; + continue; + } + + // Filters out search paths like <nixpkgs> + if text.starts_with('<') { + context.error_writer.write(&format!( + "{}: File {} at line {line} contains the nix search path expression \"{}\" which may point outside the directory of that package.", + context.relative_package_dir.display(), + subpath.display(), + text + ))?; + continue; + } + + // Resolves the reference of the Nix path + // turning `../baz` inside `/foo/bar/default.nix` to `/foo/baz` + match parent_dir.join(Path::new(&text)).canonicalize() { + Ok(target) => { + // Then checking if it's still in the package directory + // No need to handle the case of it being inside the directory, since we scan through the + // entire directory recursively anyways + if let Err(_prefix_error) = target.strip_prefix(context.absolute_package_dir) { + context.error_writer.write(&format!( + "{}: File {} at line {line} contains the path expression \"{}\" which may point outside the directory of that package.", + context.relative_package_dir.display(), + subpath.display(), + text, + ))?; + } + } + Err(e) => { + context.error_writer.write(&format!( + "{}: File {} at line {line} contains the path expression \"{}\" which cannot be resolved: {e}.", + context.relative_package_dir.display(), + subpath.display(), + text, + ))?; + } + }; + } + + Ok(()) +} diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/structure.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/structure.rs new file mode 100644 index 000000000000..ea80128e487a --- /dev/null +++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/structure.rs @@ -0,0 +1,152 @@ +use crate::utils; +use crate::utils::ErrorWriter; +use lazy_static::lazy_static; +use regex::Regex; +use std::collections::HashMap; +use std::io; +use std::path::{Path, PathBuf}; + +pub const BASE_SUBPATH: &str = "pkgs/by-name"; +pub const PACKAGE_NIX_FILENAME: &str = "package.nix"; + +lazy_static! { + static ref SHARD_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_-]{1,2}$").unwrap(); + static ref PACKAGE_NAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap(); +} + +/// Contains information about the structure of the pkgs/by-name directory of a Nixpkgs +pub struct Nixpkgs { + /// The path to nixpkgs + pub path: PathBuf, + /// The names of all packages declared in pkgs/by-name + pub package_names: Vec<String>, +} + +impl Nixpkgs { + // Some utility functions for the basic structure + + pub fn shard_for_package(package_name: &str) -> String { + package_name.to_lowercase().chars().take(2).collect() + } + + pub fn relative_dir_for_shard(shard_name: &str) -> PathBuf { + PathBuf::from(format!("{BASE_SUBPATH}/{shard_name}")) + } + + pub fn relative_dir_for_package(package_name: &str) -> PathBuf { + Nixpkgs::relative_dir_for_shard(&Nixpkgs::shard_for_package(package_name)) + .join(package_name) + } + + pub fn relative_file_for_package(package_name: &str) -> PathBuf { + Nixpkgs::relative_dir_for_package(package_name).join(PACKAGE_NIX_FILENAME) + } +} + +impl Nixpkgs { + /// Read the structure of a Nixpkgs directory, displaying errors on the writer. + /// May return early with I/O errors. + pub fn new<W: io::Write>( + path: &Path, + error_writer: &mut ErrorWriter<W>, + ) -> anyhow::Result<Nixpkgs> { + let base_dir = path.join(BASE_SUBPATH); + + let mut package_names = Vec::new(); + + for shard_entry in utils::read_dir_sorted(&base_dir)? { + let shard_path = shard_entry.path(); + let shard_name = shard_entry.file_name().to_string_lossy().into_owned(); + let relative_shard_path = Nixpkgs::relative_dir_for_shard(&shard_name); + + if shard_name == "README.md" { + // README.md is allowed to be a file and not checked + continue; + } + + if !shard_path.is_dir() { + error_writer.write(&format!( + "{}: This is a file, but it should be a directory.", + relative_shard_path.display(), + ))?; + // we can't check for any other errors if it's a file, since there's no subdirectories to check + continue; + } + + let shard_name_valid = SHARD_NAME_REGEX.is_match(&shard_name); + if !shard_name_valid { + error_writer.write(&format!( + "{}: Invalid directory name \"{shard_name}\", must be at most 2 ASCII characters consisting of a-z, 0-9, \"-\" or \"_\".", + relative_shard_path.display() + ))?; + } + + let mut unique_package_names = HashMap::new(); + + for package_entry in utils::read_dir_sorted(&shard_path)? { + let package_path = package_entry.path(); + let package_name = package_entry.file_name().to_string_lossy().into_owned(); + let relative_package_dir = + PathBuf::from(format!("{BASE_SUBPATH}/{shard_name}/{package_name}")); + + if !package_path.is_dir() { + error_writer.write(&format!( + "{}: This path is a file, but it should be a directory.", + relative_package_dir.display(), + ))?; + continue; + } + + if let Some(duplicate_package_name) = + unique_package_names.insert(package_name.to_lowercase(), package_name.clone()) + { + error_writer.write(&format!( + "{}: Duplicate case-sensitive package directories \"{duplicate_package_name}\" and \"{package_name}\".", + relative_shard_path.display(), + ))?; + } + + let package_name_valid = PACKAGE_NAME_REGEX.is_match(&package_name); + if !package_name_valid { + error_writer.write(&format!( + "{}: Invalid package directory name \"{package_name}\", must be ASCII characters consisting of a-z, A-Z, 0-9, \"-\" or \"_\".", + relative_package_dir.display(), + ))?; + } + + let correct_relative_package_dir = Nixpkgs::relative_dir_for_package(&package_name); + if relative_package_dir != correct_relative_package_dir { + // Only show this error if we have a valid shard and package name + // Because if one of those is wrong, you should fix that first + if shard_name_valid && package_name_valid { + error_writer.write(&format!( + "{}: Incorrect directory location, should be {} instead.", + relative_package_dir.display(), + correct_relative_package_dir.display(), + ))?; + } + } + + let package_nix_path = package_path.join(PACKAGE_NIX_FILENAME); + if !package_nix_path.exists() { + error_writer.write(&format!( + "{}: Missing required \"{PACKAGE_NIX_FILENAME}\" file.", + relative_package_dir.display(), + ))?; + } else if package_nix_path.is_dir() { + error_writer.write(&format!( + "{}: \"{PACKAGE_NIX_FILENAME}\" must be a file.", + relative_package_dir.display(), + ))?; + } + + package_names.push(package_name.clone()); + } + } + + Ok(Nixpkgs { + path: path.to_owned(), + package_names, + }) + } +} diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/utils.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/utils.rs new file mode 100644 index 000000000000..325c736eca98 --- /dev/null +++ b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/utils.rs @@ -0,0 +1,72 @@ +use anyhow::Context; +use colored::Colorize; +use std::fs; +use std::io; +use std::path::Path; + +/// Deterministic file listing so that tests are reproducible +pub fn read_dir_sorted(base_dir: &Path) -> anyhow::Result<Vec<fs::DirEntry>> { + let listing = base_dir + .read_dir() + .context(format!("Could not list directory {}", base_dir.display()))?; + let mut shard_entries = listing + .collect::<io::Result<Vec<_>>>() + .context(format!("Could not list directory {}", base_dir.display()))?; + shard_entries.sort_by_key(|entry| entry.file_name()); + Ok(shard_entries) +} + +/// A simple utility for calculating the line for a string offset. +/// This doesn't do any Unicode handling, though that probably doesn't matter +/// because newlines can't split up Unicode characters. Also this is only used +/// for error reporting +pub struct LineIndex { + /// Stores the indices of newlines + newlines: Vec<usize>, +} + +impl LineIndex { + pub fn new(s: &str) -> LineIndex { + let mut newlines = vec![]; + let mut index = 0; + // Iterates over all newline-split parts of the string, adding the index of the newline to + // the vec + for split in s.split_inclusive('\n') { + index += split.len(); + newlines.push(index); + } + LineIndex { newlines } + } + + /// Returns the line number for a string index + pub fn line(&self, index: usize) -> usize { + match self.newlines.binary_search(&index) { + // +1 because lines are 1-indexed + Ok(x) => x + 1, + Err(x) => x + 1, + } + } +} + +/// A small wrapper around a generic io::Write specifically for errors: +/// - Print everything in red to signal it's an error +/// - Keep track of whether anything was printed at all, so that +/// it can be queried whether any errors were encountered at all +pub struct ErrorWriter<W> { + pub writer: W, + pub empty: bool, +} + +impl<W: io::Write> ErrorWriter<W> { + pub fn new(writer: W) -> ErrorWriter<W> { + ErrorWriter { + writer, + empty: true, + } + } + + pub fn write(&mut self, string: &str) -> io::Result<()> { + self.empty = false; + writeln!(self.writer, "{}", string.red()) + } +} |