From fab8efda272e47d4e862e25da66adee4b10e227d Mon Sep 17 00:00:00 2001 From: Shunpoco Date: Wed, 11 Dec 2024 20:33:39 +0000 Subject: [PATCH] Detect cycle auxiliary Adds a cycle detection for auxiliary. The function builds a graph from files in the given auxiliary dir, then search the graph using Depth-first search algorithm. The function runs in collect tests time, so a cycle is detected before actual tests are executed. Signed-off-by: Shunpoco --- src/tools/compiletest/src/header/auxiliary.rs | 127 +++++++++++++++++- src/tools/compiletest/src/lib.rs | 3 + 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/tools/compiletest/src/header/auxiliary.rs b/src/tools/compiletest/src/header/auxiliary.rs index 0e1f3a785f87f..72779e0e990f7 100644 --- a/src/tools/compiletest/src/header/auxiliary.rs +++ b/src/tools/compiletest/src/header/auxiliary.rs @@ -1,8 +1,12 @@ //! Code for dealing with test directives that request an "auxiliary" crate to //! be built and made available to the test in some way. -use std::iter; +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::path::Path; +use std::{fs, io, iter}; +use super::{DirectiveLine, iter_header}; use crate::common::Config; use crate::header::directives::{AUX_BIN, AUX_BUILD, AUX_CODEGEN_BACKEND, AUX_CRATE, PROC_MACRO}; @@ -63,3 +67,124 @@ fn parse_aux_crate(r: String) -> (String, String) { parts.next().expect("missing aux-crate value (e.g. log=log.rs)").to_string(), ) } + +/// Return an error if the given directory has cyclic aux. +pub(crate) fn check_cycles(config: &Config, dir: &Path) -> io::Result<()> { + let mut filenames = vec![]; + let mut auxiliaries = HashMap::new(); + + build_graph(config, dir, dir, &mut filenames, &mut auxiliaries)?; + + has_cycle(&filenames, &auxiliaries) +} + +fn build_graph( + config: &Config, + dir: &Path, + base_dir: &Path, + filenames: &mut Vec, + auxiliaries: &mut HashMap>, +) -> io::Result<()> { + for file in fs::read_dir(dir)? { + let file = file?; + let file_path = file.path(); + + if file_path.is_dir() { + // explore in sub directory. + build_graph(config, &file_path, base_dir, filenames, auxiliaries)?; + } else { + // We'd like to put a filename with relative path from the auxiliary directory (e.g., ["foo.rs", "foo/bar.rs"]). + let relative_filename = file_path + .strip_prefix(base_dir) + .map_err(|e| io::Error::other(e))? + .to_str() + .unwrap(); + + filenames.push(relative_filename.to_string()); + + let mut aux_props = AuxProps::default(); + let mut poisoned = false; + let f = File::open(&file_path).expect("open file to parse aux for cycle detection"); + iter_header( + config.mode, + &config.suite, + &mut poisoned, + &file_path, + f, + &mut |DirectiveLine { raw_directive: ln, .. }| { + parse_and_update_aux(config, ln, &mut aux_props); + }, + ); + + let mut auxs = vec![]; + for aux in aux_props.all_aux_path_strings() { + auxs.push(aux.to_string()); + } + + if auxs.len() > 0 { + auxiliaries.insert(relative_filename.to_string(), auxs); + } + } + } + + Ok(()) +} + +/// has_cycle checks if the given graph has cycle. +/// It performs with a simple Depth-first search. +fn has_cycle( + filenames: &Vec, + auxiliaries: &HashMap>, +) -> io::Result<()> { + // checked tracks nodes which the function already finished to search. + let mut checked = HashSet::with_capacity(filenames.len()); + // During onde DFS exploration, on_search tracks visited nodes. + // If the current node is already in on_search, that's a cycle. + // The capacity `4` is added, because we can guess that an aux dependency is not so a long path. + let mut on_search = HashSet::with_capacity(4); + // path tracks visited nodes in on exploration. + // This is used for generating an error message when a cycle is detected. + let mut path = Vec::with_capacity(4); + + for vertex in filenames.iter() { + if !checked.contains(vertex) { + search(filenames, auxiliaries, &vertex, &mut checked, &mut on_search, &mut path)?; + } + } + + fn search( + filenames: &Vec, + auxiliaries: &HashMap>, + vertex: &str, + checked: &mut HashSet, + on_search: &mut HashSet, + path: &mut Vec, + ) -> io::Result<()> { + if !on_search.insert(vertex.to_string()) { + let mut cyclic_path = vec![vertex]; + for v in path.iter().rev() { + if v == vertex { + break; + } + cyclic_path.push(v); + } + + return Err(io::Error::other(format!("detect cyclic auxiliary: {:?}", cyclic_path))); + } + + if checked.insert(vertex.to_string()) { + path.push(vertex.to_string()); + if let Some(auxs) = auxiliaries.get(&vertex.to_string()) { + for aux in auxs.iter() { + search(filenames, auxiliaries, &aux, checked, on_search, path)?; + } + } + path.pop().unwrap(); + } + + on_search.remove(&vertex.to_string()); + Ok(()) + } + + Ok(()) +} diff --git a/src/tools/compiletest/src/lib.rs b/src/tools/compiletest/src/lib.rs index 9acb7d393b461..9059138cee8c7 100644 --- a/src/tools/compiletest/src/lib.rs +++ b/src/tools/compiletest/src/lib.rs @@ -31,6 +31,7 @@ use std::{env, fs, vec}; use build_helper::git::{get_git_modified_files, get_git_untracked_files}; use getopts::Options; +use header::auxiliary::check_cycles; use test::ColorConfig; use tracing::*; use walkdir::WalkDir; @@ -766,6 +767,8 @@ fn collect_tests_from_dir( if &file_name != "auxiliary" { debug!("found directory: {:?}", file_path.display()); collect_tests_from_dir(cx, collector, &file_path, &relative_file_path)?; + } else { + check_cycles(&cx.config, &file_path)?; } } else { debug!("found other file/directory: {:?}", file_path.display());