#![deny(missing_docs)]
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use breezyshim::dirty_tracker::DirtyTreeTracker;
use breezyshim::error::Error;
use breezyshim::tree::{TreeChange, WorkingTree};
use breezyshim::workspace::{check_clean_tree, reset_tree_with_dirty_tracker};
use breezyshim::RevisionId;
use debian_analyzer::detect_gbp_dch::{guess_update_changelog, ChangelogBehaviour};
use debian_analyzer::{
add_changelog_entry, apply_or_revert, get_committer, min_certainty, ApplyError, ChangelogError,
};
use debian_changelog::ChangeLog;
use debian_workspace::Workspace;
pub mod builtin_fixers;
pub mod debhelper;
pub mod detector;
pub mod diagnostic;
#[macro_use]
pub mod fixers;
pub mod licenses;
pub mod lintian_overrides;
pub mod rules;
pub mod upstream_metadata;
#[cfg(feature = "upstream")]
pub mod vcs;
pub mod watch;
pub use debian_analyzer::Certainty;
pub use debversion::Version;
pub use fixers::get_renamed_tags;
pub use inventory;
#[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize,
)]
pub enum Visibility {
#[serde(rename = "pedantic")]
Pedantic,
#[serde(rename = "info")]
Info,
#[serde(rename = "warning")]
Warning,
#[serde(rename = "error")]
Error,
}
impl std::fmt::Display for Visibility {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Visibility::Pedantic => write!(f, "pedantic"),
Visibility::Info => write!(f, "info"),
Visibility::Warning => write!(f, "warning"),
Visibility::Error => write!(f, "error"),
}
}
}
impl FromStr for Visibility {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"pedantic" => Ok(Visibility::Pedantic),
"info" => Ok(Visibility::Info),
"warning" => Ok(Visibility::Warning),
"error" => Ok(Visibility::Error),
_ => Err(format!("Invalid visibility: {}", value)),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum PackageType {
#[serde(rename = "source")]
Source,
#[serde(rename = "binary")]
Binary,
}
impl FromStr for PackageType {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"source" => Ok(PackageType::Source),
"binary" => Ok(PackageType::Binary),
_ => Err(format!("Invalid package type: {}", value)),
}
}
}
impl std::fmt::Display for PackageType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
PackageType::Source => write!(f, "source"),
PackageType::Binary => write!(f, "binary"),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct LintianIssue {
pub package: Option<String>,
pub package_type: Option<PackageType>,
pub visibility: Option<Visibility>,
pub tag: Option<String>,
pub info: Option<String>,
}
impl std::fmt::Display for LintianIssue {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some(ref pkg) = self.package {
write!(f, "{} ", pkg)?;
}
if let Some(ref pt) = self.package_type {
write!(f, "{}", pt)?;
} else {
write!(f, "source")?;
}
write!(f, ":")?;
if let Some(ref tag) = self.tag {
write!(f, " {}", tag)?;
}
if let Some(ref info) = self.info {
if !info.is_empty() {
write!(f, " {}", info)?;
}
}
Ok(())
}
}
impl LintianIssue {
pub fn json(&self) -> serde_json::Value {
serde_json::json!({
"package": self.package,
"package_type": self.package_type.as_ref().map(|t| t.to_string()),
"visibility": self.visibility.as_ref().map(|v| v.to_string()),
"tag": self.tag,
"info": self.info,
})
}
pub fn from_json(value: serde_json::Value) -> serde_json::Result<Self> {
serde_json::from_value(value)
}
pub fn just_tag(tag: String) -> Self {
Self {
package: None,
package_type: None,
visibility: None,
tag: Some(tag),
info: None,
}
}
pub fn source(tag: impl Into<String>, visibility: Visibility) -> Self {
Self {
package: None,
package_type: Some(PackageType::Source),
visibility: Some(visibility),
tag: Some(tag.into()),
info: None,
}
}
pub fn source_with_info(
tag: impl Into<String>,
visibility: Visibility,
info: Vec<String>,
) -> Self {
let joined = info.join(" ");
Self {
package: None,
package_type: Some(PackageType::Source),
visibility: Some(visibility),
tag: Some(tag.into()),
info: if joined.is_empty() {
None
} else {
Some(joined)
},
}
}
pub fn binary_with_info(
package: impl Into<String>,
tag: impl Into<String>,
visibility: Visibility,
info: Vec<String>,
) -> Self {
let joined = info.join(" ");
Self {
package: Some(package.into()),
package_type: Some(PackageType::Binary),
visibility: Some(visibility),
tag: Some(tag.into()),
info: if joined.is_empty() {
None
} else {
Some(joined)
},
}
}
pub fn with_info(mut self, info: Vec<String>) -> Self {
let joined = info.join(" ");
self.info = if joined.is_empty() {
None
} else {
Some(joined)
};
self
}
pub fn should_fix(&self, base_path: &std::path::Path) -> bool {
use crate::lintian_overrides::{self, OverrideLineMatch};
for line in lintian_overrides::try_iter_overrides(base_path).unwrap() {
if line.matches_issue(self) {
return false;
}
}
true
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum LintianIssueParseError {
InvalidPackageType(String),
}
impl std::fmt::Display for LintianIssueParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
LintianIssueParseError::InvalidPackageType(s) => {
write!(f, "Invalid package type: {}", s)
}
}
}
}
impl std::error::Error for LintianIssueParseError {}
impl TryFrom<&str> for LintianIssue {
type Error = LintianIssueParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.trim();
let package_type;
let package;
let after = if let Some((before, after)) = value.split_once(':') {
if let Some((first, second)) = before.trim().split_once(' ') {
if second == "source" {
package_type = Some(PackageType::Source);
package = Some(first.to_string());
} else if second == "binary" {
package_type = Some(PackageType::Binary);
package = Some(first.to_string());
} else if first == "source" {
package_type = Some(PackageType::Source);
package = Some(second.to_string());
} else if first == "binary" {
package_type = Some(PackageType::Binary);
package = Some(second.to_string());
} else {
return Err(LintianIssueParseError::InvalidPackageType(format!(
"{} {}",
first, second
)));
}
} else {
if before == "source" {
package_type = Some(PackageType::Source);
package = None;
} else if before == "binary" {
package_type = Some(PackageType::Binary);
package = None;
} else {
package_type = None;
package = Some(before.to_string());
}
}
after
} else {
package_type = None;
package = None;
value
};
let after = after.trim();
let (tag, info) = if let Some((tag_str, info_str)) = after.split_once(' ') {
let info = if info_str.is_empty() {
None
} else {
Some(info_str.to_string())
};
(Some(tag_str.to_string()), info)
} else {
(Some(after.to_string()), None)
};
Ok(Self {
package,
package_type,
visibility: None,
tag,
info,
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct FixerResult {
pub description: String,
pub certainty: Option<Certainty>,
pub patch_name: Option<String>,
pub revision_id: Option<RevisionId>,
pub fixed_lintian_issues: Vec<LintianIssue>,
pub overridden_lintian_issues: Vec<LintianIssue>,
}
impl FixerResult {
pub fn new(
description: String,
certainty: Option<Certainty>,
patch_name: Option<String>,
revision_id: Option<RevisionId>,
fixed_lintian_issues: Vec<LintianIssue>,
overridden_lintian_issues: Option<Vec<LintianIssue>>,
) -> Self {
Self {
description,
certainty,
patch_name,
revision_id,
fixed_lintian_issues,
overridden_lintian_issues: overridden_lintian_issues.unwrap_or_default(),
}
}
pub fn fixed_lintian_tags(&self) -> Vec<&str> {
self.fixed_lintian_issues
.iter()
.filter_map(|issue| issue.tag.as_deref())
.collect()
}
pub fn builder(description: impl Into<String>) -> FixerResultBuilder {
FixerResultBuilder::new(description)
}
}
#[derive(Debug, Default)]
pub struct FixerResultBuilder {
description: String,
certainty: Option<Certainty>,
patch_name: Option<String>,
revision_id: Option<RevisionId>,
fixed_lintian_issues: Vec<LintianIssue>,
overridden_lintian_issues: Vec<LintianIssue>,
}
impl FixerResultBuilder {
pub fn new(description: impl Into<String>) -> Self {
Self {
description: description.into(),
..Default::default()
}
}
pub fn certainty(mut self, certainty: Certainty) -> Self {
self.certainty = Some(certainty);
self
}
pub fn patch_name(mut self, patch_name: impl Into<String>) -> Self {
self.patch_name = Some(patch_name.into());
self
}
pub fn revision_id(mut self, revision_id: RevisionId) -> Self {
self.revision_id = Some(revision_id);
self
}
pub fn fixed_issue(mut self, issue: LintianIssue) -> Self {
self.fixed_lintian_issues.push(issue);
self
}
pub fn fixed_issues(mut self, issues: impl IntoIterator<Item = LintianIssue>) -> Self {
self.fixed_lintian_issues.extend(issues);
self
}
pub fn overridden_issue(mut self, issue: LintianIssue) -> Self {
self.overridden_lintian_issues.push(issue);
self
}
pub fn overridden_issues(mut self, issues: impl IntoIterator<Item = LintianIssue>) -> Self {
self.overridden_lintian_issues.extend(issues);
self
}
pub fn build(self) -> FixerResult {
FixerResult {
description: self.description,
certainty: self.certainty,
patch_name: self.patch_name,
revision_id: self.revision_id,
fixed_lintian_issues: self.fixed_lintian_issues,
overridden_lintian_issues: self.overridden_lintian_issues,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum OutputParseError {
UnsupportedCertainty(String),
LintianIssueParseError(LintianIssueParseError),
}
impl std::fmt::Display for OutputParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
OutputParseError::UnsupportedCertainty(s) => {
write!(f, "Unsupported certainty: {}", s)
}
OutputParseError::LintianIssueParseError(e) => write!(f, "{}", e),
}
}
}
impl std::error::Error for OutputParseError {}
impl From<LintianIssueParseError> for OutputParseError {
fn from(value: LintianIssueParseError) -> Self {
Self::LintianIssueParseError(value)
}
}
pub fn is_debcargo_package(base_path: &std::path::Path) -> bool {
base_path.join("debian/debcargo.toml").exists()
}
pub fn parse_script_fixer_output(text: &str) -> Result<FixerResult, OutputParseError> {
let mut description: Vec<String> = Vec::new();
let mut overridden_issues: Vec<LintianIssue> = Vec::new();
let mut fixed_lintian_issues: Vec<LintianIssue> = Vec::new();
let mut certainty: Option<String> = None;
let mut patch_name: Option<String> = None;
let lines: Vec<&str> = text.split_terminator('\n').collect();
let mut i = 0;
while i < lines.len() {
if let Some((key, value)) = lines[i].split_once(':') {
match key.trim() {
"Fixed-Lintian-Issues" => {
i += 1;
while i < lines.len() && lines[i].starts_with(' ') {
fixed_lintian_issues.push(LintianIssue::try_from(&lines[i][1..])?);
i += 1;
}
continue;
}
"Overridden-Lintian-Issues" => {
i += 1;
while i < lines.len() && lines[i].starts_with(' ') {
overridden_issues.push(LintianIssue::try_from(&lines[i][1..])?);
i += 1;
}
continue;
}
"Certainty" => {
certainty = Some(value.trim().to_owned());
}
"Patch-Name" => {
patch_name = Some(value.trim().to_owned());
}
_ => {
description.push(lines[i].to_owned());
}
}
} else {
description.push(lines[i].to_owned());
}
i += 1;
}
let certainty = certainty
.map(|c| c.parse())
.transpose()
.map_err(OutputParseError::UnsupportedCertainty)?;
let overridden_issues = if overridden_issues.is_empty() {
None
} else {
Some(overridden_issues)
};
Ok(FixerResult::new(
description.join("\n"),
certainty,
patch_name,
None,
fixed_lintian_issues,
overridden_issues,
))
}
pub fn determine_env(
package: &str,
current_version: &Version,
preferences: &FixerPreferences,
) -> std::collections::HashMap<String, String> {
let mut env = std::env::vars().collect::<std::collections::HashMap<_, _>>();
env.insert("DEB_SOURCE".to_owned(), package.to_owned());
env.insert("CURRENT_VERSION".to_owned(), current_version.to_string());
if let Some(compat_release) = preferences.compat_release.as_ref() {
env.insert("COMPAT_RELEASE".to_owned(), compat_release.to_owned());
}
if let Some(upgrade_release) = preferences.upgrade_release.as_ref() {
env.insert("UPGRADE_RELEASE".to_owned(), upgrade_release.to_owned());
}
env.insert(
"MINIMUM_CERTAINTY".to_owned(),
preferences
.minimum_certainty
.unwrap_or_default()
.to_string(),
);
env.insert(
"TRUST_PACKAGE".to_owned(),
preferences.trust_package.unwrap_or(false).to_string(),
);
env.insert(
"REFORMATTING".to_owned(),
if preferences.allow_reformatting.unwrap_or(false) {
"allow"
} else {
"disallow"
}
.to_owned(),
);
env.insert(
"NET_ACCESS".to_owned(),
if preferences.net_access.unwrap_or(true) {
"allow"
} else {
"disallow"
}
.to_owned(),
);
env.insert(
"OPINIONATED".to_owned(),
if preferences.opinionated.unwrap_or(false) {
"yes"
} else {
"no"
}
.to_owned(),
);
env.insert(
"DILIGENCE".to_owned(),
preferences.diligence.unwrap_or(0).to_string(),
);
let py_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("py");
if let Ok(existing_pythonpath) = std::env::var("PYTHONPATH") {
env.insert(
"PYTHONPATH".to_owned(),
format!("{}:{}", py_path.to_string_lossy(), existing_pythonpath),
);
} else {
env.insert(
"PYTHONPATH".to_owned(),
py_path.to_string_lossy().to_string(),
);
}
if let Some(extra_env) = &preferences.extra_env {
for (key, value) in extra_env {
env.insert(key.clone(), value.clone());
}
}
env
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FixerPreferences {
pub compat_release: Option<String>,
pub minimum_certainty: Option<Certainty>,
pub trust_package: Option<bool>,
pub allow_reformatting: Option<bool>,
pub net_access: Option<bool>,
pub opinionated: Option<bool>,
pub diligence: Option<i32>,
pub upgrade_release: Option<String>,
pub extra_env: Option<std::collections::HashMap<String, String>>,
pub lintian_data_path: Option<std::path::PathBuf>,
}
#[derive(Debug)]
pub enum FixerError {
NoChanges,
NoChangesAfterOverrides(Vec<LintianIssue>),
NotCertainEnough(Certainty, Option<Certainty>, Vec<LintianIssue>),
NotDebianPackage(std::path::PathBuf),
DescriptionMissing,
InvalidChangelog(std::path::PathBuf, String),
ScriptNotFound(std::path::PathBuf),
OutputParseError(OutputParseError),
FailedPatchManipulation(String),
ChangelogCreate(String),
ScriptFailed {
path: std::path::PathBuf,
exit_code: i32,
stderr: String,
},
FormattingUnpreservable(std::path::PathBuf),
GeneratedFile(std::path::PathBuf),
Io(std::io::Error),
BrzError(Error),
Panic {
message: String,
backtrace: Option<std::backtrace::Backtrace>,
},
MissingDependency(String),
Other(String),
}
impl From<debian_analyzer::editor::EditorError> for FixerError {
fn from(e: debian_analyzer::editor::EditorError) -> Self {
match e {
debian_analyzer::editor::EditorError::IoError(e) => e.into(),
debian_analyzer::editor::EditorError::BrzError(e) => e.into(),
debian_analyzer::editor::EditorError::GeneratedFile(p, _) => {
FixerError::GeneratedFile(p)
}
debian_analyzer::editor::EditorError::FormattingUnpreservable(p, _e) => {
FixerError::FormattingUnpreservable(p)
}
debian_analyzer::editor::EditorError::TemplateError(p, _e) => {
FixerError::GeneratedFile(p)
}
}
}
}
impl From<std::io::Error> for FixerError {
fn from(e: std::io::Error) -> Self {
FixerError::Io(e)
}
}
impl From<debian_changelog::Error> for FixerError {
fn from(e: debian_changelog::Error) -> Self {
match e {
debian_changelog::Error::Io(e) => FixerError::Io(e),
debian_changelog::Error::Parse(e) => FixerError::ChangelogCreate(e.to_string()),
}
}
}
impl From<debian_changelog::ParseError> for FixerError {
fn from(e: debian_changelog::ParseError) -> Self {
FixerError::ChangelogCreate(e.to_string())
}
}
impl From<ChangelogError> for FixerError {
fn from(e: ChangelogError) -> Self {
match e {
ChangelogError::NotDebianPackage(path) => FixerError::NotDebianPackage(path),
ChangelogError::Python(e) => FixerError::Other(e.to_string()),
}
}
}
impl From<Error> for FixerError {
fn from(e: Error) -> Self {
FixerError::BrzError(e)
}
}
impl From<debian_workspace::Error> for FixerError {
fn from(e: debian_workspace::Error) -> Self {
match e {
debian_workspace::Error::NotFound => FixerError::NoChanges,
debian_workspace::Error::Io(e) => FixerError::Io(e),
debian_workspace::Error::Parse(msg) | debian_workspace::Error::Other(msg) => {
FixerError::Other(msg)
}
debian_workspace::Error::MissingDependency(dep) => FixerError::MissingDependency(dep),
}
}
}
impl From<OutputParseError> for FixerError {
fn from(e: OutputParseError) -> Self {
FixerError::OutputParseError(e)
}
}
impl std::fmt::Display for FixerError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
FixerError::NoChanges => write!(f, "No changes"),
FixerError::NoChangesAfterOverrides(_) => write!(f, "No changes after overrides"),
FixerError::OutputParseError(e) => write!(f, "Output parse error: {}", e),
FixerError::ScriptNotFound(p) => write!(f, "Command not found: {}", p.display()),
FixerError::ChangelogCreate(m) => write!(f, "Changelog create error: {}", m),
FixerError::FormattingUnpreservable(p) => {
write!(f, "Formatting unpreservable for {}", p.display())
}
FixerError::ScriptFailed {
path,
exit_code,
stderr,
} => write!(
f,
"Script failed: {} (exit code {}) (stderr: {})",
path.display(),
exit_code,
stderr
),
FixerError::Other(s) => write!(f, "{}", s),
FixerError::NotDebianPackage(p) => write!(f, "Not a Debian package: {}", p.display()),
FixerError::DescriptionMissing => {
write!(f, "Description missing")
}
FixerError::NotCertainEnough(actual, minimum, _) => write!(
f,
"Not certain enough to fix (actual: {}, minimum : {:?})",
actual, minimum
),
FixerError::Io(e) => write!(f, "IO error: {}", e),
FixerError::FailedPatchManipulation(s) => {
write!(f, "Failed to manipulate patch: {}", s)
}
FixerError::BrzError(e) => write!(f, "Breezy error: {}", e),
FixerError::InvalidChangelog(p, s) => {
write!(f, "Invalid changelog {}: {}", p.display(), s)
}
FixerError::GeneratedFile(p) => write!(f, "Generated file: {}", p.display()),
FixerError::Panic { message, backtrace } => {
write!(f, "Panic: {}", message)?;
if let Some(bt) = backtrace {
write!(f, "\nBacktrace:\n{}", bt)?;
}
Ok(())
}
FixerError::MissingDependency(dep) => write!(f, "Missing optional dependency: {}", dep),
}
}
}
impl std::error::Error for FixerError {}
pub fn all_lintian_fixers() -> impl Iterator<Item = Box<dyn crate::detector::Detector>> {
builtin_fixers::get_builtin_fixers().into_iter()
}
pub const DEFAULT_VALUE_LINTIAN_BRUSH_ADDON_ONLY: i32 = 10;
pub const DEFAULT_VALUE_LINTIAN_BRUSH: i32 = 50;
pub const LINTIAN_BRUSH_TAG_VALUES: [(&str, i32); 1] = [("trailing-whitespace", 0)];
pub const DEFAULT_ADDON_FIXERS: &[&str] = &[
"debian-changelog-line-too-long",
"trailing-whitespace",
"out-of-date-standards-version",
"package-uses-old-debhelper-compat-version",
"public-upstream-key-not-minimal",
];
pub const LINTIAN_BRUSH_TAG_DEFAULT_VALUE: i32 = 5;
pub fn calculate_value(tags: &[&str]) -> i32 {
if tags.is_empty() {
return 0;
}
let default_addon_fixers: HashSet<&str> = DEFAULT_ADDON_FIXERS.iter().cloned().collect();
let tag_set: HashSet<&str> = tags.iter().cloned().collect();
if tag_set.is_subset(&default_addon_fixers) {
return DEFAULT_VALUE_LINTIAN_BRUSH_ADDON_ONLY;
}
let mut value = DEFAULT_VALUE_LINTIAN_BRUSH;
for tag in tags {
if let Some(tag_value) = LINTIAN_BRUSH_TAG_VALUES.iter().find(|(t, _)| t == tag) {
value += tag_value.1;
} else {
value += LINTIAN_BRUSH_TAG_DEFAULT_VALUE;
}
}
value
}
pub fn data_file_path(
name: &str,
check: impl Fn(&std::path::Path) -> bool,
) -> Option<std::path::PathBuf> {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path = path.join("..").join(name);
if check(&path) {
return Some(path);
}
let base_paths = &["/usr/share/lintian-brush", "/usr/local/share/lintian-brush"];
for base_path in base_paths {
let path = std::path::Path::new(base_path).join(name);
if check(&path) {
return Some(path);
}
}
None
}
pub fn render_lintian_trailers(issues: &[LintianIssue]) -> String {
let mut out = String::new();
for issue in issues {
out.push_str(&format!("Fixes: lintian: {}\n", issue));
}
let mut seen = std::collections::HashSet::new();
for issue in issues {
if let Some(tag) = issue.tag.as_deref() {
if seen.insert(tag) {
out.push_str(&format!(
"See-also: https://lintian.debian.org/tags/{}.html\n",
tag
));
}
}
}
out
}
pub fn run_lintian_fixer(
local_tree: &breezyshim::workingtree::GenericWorkingTree,
detector: &dyn crate::detector::Detector,
committer: Option<&str>,
mut update_changelog: impl FnMut() -> bool,
preferences: &FixerPreferences,
dirty_tracker: &mut Option<DirtyTreeTracker>,
subpath: &std::path::Path,
timestamp: Option<chrono::naive::NaiveDateTime>,
basis_tree: Option<&dyn breezyshim::tree::PyTree>,
changes_by: Option<&str>,
) -> Result<(FixerResult, String), FixerError> {
let changes_by = changes_by.unwrap_or("lintian-brush");
let changelog_path = subpath.join("debian/changelog");
let basedir = local_tree.abspath(subpath).unwrap();
let cl = match debian_workspace::fs_workspace::FsWorkspace::new(basedir.as_path(), None, None)
.parsed_changelog()
{
Ok(cl) => cl,
Err(debian_workspace::Error::NotFound) => {
return Err(FixerError::NotDebianPackage(basedir));
}
Err(e) => return Err(FixerError::Other(e.to_string())),
};
let first_entry = if let Some(entry) = cl.iter().next() {
entry
} else {
return Err(FixerError::InvalidChangelog(
basedir,
"No entries in changelog".to_string(),
));
};
let package = first_entry.package().ok_or_else(|| {
FixerError::InvalidChangelog(
basedir.clone(),
"First changelog entry has no package name".to_string(),
)
})?;
let version = first_entry.version().ok_or_else(|| {
FixerError::InvalidChangelog(
basedir.clone(),
"First changelog entry has no version".to_string(),
)
})?;
let current_version: Version = if first_entry
.distributions()
.as_deref()
.is_some_and(|d| d == vec!["UNRELEASED"])
{
version
} else {
let mut version = version;
version.increment_debian();
version
};
let mut _bt: Option<breezyshim::tree::RevisionTree> = None;
let basis_tree = if let Some(_basis_tree) = basis_tree {
local_tree.basis_tree().unwrap()
} else {
local_tree.basis_tree().unwrap()
};
let ws = debian_workspace::fs_workspace::FsWorkspace::new(
basedir.as_path(),
Some(package.to_string()),
Some(current_version.clone()),
);
tracing::debug!("Running detector {}", detector.name());
let plan = crate::detector::detect_and_plan(detector, &ws, preferences)?;
let make_changes = |basedir: &std::path::Path| -> Result<_, FixerError> {
crate::builtin_fixers::apply_plan(basedir, plan, &|fixed, actions| {
detector.describe(fixed, actions)
})
};
let (mut result, changes, mut specific_files) = match apply_or_revert(
local_tree,
subpath,
&basis_tree,
dirty_tracker.as_mut(),
make_changes,
) {
Ok(r) => {
if r.0.description.is_empty() {
return Err(FixerError::DescriptionMissing);
}
r
}
Err(ApplyError::NoChanges(r)) => {
if r.overridden_lintian_issues.is_empty() {
return Err(FixerError::NoChanges);
} else {
return Err(FixerError::NoChangesAfterOverrides(
r.overridden_lintian_issues,
));
}
}
Err(ApplyError::BrzError(e)) => {
return Err(e.into());
}
Err(ApplyError::CallbackError(e)) => {
return Err(e);
}
};
let lines = result.description.split('\n').collect::<Vec<_>>();
let mut summary = lines[0].to_string();
let details = lines
.iter()
.skip(1)
.take_while(|l| !l.is_empty())
.collect::<Vec<_>>();
if has_non_debian_changes(changes.as_slice(), subpath)
&& current_version.debian_revision.is_some()
{
let (patch_name, updated_specific_files) = match upstream_changes_to_patch(
local_tree,
&basis_tree,
dirty_tracker.as_mut(),
subpath,
&result
.patch_name
.as_deref()
.map_or_else(|| detector.name().to_string(), |n| n.to_string()),
result.description.as_str(),
timestamp.map(|t| t.date()),
) {
Ok(r) => r,
Err(e) => {
reset_tree_with_dirty_tracker(
local_tree,
Some(&basis_tree),
Some(subpath),
dirty_tracker.as_mut(),
)
.map_err(|e| FixerError::Other(e.to_string()))?;
return Err(FixerError::FailedPatchManipulation(e.to_string()));
}
};
specific_files = Some(updated_specific_files);
summary = format!("Add patch {}: {}", patch_name, summary);
}
let update_changelog = if debian_analyzer::changelog::only_changes_last_changelog_block(
local_tree,
&basis_tree,
changelog_path.as_path(),
changes.iter(),
)? {
false
} else {
update_changelog()
};
if update_changelog {
let summary_with_prefix = format!("* {}", summary);
let details_with_prefix: Vec<String> = details.iter().map(|d| format!("* {}", d)).collect();
let mut entry = vec![summary_with_prefix.as_str()];
entry.extend(details_with_prefix.iter().map(|s| s.as_str()));
add_changelog_entry(local_tree, changelog_path.as_path(), entry.as_slice())?;
if let Some(specific_files) = specific_files.as_mut() {
specific_files.push(changelog_path);
}
}
let mut description = format!("{}\n", result.description);
description.push('\n');
description.push_str(format!("Changes-By: {}\n", changes_by).as_str());
description.push_str(&render_lintian_trailers(&result.fixed_lintian_issues));
let committer = committer.map_or_else(|| get_committer(local_tree), |c| c.to_string());
let specific_files_ref = specific_files
.as_ref()
.map(|fs| fs.iter().map(|p| p.as_path()).collect::<Vec<_>>());
let mut builder = local_tree
.build_commit()
.message(description.as_str())
.allow_pointless(false)
.committer(committer.as_str());
if let Some(specific_files_ref) = specific_files_ref.as_ref() {
builder = builder.specific_files(specific_files_ref);
}
let revid = builder.commit().map_err(|e| match e {
Error::PointlessCommit => FixerError::NoChanges,
Error::NoWhoami => FixerError::Other("No committer specified".to_string()),
e => FixerError::Other(e.to_string()),
})?;
result.revision_id = Some(revid);
Ok((result, summary))
}
#[derive(Debug)]
pub enum OverallError {
NotDebianPackage(std::path::PathBuf),
WorkspaceDirty(std::path::PathBuf),
ChangelogCreate(String),
InvalidChangelog(std::path::PathBuf, String),
BrzError(Error),
IoError(std::io::Error),
Other(String),
}
impl std::fmt::Display for OverallError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
OverallError::NotDebianPackage(path) => {
write!(f, "Not a Debian package: {}", path.display())
}
OverallError::WorkspaceDirty(path) => {
write!(f, "Workspace is dirty: {}", path.display())
}
OverallError::ChangelogCreate(m) => {
write!(f, "Failed to create changelog entry: {}", m)
}
OverallError::Other(e) => write!(f, "{}", e),
OverallError::BrzError(e) => write!(f, "{}", e),
OverallError::IoError(e) => write!(f, "{}", e),
OverallError::InvalidChangelog(path, e) => {
write!(f, "Invalid changelog at {}: {}", path.display(), e)
}
}
}
}
impl std::error::Error for OverallError {}
pub fn run_lintian_fixers(
local_tree: &breezyshim::workingtree::GenericWorkingTree,
detectors: &[Box<dyn crate::detector::Detector>],
mut update_changelog: Option<impl FnMut() -> bool>,
verbose: bool,
committer: Option<&str>,
preferences: &FixerPreferences,
use_dirty_tracker: Option<bool>,
subpath: Option<&std::path::Path>,
changes_by: Option<&str>,
multi_progress: Option<&MultiProgress>,
) -> Result<ManyResult, OverallError> {
let subpath = subpath.unwrap_or_else(|| std::path::Path::new(""));
let mut basis_tree = local_tree.basis_tree().unwrap();
check_clean_tree(local_tree, &basis_tree, subpath).map_err(|e| match e {
Error::WorkspaceDirty(p) => OverallError::WorkspaceDirty(p),
e => OverallError::Other(e.to_string()),
})?;
let mut changelog_behaviour = None;
let mut update_changelog = || {
if let Some(update_changelog) = update_changelog.as_mut() {
return update_changelog();
}
let debian_path = subpath.join("debian");
let cb = determine_update_changelog(local_tree, debian_path.as_path());
changelog_behaviour = Some(cb);
changelog_behaviour.as_ref().unwrap().update_changelog
};
let mut ret = ManyResult::new();
let pb = if let Some(mp) = multi_progress {
mp.add(ProgressBar::new(detectors.len() as u64))
} else {
ProgressBar::new(detectors.len() as u64)
};
pb.set_style(
ProgressStyle::with_template("[{pos}/{len}] {wide_bar} {msg}")
.expect("static template is valid"),
);
#[cfg(test)]
pb.set_draw_target(indicatif::ProgressDrawTarget::hidden());
let mut dirty_tracker = if use_dirty_tracker.unwrap_or(true) {
Some(DirtyTreeTracker::new_in_subpath(
Clone::clone(local_tree),
subpath,
))
} else {
None
};
for detector in detectors {
let fixer_name = detector.name();
pb.set_message(fixer_name);
let start = std::time::SystemTime::now();
if let Some(dirty_tracker) = dirty_tracker.as_mut() {
dirty_tracker.mark_clean();
}
pb.inc(1);
let _span = tracing::info_span!("fixer", name = fixer_name).entered();
match run_lintian_fixer(
local_tree,
detector.as_ref(),
committer,
&mut update_changelog,
preferences,
&mut dirty_tracker,
subpath,
None,
Some(&basis_tree),
changes_by,
) {
Err(e) => match e {
FixerError::NotDebianPackage(path) => {
return Err(OverallError::NotDebianPackage(path));
}
FixerError::ChangelogCreate(ref _m) => {
ret.failed_fixers
.insert(fixer_name.to_string(), e.to_string());
if verbose {
tracing::info!("Fixer {} failed to create changelog entry.", fixer_name);
}
continue;
}
FixerError::OutputParseError(ref _e) => {
ret.failed_fixers
.insert(fixer_name.to_string(), e.to_string());
if verbose {
tracing::info!("Fixer {} failed to parse output.", fixer_name);
}
continue;
}
FixerError::DescriptionMissing => {
ret.failed_fixers
.insert(fixer_name.to_string(), e.to_string());
if verbose {
tracing::info!(
"Fixer {} failed because description is missing.",
fixer_name
);
}
continue;
}
FixerError::FormattingUnpreservable(path) => {
ret.formatting_unpreservable
.insert(fixer_name.to_string(), path.clone());
if verbose {
tracing::info!(
"Fixer {} was unable to preserve formatting of {}.",
fixer_name,
path.display()
);
}
continue;
}
FixerError::GeneratedFile(p) => {
ret.failed_fixers.insert(
fixer_name.to_string(),
format!("Generated file: {}", p.display()),
);
if verbose {
tracing::info!(
"Fixer {} encountered generated file {}",
fixer_name,
p.display()
);
}
}
FixerError::ScriptNotFound(ref p) => {
ret.failed_fixers
.insert(fixer_name.to_string(), e.to_string());
if verbose {
tracing::info!("Fixer {} ({}) not found.", fixer_name, p.display());
}
continue;
}
FixerError::ScriptFailed { .. } => {
ret.failed_fixers
.insert(fixer_name.to_string(), e.to_string());
if verbose {
tracing::info!("Fixer {} failed to run.", fixer_name);
eprintln!("{}", e);
}
continue;
}
FixerError::BrzError(e) => {
return Err(OverallError::BrzError(e));
}
FixerError::Io(e) => {
tracing::error!("Fixer {} hit I/O error: {}", fixer_name, e);
return Err(OverallError::IoError(e));
}
FixerError::NotCertainEnough(actual_certainty, minimum_certainty, _overrides) => {
let duration = std::time::SystemTime::now().duration_since(start).unwrap();
ret.fixer_durations.insert(fixer_name.to_string(), duration);
ret.uncertain_fixers
.insert(fixer_name.to_string(), actual_certainty);
if verbose {
tracing::info!(
"Fixer {} made changes but not high enough certainty (was {}, needed {}). (took: {:2}s)",
fixer_name,
actual_certainty,
minimum_certainty.map_or("default".to_string(), |c| c.to_string()),
duration.as_secs_f32(),
);
}
continue;
}
FixerError::FailedPatchManipulation(ref reason) => {
if verbose {
tracing::info!("Unable to manipulate upstream patches: {}", reason);
}
ret.failed_fixers
.insert(fixer_name.to_string(), e.to_string());
continue;
}
FixerError::NoChanges => {
let duration = std::time::SystemTime::now().duration_since(start).unwrap();
ret.fixer_durations.insert(fixer_name.to_string(), duration);
if verbose {
tracing::info!(
"Fixer {} made no changes. (took: {:2}s)",
fixer_name,
duration.as_secs_f32(),
);
}
continue;
}
FixerError::NoChangesAfterOverrides(os) => {
let duration = std::time::SystemTime::now().duration_since(start).unwrap();
ret.fixer_durations.insert(fixer_name.to_string(), duration);
if verbose {
tracing::info!(
"Fixer {} made no changes. (took: {:2}s)",
fixer_name,
duration.as_secs_f32(),
);
}
ret.overridden_lintian_issues.extend(os);
continue;
}
FixerError::Panic {
ref message,
ref backtrace,
} => {
if verbose {
tracing::error!("Fixer {} panicked: {}", fixer_name, message);
if let Some(bt) = backtrace {
tracing::error!("Backtrace:\n{}", bt);
}
}
ret.failed_fixers
.insert(fixer_name.to_string(), e.to_string());
continue;
}
FixerError::MissingDependency(ref dep) => {
if verbose {
tracing::info!(
"Fixer {} skipped: missing optional dependency '{}'",
fixer_name,
dep
);
}
ret.failed_fixers
.insert(fixer_name.to_string(), e.to_string());
continue;
}
FixerError::Other(ref em) => {
if verbose {
tracing::info!("Fixer {} failed: {}", fixer_name, em);
}
ret.failed_fixers
.insert(fixer_name.to_string(), e.to_string());
continue;
}
FixerError::InvalidChangelog(path, reason) => {
return Err(OverallError::InvalidChangelog(path, reason));
}
},
Ok((result, summary)) => {
let duration = std::time::SystemTime::now().duration_since(start).unwrap();
ret.fixer_durations.insert(fixer_name.to_string(), duration);
if verbose {
tracing::info!(
"Fixer {} made changes. (took {:2}s)",
fixer_name,
duration.as_secs_f32(),
);
}
ret.success.push(FixerSuccess {
result,
summary,
fixer_name: fixer_name.to_string(),
});
basis_tree = local_tree.basis_tree().unwrap();
}
}
}
pb.finish();
ret.changelog_behaviour = changelog_behaviour;
Ok(ret)
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FixerSuccess {
#[serde(flatten)]
pub result: FixerResult,
pub summary: String,
pub fixer_name: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct ManyResult {
#[serde(rename = "applied")]
pub success: Vec<FixerSuccess>,
#[serde(rename = "failed")]
pub failed_fixers: std::collections::HashMap<String, String>,
#[serde(skip)]
pub uncertain_fixers: std::collections::HashMap<String, Certainty>,
pub changelog_behaviour: Option<ChangelogBehaviour>,
#[serde(skip)]
pub overridden_lintian_issues: Vec<LintianIssue>,
#[serde(skip)]
pub formatting_unpreservable: std::collections::HashMap<String, std::path::PathBuf>,
#[serde(skip)]
pub fixer_durations: std::collections::HashMap<String, std::time::Duration>,
}
impl ManyResult {
pub fn tags_count(&self) -> HashMap<&str, u32> {
self.success
.iter()
.fold(HashMap::new(), |mut acc, fixer_success| {
for tag in fixer_success.result.fixed_lintian_tags() {
*acc.entry(tag).or_insert(0) += 1;
}
acc
})
}
pub fn value(&self) -> i32 {
let tags = self
.success
.iter()
.flat_map(|fixer_success| fixer_success.result.fixed_lintian_tags())
.collect::<Vec<_>>();
calculate_value(tags.as_slice())
}
pub fn minimum_success_certainty(&self) -> Certainty {
min_certainty(
self.success
.iter()
.filter_map(|fixer_success| fixer_success.result.certainty)
.collect::<Vec<_>>()
.as_slice(),
)
.unwrap_or(Certainty::Certain)
}
pub fn new() -> Self {
Self {
success: Vec::new(),
failed_fixers: std::collections::HashMap::new(),
uncertain_fixers: std::collections::HashMap::new(),
changelog_behaviour: None,
overridden_lintian_issues: Vec::new(),
formatting_unpreservable: std::collections::HashMap::new(),
fixer_durations: std::collections::HashMap::new(),
}
}
}
fn has_non_debian_changes(changes: &[TreeChange], subpath: &std::path::Path) -> bool {
let debian_path = subpath.join("debian");
changes.iter().any(|change| {
[change.path.0.as_deref(), change.path.1.as_deref()]
.into_iter()
.flatten()
.any(|path| !path.starts_with(&debian_path))
})
}
#[derive(Debug)]
struct FailedPatchManipulation(String);
impl std::fmt::Display for FailedPatchManipulation {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Failed to manipulate patches: {}", self.0)
}
}
impl std::error::Error for FailedPatchManipulation {}
fn upstream_changes_to_patch<T: breezyshim::tree::PyTree>(
local_tree: &breezyshim::workingtree::GenericWorkingTree,
basis_tree: &T,
dirty_tracker: Option<&mut DirtyTreeTracker>,
subpath: &std::path::Path,
patch_name: &str,
description: &str,
timestamp: Option<chrono::naive::NaiveDate>,
) -> Result<(String, Vec<std::path::PathBuf>), FailedPatchManipulation> {
use debian_analyzer::patches::{
move_upstream_changes_to_patch, read_quilt_patches, tree_patches_directory,
};
let patches_directory = tree_patches_directory(local_tree, subpath);
let quilt_patches =
read_quilt_patches(local_tree, patches_directory.as_path()).collect::<Vec<_>>();
if !quilt_patches.is_empty() {
return Err(FailedPatchManipulation(
"Creating patch on top of existing quilt patches not supported.".to_string(),
));
}
tracing::debug!("Moving upstream changes to patch {}", patch_name);
let (specific_files, patch_name) = match move_upstream_changes_to_patch(
local_tree,
basis_tree,
subpath,
patch_name,
description,
dirty_tracker,
timestamp,
) {
Ok(r) => r,
Err(e) => {
return Err(FailedPatchManipulation(e.to_string()));
}
};
Ok((patch_name, specific_files))
}
fn note_changelog_policy(policy: bool, msg: &str) {
lazy_static::lazy_static! {
static ref CHANGELOG_POLICY_NOTED: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
}
if let Ok(mut policy_noted) = CHANGELOG_POLICY_NOTED.lock() {
if !*policy_noted {
let extra = if policy {
"Specify --no-update-changelog to override."
} else {
"Specify --update-changelog to override."
};
tracing::info!("{} {}", msg, extra);
}
*policy_noted = true;
}
}
pub fn determine_update_changelog(
local_tree: &dyn WorkingTree,
debian_path: &std::path::Path,
) -> ChangelogBehaviour {
let changelog_path = debian_path.join("changelog");
let cl = match local_tree.get_file(changelog_path.as_path()) {
Ok(f) => ChangeLog::read_relaxed(f).unwrap(),
Err(Error::NoSuchFile(_)) => {
return ChangelogBehaviour {
update_changelog: false,
explanation: "No changelog found".to_string(),
};
}
Err(e) => {
panic!("Error reading changelog: {}", e);
}
};
let behaviour = guess_update_changelog(local_tree, debian_path, Some(cl));
let behaviour = if let Some(behaviour) = behaviour {
note_changelog_policy(behaviour.update_changelog, behaviour.explanation.as_str());
behaviour
} else {
ChangelogBehaviour {
update_changelog: true,
explanation: "Assuming changelog should be updated".to_string(),
}
};
behaviour
}
#[cfg(test)]
mod tests {
use super::*;
use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
use breezyshim::repository::Repository;
use breezyshim::tree::{MutableTree, Tree, WorkingTree};
use breezyshim::workingtree::GenericWorkingTree;
use breezyshim::Branch;
use std::path::Path;
pub const COMMITTER: &str = "Testsuite <lintian-brush@example.com>";
mod render_lintian_trailers_tests {
use super::*;
#[test]
fn empty_issues_renders_empty_string() {
assert_eq!(render_lintian_trailers(&[]), "");
}
#[test]
fn single_source_tag_without_info() {
let trailers = render_lintian_trailers(&[LintianIssue {
package: Some("blah".to_string()),
package_type: Some(PackageType::Source),
visibility: None,
tag: Some("some-tag".to_string()),
info: None,
}]);
assert_eq!(
trailers,
"Fixes: lintian: blah source: some-tag\n\
See-also: https://lintian.debian.org/tags/some-tag.html\n"
);
}
#[test]
fn info_string_is_preserved_in_fixes_line() {
let trailers = render_lintian_trailers(&[LintianIssue {
package: None,
package_type: Some(PackageType::Source),
visibility: None,
tag: Some("globbing-patterns-out-of-order".to_string()),
info: Some("debian/*".to_string()),
}]);
assert_eq!(
trailers,
"Fixes: lintian: source: globbing-patterns-out-of-order debian/*\n\
See-also: https://lintian.debian.org/tags/globbing-patterns-out-of-order.html\n"
);
}
#[test]
fn repeated_tag_keeps_each_fixes_but_dedupes_see_also() {
let issue = |info: &str| LintianIssue {
package: None,
package_type: Some(PackageType::Source),
visibility: None,
tag: Some("globbing-patterns-out-of-order".to_string()),
info: Some(info.to_string()),
};
let trailers =
render_lintian_trailers(&[issue("debian/*"), issue("src/*"), issue("tests/*")]);
assert_eq!(
trailers,
"Fixes: lintian: source: globbing-patterns-out-of-order debian/*\n\
Fixes: lintian: source: globbing-patterns-out-of-order src/*\n\
Fixes: lintian: source: globbing-patterns-out-of-order tests/*\n\
See-also: https://lintian.debian.org/tags/globbing-patterns-out-of-order.html\n"
);
}
#[test]
fn multiple_distinct_tags_each_get_one_see_also() {
let trailers = render_lintian_trailers(&[
LintianIssue {
package: None,
package_type: Some(PackageType::Source),
visibility: None,
tag: Some("tag-a".to_string()),
info: None,
},
LintianIssue {
package: None,
package_type: Some(PackageType::Source),
visibility: None,
tag: Some("tag-b".to_string()),
info: None,
},
]);
assert_eq!(
trailers,
"Fixes: lintian: source: tag-a\n\
Fixes: lintian: source: tag-b\n\
See-also: https://lintian.debian.org/tags/tag-a.html\n\
See-also: https://lintian.debian.org/tags/tag-b.html\n"
);
}
#[test]
fn see_also_order_follows_first_occurrence_of_tag() {
let issue = |tag: &str, info: &str| LintianIssue {
package: None,
package_type: Some(PackageType::Source),
visibility: None,
tag: Some(tag.to_string()),
info: Some(info.to_string()),
};
let trailers = render_lintian_trailers(&[
issue("tag-b", "x"),
issue("tag-a", "y"),
issue("tag-b", "z"),
]);
let see_also: Vec<&str> = trailers
.lines()
.filter(|l| l.starts_with("See-also:"))
.collect();
assert_eq!(
see_also,
vec![
"See-also: https://lintian.debian.org/tags/tag-b.html",
"See-also: https://lintian.debian.org/tags/tag-a.html",
]
);
}
#[test]
fn binary_package_issue_includes_package_and_type() {
let trailers = render_lintian_trailers(&[LintianIssue {
package: Some("libfoo".to_string()),
package_type: Some(PackageType::Binary),
visibility: None,
tag: Some("some-binary-tag".to_string()),
info: Some("/usr/bin/foo".to_string()),
}]);
assert_eq!(
trailers,
"Fixes: lintian: libfoo binary: some-binary-tag /usr/bin/foo\n\
See-also: https://lintian.debian.org/tags/some-binary-tag.html\n"
);
}
#[test]
fn issue_without_tag_emits_fixes_but_no_see_also() {
let trailers = render_lintian_trailers(&[LintianIssue {
package: None,
package_type: Some(PackageType::Source),
visibility: None,
tag: None,
info: Some("orphaned".to_string()),
}]);
assert_eq!(trailers, "Fixes: lintian: source: orphaned\n");
}
}
mod test_run_lintian_fixer {
use super::*;
use crate::detector::Detector;
use crate::diagnostic::{Action, Diagnostic, FilesystemAction, RunCommandAction};
struct DummyFixer {
name: &'static str,
lintian_tags: &'static [&'static str],
}
impl DummyFixer {
fn new(name: &'static str, lintian_tags: &'static [&'static str]) -> Self {
Self { name, lintian_tags }
}
}
impl Detector for DummyFixer {
fn name(&self) -> &'static str {
self.name
}
fn lintian_tags(&self) -> &'static [&'static str] {
self.lintian_tags
}
fn detect(
&self,
ws: &dyn debian_workspace::Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
let control = std::path::Path::new("debian/control");
let mut content = ws.read_file(control)?.unwrap_or_default().into_owned();
content.extend_from_slice(b"a new line\n");
let issue = LintianIssue {
package: ws.package().map(|s| s.to_string()),
..LintianIssue::source("some-tag", Visibility::Warning)
};
Ok(vec![Diagnostic::with_actions(
issue,
"Fixed some tag.",
"Append a line to debian/control.",
vec![Action::Filesystem(FilesystemAction::Write {
file: control.to_path_buf(),
content,
})],
)
.with_certainty(Certainty::Certain)])
}
fn describe(
&self,
_fixed: &[(Diagnostic, crate::diagnostic::ActionPlan)],
_actions: &[Action],
) -> String {
"Fixed some tag.\nExtended description.".to_string()
}
}
struct FailingFixer {
name: &'static str,
lintian_tags: &'static [&'static str],
}
impl FailingFixer {
fn new(name: &'static str, lintian_tags: &'static [&'static str]) -> Self {
Self { name, lintian_tags }
}
}
impl Detector for FailingFixer {
fn name(&self) -> &'static str {
self.name
}
fn lintian_tags(&self) -> &'static [&'static str] {
self.lintian_tags
}
fn detect(
&self,
_ws: &dyn debian_workspace::Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
Ok(vec![Diagnostic::with_actions(
LintianIssue::source("some-tag", Visibility::Warning),
"Some tag.",
"Run a failing command.",
vec![Action::RunCommand(RunCommandAction::Run {
argv: vec![
"sh".to_string(),
"-c".to_string(),
"echo blah > debian/foo; echo foo > debian/control; \
echo 'Not successful' >&2; exit 1"
.to_string(),
],
scope: std::path::PathBuf::from("."),
env: vec![],
})],
)])
}
}
fn setup(version: Option<&str>) -> (tempfile::TempDir, GenericWorkingTree) {
let version = version.unwrap_or("0.1");
let td = tempfile::tempdir().unwrap();
let tree =
create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
tree.mkdir(std::path::Path::new("debian")).unwrap();
std::fs::write(
td.path().join("debian/control"),
r#"Source: blah
Vcs-Git: https://example.com/blah
Testsuite: autopkgtest
Binary: blah
Arch: all
"#,
)
.unwrap();
tree.add(&[std::path::Path::new("debian/control")]).unwrap();
std::fs::write(
td.path().join("debian/changelog"),
format!(
r#"blah ({}) UNRELEASED; urgency=medium
* Initial release. (Closes: #911016)
-- Blah <example@debian.org> Sat, 13 Oct 2018 11:21:39 +0100
"#,
version
),
)
.unwrap();
tree.add(&[std::path::Path::new("debian/changelog")])
.unwrap();
tree.build_commit()
.message("Initial thingy.")
.committer(COMMITTER)
.commit()
.unwrap();
(td, tree)
}
#[test]
fn test_invalid_changelog_entry() {
let (td, tree) = setup(None);
std::fs::write(
td.path().join("debian/changelog"),
r#"Gson Parent (1.7-1) UNRELEASED; urgency=low
* Initial release.
-- Blah <example@debian.org> Sat, 13 Oct 2018 11:21:39 +0100
"#,
)
.unwrap();
tree.build_commit()
.message("Break the changelog.")
.committer(COMMITTER)
.commit()
.unwrap();
let lock = tree.lock_write().unwrap();
let result = run_lintian_fixer(
&tree,
&DummyFixer::new("dummy", &["some-tag"]),
Some(COMMITTER),
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
None,
None,
None,
);
assert!(
matches!(result, Err(FixerError::InvalidChangelog(..))),
"{:?}",
result
);
std::mem::drop(lock);
std::mem::drop(td);
}
#[test]
fn test_fails() {
let (td, tree) = setup(None);
let lock = tree.lock_write().unwrap();
let result = run_lintian_fixers(
&tree,
&[Box::new(FailingFixer::new("fail", &["some-tag"]))],
Some(|| false),
false,
None,
&FixerPreferences::default(),
None,
None,
None,
None,
)
.unwrap();
std::mem::drop(lock);
assert_eq!(0, result.success.len());
assert_eq!(1, result.failed_fixers.len());
let fixer = result.failed_fixers.get("fail").unwrap();
assert!(fixer.contains("Not successful"));
let lock = tree.lock_read().unwrap();
assert_eq!(
Vec::<breezyshim::tree::TreeChange>::new(),
tree.iter_changes(&tree.basis_tree().unwrap(), None, None, None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
);
std::mem::drop(lock);
std::mem::drop(td);
}
#[test]
fn test_not_debian_tree() {
let (td, tree) = setup(None);
tree.remove(&[(std::path::Path::new("debian/changelog"))])
.unwrap();
std::fs::remove_file(td.path().join("debian/changelog")).unwrap();
tree.build_commit()
.message("not a debian dir")
.committer(COMMITTER)
.commit()
.unwrap();
let lock = tree.lock_write().unwrap();
assert!(matches!(
run_lintian_fixers(
&tree,
&[Box::new(DummyFixer::new("dummy", &["some-tag"][..]))],
Some(|| false),
false,
None,
&FixerPreferences::default(),
None,
None,
None,
None,
),
Err(OverallError::NotDebianPackage(_))
));
std::mem::drop(lock);
std::mem::drop(td);
}
#[test]
fn test_simple_modify() {
let (td, tree) = setup(None);
let lock = tree.lock_write().unwrap();
let result = run_lintian_fixers(
&tree,
&[Box::new(DummyFixer::new("dummy", &["some-tag"]))],
Some(|| false),
false,
Some(COMMITTER),
&FixerPreferences::default(),
None,
None,
None,
None,
)
.unwrap();
let revid = tree.last_revision().unwrap();
std::mem::drop(lock);
assert_eq!(result.success.len(), 1);
assert_eq!(
result.success[0].result,
FixerResult::new(
"Fixed some tag.\nExtended description.".to_string(),
Some(Certainty::Certain),
None,
Some(revid),
vec![LintianIssue {
tag: Some("some-tag".to_string()),
package: Some("blah".to_string()),
info: None,
package_type: Some(PackageType::Source),
visibility: Some(Visibility::Warning),
}],
None,
),
);
assert_eq!(result.success[0].summary, "Fixed some tag.");
assert_eq!(maplit::hashmap! {}, result.failed_fixers);
assert_eq!(2, tree.branch().revno());
let lines = tree
.get_file_lines(std::path::Path::new("debian/control"))
.unwrap();
assert_eq!(lines.last().unwrap(), &b"a new line\n".to_vec());
std::mem::drop(td);
}
#[test]
fn test_below_certainty_recorded_as_uncertain() {
struct UncertainFixer {
name: &'static str,
lintian_tags: &'static [&'static str],
}
impl Detector for UncertainFixer {
fn name(&self) -> &'static str {
self.name
}
fn lintian_tags(&self) -> &'static [&'static str] {
self.lintian_tags
}
fn detect(
&self,
_ws: &dyn debian_workspace::Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
Ok(vec![Diagnostic::with_actions(
LintianIssue::source("some-tag", Visibility::Warning),
"Renamed a file.",
"Renamed a file.",
vec![Action::Filesystem(FilesystemAction::Write {
file: std::path::PathBuf::from("debian/somefile"),
content: b"test".to_vec(),
})],
)
.with_certainty(Certainty::Possible)])
}
}
let (td, tree) = setup(None);
let lock = tree.lock_write().unwrap();
let result = run_lintian_fixers(
&tree,
&[Box::new(UncertainFixer {
name: "dummy",
lintian_tags: &["some-tag"],
})],
Some(|| false),
false,
Some(COMMITTER),
&FixerPreferences {
minimum_certainty: Some(Certainty::Certain),
..Default::default()
},
None,
None,
None,
None,
)
.unwrap();
std::mem::drop(lock);
assert_eq!(0, result.success.len());
assert_eq!(maplit::hashmap! {}, result.failed_fixers);
assert_eq!(
maplit::hashmap! { "dummy".to_string() => Certainty::Possible },
result.uncertain_fixers
);
assert_eq!(1, tree.branch().revno());
std::mem::drop(td);
}
#[test]
fn test_changelog_entry_has_asterisk_prefix() {
let (td, tree) = setup(None);
let lock = tree.lock_write().unwrap();
let _result = run_lintian_fixers(
&tree,
&[Box::new(DummyFixer::new("dummy", &["some-tag"]))],
Some(|| true), false,
Some(COMMITTER),
&FixerPreferences::default(),
None,
None,
None,
None,
)
.unwrap();
std::mem::drop(lock);
let changelog_content =
std::fs::read_to_string(td.path().join("debian/changelog")).unwrap();
let changelog: ChangeLog = changelog_content.parse().unwrap();
let first_entry = changelog.iter().next().unwrap();
let change_lines: Vec<String> = first_entry.change_lines().collect();
let bullet_lines: Vec<String> = change_lines
.iter()
.filter(|line| line.starts_with("* "))
.cloned()
.collect();
assert_eq!(bullet_lines.len(), 3);
assert_eq!(bullet_lines[0], "* Initial release. (Closes: #911016)");
assert_eq!(bullet_lines[1], "* Fixed some tag.");
assert_eq!(bullet_lines[2], "* Extended description.");
std::mem::drop(td);
}
#[test]
fn test_simple_modify_too_uncertain() {
let (td, tree) = setup(None);
struct UncertainFixer {
name: &'static str,
lintian_tags: &'static [&'static str],
}
impl UncertainFixer {
fn new(name: &'static str, lintian_tags: &'static [&'static str]) -> Self {
Self { name, lintian_tags }
}
}
impl Detector for UncertainFixer {
fn name(&self) -> &'static str {
self.name
}
fn lintian_tags(&self) -> &'static [&'static str] {
self.lintian_tags
}
fn detect(
&self,
_ws: &dyn debian_workspace::Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
Ok(vec![Diagnostic::with_actions(
LintianIssue::source("some-tag", Visibility::Warning),
"Renamed a file.",
"Renamed a file.",
vec![Action::Filesystem(FilesystemAction::Write {
file: std::path::PathBuf::from("debian/somefile"),
content: b"test".to_vec(),
})],
)
.with_certainty(Certainty::Possible)])
}
}
let lock_write = tree.lock_write().unwrap();
let result = run_lintian_fixer(
&tree,
&UncertainFixer::new("dummy", &["some-tag"]),
Some(COMMITTER),
|| false,
&FixerPreferences {
minimum_certainty: Some(Certainty::Certain),
..Default::default()
},
&mut None,
Path::new(""),
None,
None,
None,
);
assert!(
matches!(result, Err(FixerError::NotCertainEnough(..))),
"{:?}",
result
);
assert_eq!(1, tree.branch().revno());
std::mem::drop(lock_write);
std::mem::drop(td);
}
#[test]
fn test_simple_modify_acceptably_uncertain() {
let (td, tree) = setup(None);
struct UncertainFixer {
name: &'static str,
lintian_tags: &'static [&'static str],
}
impl UncertainFixer {
fn new(name: &'static str, lintian_tags: &'static [&'static str]) -> Self {
Self { name, lintian_tags }
}
}
impl Detector for UncertainFixer {
fn name(&self) -> &'static str {
self.name
}
fn lintian_tags(&self) -> &'static [&'static str] {
self.lintian_tags
}
fn detect(
&self,
_ws: &dyn debian_workspace::Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
Ok(vec![Diagnostic::with_actions(
LintianIssue::source("some-tag", Visibility::Warning),
"Renamed a file.",
"Renamed a file.",
vec![Action::Filesystem(FilesystemAction::Write {
file: std::path::PathBuf::from("debian/somefile"),
content: b"test".to_vec(),
})],
)
.with_certainty(Certainty::Possible)])
}
}
let lock_write = tree.lock_write().unwrap();
let (_result, summary) = run_lintian_fixer(
&tree,
&UncertainFixer::new("dummy", &["some-tag"]),
Some("Testsuite <lintian-brush@example.com>"),
|| false,
&FixerPreferences {
minimum_certainty: Some(Certainty::Possible),
..Default::default()
},
&mut None,
Path::new(""),
None,
None,
None,
)
.unwrap();
assert_eq!("Renamed a file.", summary);
assert_eq!(2, tree.branch().revno());
std::mem::drop(lock_write);
std::mem::drop(td);
}
#[test]
fn test_new_file() {
let (td, tree) = setup(None);
struct NewFileFixer {
name: &'static str,
lintian_tags: &'static [&'static str],
}
impl NewFileFixer {
fn new(name: &'static str, lintian_tags: &'static [&'static str]) -> Self {
Self { name, lintian_tags }
}
}
impl Detector for NewFileFixer {
fn name(&self) -> &'static str {
self.name
}
fn lintian_tags(&self) -> &'static [&'static str] {
self.lintian_tags
}
fn detect(
&self,
ws: &dyn debian_workspace::Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
let issue = LintianIssue {
package: ws.package().map(|s| s.to_string()),
..LintianIssue::source("some-tag", Visibility::Warning)
};
Ok(vec![Diagnostic::with_actions(
issue,
"Created new file.",
"Created new file.",
vec![Action::Filesystem(FilesystemAction::Write {
file: std::path::PathBuf::from("debian/somefile"),
content: b"test".to_vec(),
})],
)])
}
}
let lock_write = tree.lock_write().unwrap();
let (result, summary) = run_lintian_fixer(
&tree,
&NewFileFixer::new("new-file", &["some-tag"]),
Some(COMMITTER),
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
None,
None,
None,
)
.unwrap();
assert_eq!("Created new file.", summary);
assert_eq!(result.certainty, None);
assert_eq!(result.fixed_lintian_tags(), &["some-tag"]);
let rev = tree
.branch()
.repository()
.get_revision(&tree.last_revision().unwrap())
.unwrap();
assert_eq!(
rev.message,
"Created new file.\n\nChanges-By: lintian-brush\nFixes: lintian: blah source: some-tag\nSee-also: https://lintian.debian.org/tags/some-tag.html\n"
);
assert_eq!(2, tree.branch().revno());
let basis_tree = tree.branch().basis_tree().unwrap();
let basis_lock = basis_tree.lock_read().unwrap();
assert_eq!(
basis_tree
.get_file_text(Path::new("debian/somefile"))
.unwrap(),
b"test"
);
std::mem::drop(basis_lock);
std::mem::drop(lock_write);
std::mem::drop(td);
}
#[test]
fn test_rename_file() {
let (td, tree) = setup(None);
struct RenameFileFixer {
name: &'static str,
lintian_tags: &'static [&'static str],
}
impl RenameFileFixer {
fn new(name: &'static str, lintian_tags: &'static [&'static str]) -> Self {
Self { name, lintian_tags }
}
}
impl Detector for RenameFileFixer {
fn name(&self) -> &'static str {
self.name
}
fn lintian_tags(&self) -> &'static [&'static str] {
self.lintian_tags
}
fn detect(
&self,
_ws: &dyn debian_workspace::Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
Ok(vec![Diagnostic::with_actions(
LintianIssue::source("some-tag", Visibility::Warning),
"Renamed a file.",
"Renamed a file.",
vec![Action::Filesystem(FilesystemAction::Rename {
file: std::path::PathBuf::from("debian/control"),
to: std::path::PathBuf::from("debian/control.blah"),
})],
)])
}
}
let orig_basis_tree = tree.branch().basis_tree().unwrap();
let lock_write = tree.lock_write().unwrap();
let (result, summary) = run_lintian_fixer(
&tree,
&RenameFileFixer::new("rename", &["some-tag"]),
Some(COMMITTER),
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
None,
None,
None,
)
.unwrap();
assert_eq!("Renamed a file.", summary);
assert_eq!(result.certainty, None);
assert_eq!(2, tree.branch().revno());
let basis_tree = tree.branch().basis_tree().unwrap();
let basis_lock = basis_tree.lock_read().unwrap();
let orig_basis_tree_lock = orig_basis_tree.lock_read().unwrap();
assert!(!basis_tree.has_filename(Path::new("debian/control")));
assert!(basis_tree.has_filename(Path::new("debian/control.blah")));
assert_ne!(
orig_basis_tree.get_revision_id(),
basis_tree.get_revision_id()
);
std::mem::drop(orig_basis_tree_lock);
std::mem::drop(basis_lock);
std::mem::drop(lock_write);
std::mem::drop(td);
}
#[test]
fn test_empty_change() {
let (td, tree) = setup(None);
struct EmptyFixer {
name: &'static str,
lintian_tags: &'static [&'static str],
}
impl EmptyFixer {
fn new(name: &'static str, lintian_tags: &'static [&'static str]) -> Self {
Self { name, lintian_tags }
}
}
impl Detector for EmptyFixer {
fn name(&self) -> &'static str {
self.name
}
fn lintian_tags(&self) -> &'static [&'static str] {
self.lintian_tags
}
fn detect(
&self,
_ws: &dyn debian_workspace::Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
Ok(vec![])
}
}
let lock_write = tree.lock_write().unwrap();
let result = run_lintian_fixer(
&tree,
&EmptyFixer::new("empty", &["some-tag"]),
Some(COMMITTER),
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
None,
None,
None,
);
assert!(matches!(result, Err(FixerError::NoChanges)), "{:?}", result);
assert_eq!(1, tree.branch().revno());
assert_eq!(
Vec::<breezyshim::tree::TreeChange>::new(),
tree.iter_changes(&tree.basis_tree().unwrap(), None, None, None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
);
std::mem::drop(lock_write);
std::mem::drop(td);
}
#[test]
fn test_upstream_change() {
let (td, tree) = setup(Some("0.1-1"));
#[derive(Debug)]
struct NewFileFixer {
name: &'static str,
lintian_tags: &'static [&'static str],
}
impl NewFileFixer {
fn new(name: &'static str, lintian_tags: &'static [&'static str]) -> Self {
Self { name, lintian_tags }
}
}
impl Detector for NewFileFixer {
fn name(&self) -> &'static str {
self.name
}
fn lintian_tags(&self) -> &'static [&'static str] {
self.lintian_tags
}
fn detect(
&self,
_ws: &dyn debian_workspace::Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
Ok(vec![Diagnostic::untagged(
"Created new configure.ac.",
"Created new configure.ac.",
vec![Action::Filesystem(FilesystemAction::Write {
file: std::path::PathBuf::from("configure.ac"),
content: b"AC_INIT(foo, bar)\n".to_vec(),
})],
)
.with_patch_name("add-config")])
}
}
let lock = tree.lock_write().unwrap();
let (result, summary) = run_lintian_fixer(
&tree,
&NewFileFixer::new("add-config", &["add-config"]),
Some(COMMITTER),
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
Some(
chrono::DateTime::parse_from_rfc3339("2020-09-08T00:36:35Z")
.unwrap()
.naive_utc(),
),
None,
None,
)
.unwrap();
assert_eq!(
summary,
"Add patch add-config.patch: Created new configure.ac."
);
assert_eq!(result.certainty, None);
let rev = tree
.branch()
.repository()
.get_revision(&tree.last_revision().unwrap())
.unwrap();
assert_eq!(
rev.message,
"Created new configure.ac.\n\nChanges-By: lintian-brush\n"
);
assert_eq!(2, tree.branch().revno());
let basis_tree = tree.branch().basis_tree().unwrap();
let basis_lock = basis_tree.lock_read().unwrap();
assert_eq!(
basis_tree
.get_file_text(Path::new("debian/patches/series"))
.unwrap(),
b"add-config.patch\n"
);
let lines = basis_tree
.get_file_lines(Path::new("debian/patches/add-config.patch"))
.unwrap();
assert_eq!(lines[0], b"Description: Created new configure.ac.\n");
assert_eq!(lines[1], b"Origin: other\n");
assert_eq!(lines[2], b"Last-Update: 2020-09-08\n");
assert_eq!(lines[3], b"---\n");
assert_eq!(lines[4], b"=== added file 'configure.ac'\n");
assert_eq!(
&lines[5][..(b"--- a/configure.ac".len())],
b"--- a/configure.ac"
);
assert_eq!(
&lines[6][..(b"+++ b/configure.ac".len())],
b"+++ b/configure.ac"
);
assert_eq!(lines[7], b"@@ -0,0 +1,1 @@\n");
assert_eq!(lines[8], b"+AC_INIT(foo, bar)\n");
std::mem::drop(basis_lock);
std::mem::drop(lock);
std::mem::drop(td);
}
#[test]
fn test_upstream_change_stacked() {
let (td, tree) = setup(Some("0.1-1"));
std::fs::create_dir(td.path().join("debian/patches")).unwrap();
std::fs::write(td.path().join("debian/patches/series"), "foo\n").unwrap();
std::fs::write(
td.path().join("debian/patches/foo"),
r###"--- /dev/null 2020-09-07 13:26:27.546468905 +0000
+++ a 2020-09-08 01:26:25.811742671 +0000
@@ -0,0 +1 @@
+foo
"###,
)
.unwrap();
tree.add(&[
Path::new("debian/patches"),
Path::new("debian/patches/series"),
Path::new("debian/patches/foo"),
])
.unwrap();
tree.build_commit()
.committer(COMMITTER)
.message("Add patches")
.commit()
.unwrap();
struct NewFileFixer {
name: &'static str,
lintian_tags: &'static [&'static str],
}
impl NewFileFixer {
fn new(name: &'static str, lintian_tags: &'static [&'static str]) -> Self {
Self { name, lintian_tags }
}
}
impl Detector for NewFileFixer {
fn name(&self) -> &'static str {
self.name
}
fn lintian_tags(&self) -> &'static [&'static str] {
self.lintian_tags
}
fn detect(
&self,
_ws: &dyn debian_workspace::Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
Ok(vec![Diagnostic::untagged(
"Created new configure.ac.",
"Created new configure.ac.",
vec![Action::Filesystem(FilesystemAction::Write {
file: std::path::PathBuf::from("configure.ac"),
content: b"AC_INIT(foo, bar)\n".to_vec(),
})],
)
.with_patch_name("add-config")])
}
}
let lock = tree.lock_write().unwrap();
let result = run_lintian_fixer(
&tree,
&NewFileFixer::new("add-config", &["add-config"]),
Some(COMMITTER),
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
Some(
chrono::DateTime::parse_from_rfc3339("2020-09-08T00:36:35Z")
.unwrap()
.naive_utc(),
),
None,
None,
);
std::mem::drop(lock);
assert!(matches!(
result,
Err(FixerError::FailedPatchManipulation(..))
));
std::mem::drop(td);
}
fn make_package_tree(path: &Path, format: &str) -> GenericWorkingTree {
let tree = create_standalone_workingtree(path, format).unwrap();
std::fs::create_dir(path.join("debian")).unwrap();
std::fs::write(
path.join("debian/control"),
r#""Source: blah
Vcs-Git: https://example.com/blah
Testsuite: autopkgtest
Binary: blah
Arch: all
"#,
)
.unwrap();
std::fs::write(
path.join("debian/changelog"),
r#"blah (0.1-1) UNRELEASED; urgency=medium
* Initial release. (Closes: #911016)
-- Blah <example@debian.org> Sat, 13 Oct 2018 11:21:39 +0100
"#,
)
.unwrap();
tree.add(&[
Path::new("debian"),
Path::new("debian/changelog"),
Path::new("debian/control"),
])
.unwrap();
tree.build_commit()
.committer(COMMITTER)
.message("Initial thingy.")
.commit()
.unwrap();
tree
}
fn make_change(tree: &GenericWorkingTree, committer: Option<&str>) {
let lock = tree.lock_write().unwrap();
let (result, summary) = run_lintian_fixer(
tree,
&DummyFixer::new("dummy", &["some-tag"]),
committer,
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
None,
None,
None,
)
.unwrap();
assert_eq!(summary, "Fixed some tag.");
assert_eq!(vec!["some-tag"], result.fixed_lintian_tags());
assert_eq!(Some(Certainty::Certain), result.certainty);
assert_eq!(2, tree.branch().revno());
let lines = tree.get_file_lines(Path::new("debian/control")).unwrap();
assert_eq!(lines.last().unwrap(), b"a new line\n");
std::mem::drop(lock);
}
#[test]
fn test_honors_tree_committer_specified() {
let td = tempfile::tempdir().unwrap();
let tree = make_package_tree(td.path(), "git");
make_change(&tree, Some("Jane Example <jane@example.com>"));
let rev = tree
.branch()
.repository()
.get_revision(&tree.branch().last_revision())
.unwrap();
assert_eq!(rev.committer, "Jane Example <jane@example.com>");
}
#[test]
fn test_honors_tree_committer_config() {
let td = tempfile::tempdir().unwrap();
let tree = make_package_tree(td.path(), "git");
std::fs::write(
td.path().join(".git/config"),
r###"
[user]
email = jane@example.com
name = Jane Example
"###,
)
.unwrap();
make_change(&tree, None);
let rev = tree
.branch()
.repository()
.get_revision(&tree.branch().last_revision())
.unwrap();
assert_eq!(rev.committer, "Jane Example <jane@example.com>");
}
}
mod many_result_tests {
use super::*;
#[test]
fn test_empty() {
let result = ManyResult::default();
assert_eq!(Certainty::Certain, result.minimum_success_certainty());
}
#[test]
fn test_no_certainty() {
let mut result = ManyResult::default();
result.success.push(FixerSuccess {
result: FixerResult::new(
"Do bla".to_string(),
None,
None,
None,
vec![LintianIssue::just_tag("tag-a".to_string())],
None,
),
summary: "summary".to_string(),
fixer_name: "test-fixer".to_string(),
});
assert_eq!(Certainty::Certain, result.minimum_success_certainty());
}
#[test]
fn test_possible() {
let mut result = ManyResult::default();
result.success.push(FixerSuccess {
result: FixerResult::new(
"Do bla".to_string(),
Some(Certainty::Possible),
None,
None,
vec![LintianIssue::just_tag("tag-a".to_string())],
None,
),
summary: "summary".to_string(),
fixer_name: "test-fixer-1".to_string(),
});
result.success.push(FixerSuccess {
result: FixerResult::new(
"Do bloeh".to_string(),
Some(Certainty::Certain),
None,
None,
vec![LintianIssue::just_tag("tag-b".to_string())],
None,
),
summary: "summary".to_string(),
fixer_name: "test-fixer-2".to_string(),
});
assert_eq!(Certainty::Possible, result.minimum_success_certainty());
}
}
}
#[cfg(test)]
mod fixer_tests;
#[cfg(test)]
mod fixer_result_builder_tests {
use super::*;
#[test]
fn test_fixer_result_builder_basic() {
let result = FixerResult::builder("Test fix").build();
assert_eq!(result.description, "Test fix");
assert_eq!(result.certainty, None);
assert_eq!(result.patch_name, None);
assert_eq!(result.revision_id, None);
assert_eq!(result.fixed_lintian_issues.len(), 0);
assert_eq!(result.overridden_lintian_issues.len(), 0);
}
#[test]
fn test_fixer_result_builder_with_certainty() {
let result = FixerResult::builder("Test fix")
.certainty(Certainty::Confident)
.build();
assert_eq!(result.description, "Test fix");
assert_eq!(result.certainty, Some(Certainty::Confident));
}
#[test]
fn test_fixer_result_builder_with_patch_name() {
let result = FixerResult::builder("Test fix")
.patch_name("test.patch")
.build();
assert_eq!(result.description, "Test fix");
assert_eq!(result.patch_name, Some("test.patch".to_string()));
}
#[test]
fn test_fixer_result_builder_with_fixed_tags() {
let result = FixerResult::builder("Test fix")
.fixed_issue(LintianIssue::just_tag("tag1".to_string()))
.fixed_issue(LintianIssue::just_tag("tag2".to_string()))
.build();
assert_eq!(result.description, "Test fix");
assert_eq!(result.fixed_lintian_tags(), vec!["tag1", "tag2"]);
}
#[test]
fn test_fixer_result_builder_with_fixed_tags_batch() {
let result = FixerResult::builder("Test fix")
.fixed_issues([
LintianIssue::just_tag("tag1".to_string()),
LintianIssue::just_tag("tag2".to_string()),
LintianIssue::just_tag("tag3".to_string()),
])
.build();
assert_eq!(result.description, "Test fix");
assert_eq!(result.fixed_lintian_tags(), vec!["tag1", "tag2", "tag3"]);
}
#[test]
fn test_fixer_result_builder_with_fixed_issues() {
let issue1 = LintianIssue::just_tag("tag1".to_string());
let issue2 = LintianIssue::just_tag("tag2".to_string());
let result = FixerResult::builder("Test fix")
.fixed_issue(issue1)
.fixed_issue(issue2)
.build();
assert_eq!(result.description, "Test fix");
assert_eq!(result.fixed_lintian_tags(), vec!["tag1", "tag2"]);
}
#[test]
fn test_fixer_result_builder_with_fixed_issues_batch() {
let issues = vec![
LintianIssue::just_tag("tag1".to_string()),
LintianIssue::just_tag("tag2".to_string()),
];
let result = FixerResult::builder("Test fix")
.fixed_issues(issues)
.build();
assert_eq!(result.description, "Test fix");
assert_eq!(result.fixed_lintian_tags(), vec!["tag1", "tag2"]);
}
#[test]
fn test_fixer_result_builder_with_overridden_issues() {
let issue = LintianIssue::just_tag("overridden-tag".to_string());
let result = FixerResult::builder("Test fix")
.overridden_issue(issue)
.build();
assert_eq!(result.description, "Test fix");
assert_eq!(result.overridden_lintian_issues.len(), 1);
assert_eq!(
result.overridden_lintian_issues[0].tag,
Some("overridden-tag".to_string())
);
}
#[test]
fn test_fixer_result_builder_with_overridden_issues_batch() {
let issues = vec![
LintianIssue::just_tag("tag1".to_string()),
LintianIssue::just_tag("tag2".to_string()),
];
let result = FixerResult::builder("Test fix")
.overridden_issues(issues)
.build();
assert_eq!(result.description, "Test fix");
assert_eq!(result.overridden_lintian_issues.len(), 2);
}
#[test]
fn test_fixer_result_builder_chain_all() {
let revision_id = breezyshim::RevisionId::null();
let result = FixerResult::builder("Test fix")
.certainty(Certainty::Certain)
.patch_name("comprehensive.patch")
.revision_id(revision_id.clone())
.fixed_issue(LintianIssue::just_tag("fixed-tag".to_string()))
.overridden_issue(LintianIssue::just_tag("overridden-tag".to_string()))
.build();
assert_eq!(result.description, "Test fix");
assert_eq!(result.certainty, Some(Certainty::Certain));
assert_eq!(result.patch_name, Some("comprehensive.patch".to_string()));
assert_eq!(result.revision_id, Some(revision_id));
assert_eq!(result.fixed_lintian_tags(), vec!["fixed-tag"]);
assert_eq!(result.overridden_lintian_issues.len(), 1);
}
#[test]
fn test_fixer_result_builder_mixed_tags_and_issues() {
let issue = LintianIssue::just_tag("issue-tag".to_string());
let result = FixerResult::builder("Test fix")
.fixed_issue(LintianIssue::just_tag("tag1".to_string()))
.fixed_issue(issue)
.fixed_issue(LintianIssue::just_tag("tag2".to_string()))
.build();
let tags = result.fixed_lintian_tags();
assert_eq!(tags.len(), 3);
assert!(tags.contains(&"tag1"));
assert!(tags.contains(&"tag2"));
assert!(tags.contains(&"issue-tag"));
}
}