From f5466fe720f601459d0ce29f9c38491540ef6cf4 Mon Sep 17 00:00:00 2001 From: messense Date: Wed, 30 Nov 2022 13:47:41 +0800 Subject: [PATCH] Add JUnit xml output format (#968) --- Cargo.lock | 64 ++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 1 + README.md | 4 +-- src/checks.rs | 3 +- src/commands.rs | 5 +++- src/printer.rs | 44 +++++++++++++++++++++++++++++ src/settings/types.rs | 1 + 7 files changed, 114 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5537ec1349..c3c8e17c2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,13 +220,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", + "js-sys", "num-integer", "num-traits", + "time", + "wasm-bindgen", "winapi 0.3.9", ] @@ -883,9 +886,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", @@ -1220,6 +1223,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nextest-workspace-hack" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d906846a98739ed9d73d66e62c2641eef8321f1734b7a1156ab045a0248fb2b3" + [[package]] name = "nix" version = "0.24.2" @@ -1606,6 +1615,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-junit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b909fe9bf2abb1e3d6a97c9189a37c8105c61d03dca9ce6aace023e7d682bd" +dependencies = [ + "chrono", + "indexmap", + "nextest-workspace-hack", + "quick-xml", + "thiserror", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.21" @@ -1835,6 +1867,7 @@ dependencies = [ "num-bigint", "once_cell", "path-absolutize", + "quick-junit", "rayon", "regex", "ropey", @@ -2263,6 +2296,17 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -2489,6 +2533,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" + [[package]] name = "version_check" version = "0.9.4" @@ -2527,6 +2577,12 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index a673fe7606..716513ad72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ notify = { version = "4.0.17" } num-bigint = { version = "0.4.3" } once_cell = { version = "1.16.0" } path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] } +quick-junit = "0.3.2" rayon = { version = "1.5.3" } regex = { version = "1.6.0" } ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false } diff --git a/README.md b/README.md index 80724853c3..5de3cdb410 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ Options: --per-file-ignores List of mappings from file pattern to code to exclude --format - Output serialization format for error messages [default: text] [possible values: text, json, grouped] + Output serialization format for error messages [default: text] [possible values: text, json, junit, grouped] --show-source Show violations with source code --show-files @@ -1457,7 +1457,7 @@ line-length = 120 #### [`format`](#format) The style in which violation messages should be formatted: `"text"` (default), `"grouped"` -(group messages by file), or `"json"` (machine-readable). +(group messages by file), `"json"` (machine-readable), or `"junit"` (machine-readable XML). **Default value**: `"text"` diff --git a/src/checks.rs b/src/checks.rs index 3995c5a18f..fc40c88c59 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use itertools::Itertools; use rustpython_parser::ast::Location; use serde::{Deserialize, Serialize}; -use strum_macros::{AsRefStr, EnumIter, EnumString}; +use strum_macros::{AsRefStr, Display, EnumIter, EnumString}; use crate::ast::types::Range; use crate::autofix::Fix; @@ -17,6 +17,7 @@ use crate::pyupgrade::types::Primitive; EnumIter, EnumString, Debug, + Display, PartialEq, Eq, Clone, diff --git a/src/commands.rs b/src/commands.rs index e8aea7ffd1..9f5624c762 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use anyhow::Result; +use anyhow::{bail, Result}; use serde::Serialize; use walkdir::DirEntry; @@ -61,6 +61,9 @@ pub fn explain(code: &CheckCode, format: SerializationFormat) -> Result<()> { })? ); } + SerializationFormat::Junit => { + bail!("`--explain` does not support junit format") + } }; Ok(()) } diff --git a/src/printer.rs b/src/printer.rs index 01ef8d3b05..271bb38adf 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -96,6 +96,50 @@ impl<'a> Printer<'a> { )? ); } + SerializationFormat::Junit => { + use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite}; + + // Group by filename. + let mut grouped_messages = BTreeMap::default(); + for message in &diagnostics.messages { + grouped_messages + .entry(&message.filename) + .or_insert_with(Vec::new) + .push(message); + } + + let mut report = Report::new("ruff"); + for (filename, messages) in grouped_messages { + let mut test_suite = TestSuite::new(filename); + test_suite + .extra + .insert("package".to_string(), "org.ruff".to_string()); + for message in messages { + let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure); + status.set_message(message.kind.body()); + status.set_description(format!( + "line {}, col {}, {}", + message.location.row(), + message.location.column(), + message.kind.body() + )); + let mut case = + TestCase::new(format!("org.ruff.{}", message.kind.code()), status); + let file_path = Path::new(filename); + let file_stem = file_path.file_stem().unwrap().to_str().unwrap(); + let classname = file_path.parent().unwrap().join(file_stem); + case.set_classname(classname.to_str().unwrap()); + case.extra + .insert("line".to_string(), message.location.row().to_string()); + case.extra + .insert("column".to_string(), message.location.column().to_string()); + + test_suite.add_test_case(case); + } + report.add_test_suite(test_suite); + } + println!("{}", report.to_string().unwrap()); + } SerializationFormat::Text => { self.pre_text(diagnostics); diff --git a/src/settings/types.rs b/src/settings/types.rs index 7a01b6b654..d32a6f5ea3 100644 --- a/src/settings/types.rs +++ b/src/settings/types.rs @@ -148,5 +148,6 @@ impl FromStr for PatternPrefixPair { pub enum SerializationFormat { Text, Json, + Junit, Grouped, }