use std::collections::HashMap;
use crate::{CountingModel, Stats};
use crate::engine::config::{Target, Threads};
use crate::engine::modules::{ModuleId, Modules};
#[derive(Debug,Clone)]
pub struct RunResult {
pub per_language: HashMap<String, Stats>,
pub total: Stats,
pub nested_languages: HashMap<String, HashMap<String, Stats>>,
pub modules: Vec<ModuleResult>,
pub faulty_files: Vec<FaultyFileDetails>,
pub skipped_files: SkippedFiles,
pub files_present: FilesPresent,
pub performance: Performance,
pub targets: Vec<Target>,
pub unreadable_dirs: Vec<UnreadableDirDetails>
}
impl RunResult {
pub fn sort_languages_by(&self, criterion: SortCriterion, model: CountingModel) -> Vec<(&str, &Stats)> {
sort_languages_by(&self.per_language, criterion, model)
}
pub fn all_relevant_files_were_faulty(&self) -> bool {
!self.faulty_files.is_empty() && self.faulty_files.len() == self.files_present.relevant_files
}
pub fn nothing_of_interest_was_counted(&self) -> bool {
self.files_present.relevant_files > 0 && self.total.files == 0
}
pub fn nothing_could_be_read(&self) -> bool {
self.files_present.relevant_files == 0 && !self.unreadable_dirs.is_empty()
}
pub fn has_modules(&self) -> bool {
self.modules.iter().any(|x| x.name.is_some())
}
pub(crate) fn of_nothing(files_present: FilesPresent, performance: Performance, modules: &Modules,
targets: Vec<Target>, unreadable_dirs: Vec<UnreadableDirDetails>) -> Self {
RunResult {
per_language: HashMap::new(),
total: Stats::default(),
nested_languages: HashMap::new(),
modules: (0..modules.count()).map(|id| ModuleResult {
name: modules.name_of(id as ModuleId).map(str::to_owned),
per_language: HashMap::new(),
nested_languages: HashMap::new(),
files: HashMap::new(),
total: Stats::default()
}).collect(),
faulty_files: Vec::new(),
skipped_files: SkippedFiles::default(),
files_present,
performance,
targets,
unreadable_dirs
}
}
}
#[derive(Debug,Clone)]
pub struct ModuleResult {
pub name: Option<String>,
pub per_language: HashMap<String, Stats>,
pub nested_languages: HashMap<String, HashMap<String, Stats>>,
pub files: HashMap<String, Vec<FileEntry>>,
pub total: Stats
}
impl ModuleResult {
pub fn sort_languages_by(&self, criterion: SortCriterion, model: CountingModel) -> Vec<(&str, &Stats)> {
sort_languages_by(&self.per_language, criterion, model)
}
}
#[derive(Debug,Clone)]
pub struct FileEntry {
pub path: String,
pub stats: Stats,
pub nested_languages: HashMap<String, Stats>
}
#[derive(Debug,Clone)]
pub struct Performance {
pub duration_millis: u128,
pub threads: Threads
}
#[derive(Debug,Default,Clone,Copy,PartialEq,Eq)]
pub struct FilesPresent {
pub total_files: usize,
pub relevant_files: usize,
pub excluded_files: usize
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
pub enum SortCriterion {
Files,
#[default]
Lines,
Code,
Comments,
Extra,
Blanks,
Size,
Name
}
impl SortCriterion {
pub fn parse(value: &str) -> Option<SortCriterion> {
match value.trim().to_lowercase().as_str() {
"files" => Some(Self::Files),
"lines" => Some(Self::Lines),
"code" => Some(Self::Code),
"comments" => Some(Self::Comments),
"extra" => Some(Self::Extra),
"blanks" => Some(Self::Blanks),
"size" => Some(Self::Size),
"name" => Some(Self::Name),
_ => None
}
}
pub fn name(self) -> &'static str {
match self {
Self::Files => "files",
Self::Lines => "lines",
Self::Code => "code",
Self::Comments => "comments",
Self::Extra => "extra",
Self::Blanks => "blanks",
Self::Size => "size",
Self::Name => "name"
}
}
pub fn get_value_of(self, stats: &Stats, model: CountingModel) -> usize {
match self {
Self::Files => stats.files,
Self::Size => stats.bytes,
Self::Lines => stats.lines,
Self::Code => stats.calculate_code_lines(model),
Self::Comments => stats.calculate_comment_lines(model),
Self::Extra | Self::Blanks => stats.calculate_extra_lines(model),
Self::Name => 0
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScanSkip {
Minified,
Generated,
NotCode
}
impl ScanSkip {
pub const ALL : [ScanSkip; 3] = [ScanSkip::Minified, ScanSkip::Generated, ScanSkip::NotCode];
pub fn name(self) -> &'static str {
match self {
Self::Minified => "minified",
Self::Generated => "generated",
Self::NotCode => "not_code"
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SkippedFiles {
pub minified: Vec<String>,
pub generated: Vec<String>,
pub not_code: Vec<String>
}
impl SkippedFiles {
pub fn get_of_kind(&self, kind: ScanSkip) -> &[String] {
match kind {
ScanSkip::Minified => &self.minified,
ScanSkip::Generated => &self.generated,
ScanSkip::NotCode => &self.not_code
}
}
pub fn calculate_files(&self) -> usize {
self.minified.len() + self.generated.len() + self.not_code.len()
}
pub(crate) fn get_of_kind_mut(&mut self, kind: ScanSkip) -> &mut Vec<String> {
match kind {
ScanSkip::Minified => &mut self.minified,
ScanSkip::Generated => &mut self.generated,
ScanSkip::NotCode => &mut self.not_code
}
}
}
#[derive(Debug,Clone)]
#[non_exhaustive]
pub struct FaultyFileDetails {
pub path: String,
pub error_msg: String,
pub size: u64
}
impl FaultyFileDetails {
pub fn new(path: String, error_msg: String, size: u64) -> Self {
FaultyFileDetails {
path,
error_msg,
size
}
}
}
#[derive(Debug,Clone)]
#[non_exhaustive]
pub struct UnreadableDirDetails {
pub path: String,
pub error_msg: String
}
impl UnreadableDirDetails {
pub fn new(path: String, error_msg: String) -> Self {
UnreadableDirDetails {
path,
error_msg
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum RunError {
NoTargets,
LanguagesFromAnotherConfig,
InvalidTargets(crate::engine::targets::TargetError),
InvalidExcludePattern(String),
NoThreadsAvailable {
side: &'static str,
error: std::io::Error
},
IncompleteRun {
worker_panic: String
}
}
impl std::fmt::Display for RunError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoTargets => write!(f, "The configuration names no directories or files, so there is nothing to count."),
Self::LanguagesFromAnotherConfig => write!(f, "The languages were resolved against a configuration that selects a different set of them than the one this run was given, so the counts would not be the ones the settings describe. Resolve them against the same configuration you are counting with."),
Self::InvalidTargets(x) => write!(f, "{x} Nothing was counted."),
Self::InvalidExcludePattern(x) => write!(f, "'{x}' is not a valid exclude pattern, so nothing was counted."),
Self::NoThreadsAvailable { side, error } => write!(f, "The operating system refused every {side} thread, so the run could not start: {error}"),
Self::IncompleteRun { worker_panic } => write!(f, "A worker thread died mid-run, so the counts would have been incomplete and were discarded: {worker_panic}")
}
}
}
impl std::error::Error for RunError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidTargets(x) => Some(x),
_ => None
}
}
}
fn sort_languages_by(per_language: &HashMap<String, Stats>, criterion: SortCriterion,
model: CountingModel) -> Vec<(&str, &Stats)>
{
let mut rows = per_language.iter().map(|(name, stats)| (name.as_str(), stats)).collect::<Vec<_>>();
if criterion == SortCriterion::Name {
rows.sort_by_key(|(name, _)| name.to_lowercase());
} else {
rows.sort_by(|a, b| criterion.get_value_of(b.1, model).cmp(&criterion.get_value_of(a.1, model))
.then_with(|| a.0.to_lowercase().cmp(&b.0.to_lowercase())));
}
rows
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::modules::Modules;
fn result_with(relevant: usize, unreadable: &[&str]) -> RunResult {
let unreadable = unreadable.iter().map(|path| UnreadableDirDetails {
path: (*path).to_owned(), error_msg: "Access is denied. (os error 5)".to_owned()
}).collect();
RunResult::of_nothing(
FilesPresent { total_files: relevant, relevant_files: relevant, excluded_files: 0 },
Performance { duration_millis: 0, threads: Threads::new(1, 1) },
&Modules::of(&[]), Vec::new(), unreadable)
}
#[test]
fn an_empty_scan_is_suspect_only_when_something_was_unreadable() {
assert!(!result_with(0, &[]).nothing_could_be_read());
assert!(result_with(0, &["D:/gone"]).nothing_could_be_read());
assert!(!result_with(3, &["D:/gone"]).nothing_could_be_read());
assert!(!result_with(3, &[]).nothing_could_be_read());
}
}