Compare commits

..

4 commits

20 changed files with 14426 additions and 81 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target /target
/dist /dist
/.env /.env
*.snap.new

80
Cargo.lock generated
View file

@ -156,6 +156,7 @@ dependencies = [
"hex", "hex",
"http", "http",
"humansize", "humansize",
"junit-parser",
"mime", "mime",
"mime_guess", "mime_guess",
"once_cell", "once_cell",
@ -163,6 +164,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"pin-project", "pin-project",
"proptest", "proptest",
"quick-xml",
"quick_cache", "quick_cache",
"rand", "rand",
"regex", "regex",
@ -497,6 +499,18 @@ dependencies = [
"unicode_categories", "unicode_categories",
] ]
[[package]]
name = "console"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "constant_time_eq" name = "constant_time_eq"
version = "0.1.5" version = "0.1.5"
@ -620,6 +634,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
"serde",
] ]
[[package]] [[package]]
@ -695,6 +710,12 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "entities" name = "entities"
version = "1.0.1" version = "1.0.1"
@ -1217,6 +1238,19 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "insta"
version = "1.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5"
dependencies = [
"console",
"lazy_static",
"linked-hash-map",
"serde",
"similar",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.9.0" version = "2.9.0"
@ -1253,6 +1287,19 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "junit-parser"
version = "0.1.0"
dependencies = [
"insta",
"once_cell",
"path_macro",
"quick-xml",
"serde",
"thiserror",
"time",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -1271,6 +1318,12 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.14" version = "0.4.14"
@ -1705,6 +1758,15 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quick_cache" name = "quick_cache"
version = "0.5.1" version = "0.5.1"
@ -2182,6 +2244,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "similar"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -2356,10 +2424,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa",
"num-conv", "num-conv",
"powerfmt", "powerfmt",
"serde", "serde",
"time-core", "time-core",
"time-macros",
] ]
[[package]] [[package]]
@ -2368,6 +2438,16 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.0" version = "1.6.0"

View file

@ -39,12 +39,14 @@ headers = "0.4.0"
hex = "0.4.3" hex = "0.4.3"
http = "1.1.0" http = "1.1.0"
humansize = "2.1.3" humansize = "2.1.3"
junit-parser = { path = "crates/junit-parser" }
mime = "0.3.17" mime = "0.3.17"
mime_guess = "2.0.4" mime_guess = "2.0.4"
once_cell = "1.19.0" once_cell = "1.19.0"
path_macro = "1.0.0" path_macro = "1.0.0"
percent-encoding = "2.3.1" percent-encoding = "2.3.1"
pin-project = "1.1.5" pin-project = "1.1.5"
quick-xml = { version = "0.31.0", features = ["escape-html"] }
quick_cache = "0.5.1" quick_cache = "0.5.1"
rand = "0.8.5" rand = "0.8.5"
regex = "1.10.4" regex = "1.10.4"

View file

@ -89,6 +89,7 @@ Artifactview is configured using environment variables.
| `REPO_BLACKLIST` | - | List of sites/users/repos that can NOT be accessed. The blacklist takes precedence over the whitelist (repos included in both lists cannot be accessed)<br />Example: `github.com/evil-corp/world-destruction;codeberg.org/blackhat;example.org` | | `REPO_BLACKLIST` | - | List of sites/users/repos that can NOT be accessed. The blacklist takes precedence over the whitelist (repos included in both lists cannot be accessed)<br />Example: `github.com/evil-corp/world-destruction;codeberg.org/blackhat;example.org` |
| `REPO_WHITELIST` | - | List of sites/users/repos that can ONLY be accessed. If the whitelist is empty, it will be ignored and any repository can be accessed. Uses the same syntax as `REPO_BLACLIST`. | | `REPO_WHITELIST` | - | List of sites/users/repos that can ONLY be accessed. If the whitelist is empty, it will be ignored and any repository can be accessed. Uses the same syntax as `REPO_BLACLIST`. |
| `SITE_ALIASES` | - | Aliases for sites to make URLs shorter<br />Example: `gh => github.com;cb => codeberg.org` | | `SITE_ALIASES` | - | Aliases for sites to make URLs shorter<br />Example: `gh => github.com;cb => codeberg.org` |
| `VIEWER_MAX_SIZE` | 500000 | Maximum file size to be displayed using the viewer |
## Technical details ## Technical details

View file

@ -0,0 +1,18 @@
[package]
name = "junit-parser"
version = "0.1.0"
edition = "2021"
authors = ["Boris Faure <boris@fau.re>"]
license = "BSD-2-Clause"
repository = "https://github.com/borisfaure/junit-parser"
[dependencies]
quick-xml = { version = "0.31.0", features = ["escape-html"] }
thiserror = "1.0.61"
time = { version = "0.3.36", features = ["parsing", "serde-well-known"] }
serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
insta = { version = "1.39.0", features = ["json"] }
once_cell = "1.19.0"
path_macro = "1.0.0"

View file

@ -0,0 +1,40 @@
#![warn(missing_docs)]
use thiserror::Error;
/// Error enumerates all possible errors returned by this library.
#[derive(Error, Debug)]
pub enum Error {
/// Error while parsing XML
#[error("Error while parsing XML: {0}")]
Xml(#[from] ::quick_xml::Error),
/// Error while converting f64 attribute
#[error("Error while converting f64 attribute: {0}")]
ParseFloat(#[from] std::num::ParseFloatError),
/// Error while converting u64 attribute
#[error("Error while converting u64 attribute: {0}")]
ParseInt(#[from] std::num::ParseIntError),
/// Error while converting bytes to Utf8
#[error("Error while converting bytes to Utf8: {0}")]
ParseUt8(#[from] std::str::Utf8Error),
#[error("Error while parsing timestamp: {0}")]
ParseTimestamp(#[from] time::error::Parse),
/// Error parsing the `property` element: missing `name`
#[error("Missing `name` attribute in property")]
MissingPropertyName,
}
impl From<::quick_xml::events::attributes::AttrError> for Error {
#[inline]
/// Convert [`::quick_xml::events::attributes`] into [`Error::XMLError`]
fn from(err: ::quick_xml::events::attributes::AttrError) -> Error {
Error::Xml(err.into())
}
}
impl From<::quick_xml::escape::EscapeError> for Error {
#[inline]
/// Convert [`::quick_xml::escape::EscapeError`] into [`Error::XMLError`]
fn from(err: ::quick_xml::escape::EscapeError) -> Error {
Error::Xml(err.into())
}
}

View file

@ -0,0 +1,564 @@
use std::borrow::Cow;
use std::io::BufRead;
use quick_xml::escape::unescape;
use quick_xml::events::BytesStart as XMLBytesStart;
use quick_xml::events::Event as XMLEvent;
use quick_xml::name::QName;
use quick_xml::Error as XMLError;
use quick_xml::Reader as XMLReader;
use serde::{Deserialize, Serialize};
use std::str;
use time::OffsetDateTime;
mod errors;
use errors::Error;
/// Struct representing a JUnit report, containing test suites
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct TestSuites {
/// Name of the test suites, from the `name` attribute
pub name: String,
/// List of tests suites represented by [`TestSuite`]
pub suites: Vec<TestSuite>,
/// How long the test suites took to run, from the `time` attribute
pub time: f64,
/// Number of tests in the test suites, from the `tests` attribute
pub tests: u64,
/// Number of tests in error in the test suites, from the `errors` attribute
pub errors: u64,
/// Number of tests in failure in the test suites, from the `failures` attribute
pub failures: u64,
/// Number of tests skipped in the test suites, from the `skipped` attribute
pub skipped: u64,
/// Number of tests that passed after failed attempts
pub flaky: u64,
}
/// A test suite, containing test cases [`TestCase`]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct TestSuite {
/// Name of the test suite, from the `name` attribute
pub name: String,
/// Timestamp when the test suite was run, from the `timestamp` attribute
#[serde(with = "time::serde::rfc3339::option")]
pub timestamp: Option<OffsetDateTime>,
/// List of status of tests represented by [`TestCase`]
pub cases: Vec<TestCase>,
/// How long the test suite took to run, from the `time` attribute
pub time: f64,
/// Number of tests in the test suite, from the `tests` attribute
pub tests: u64,
/// Number of tests in error in the test suite, from the `errors` attribute
pub errors: u64,
/// Number of tests in failure in the test suite, from the `failures` attribute
pub failures: u64,
/// Number of tests skipped in the test suites, from the `skipped` attribute
pub skipped: u64,
/// Number of tests that passed after failed attempts
pub flaky: u64,
}
/// A test case
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct TestCase {
/// Name of the test case, from the `name` attribute
pub name: String,
/// Original name, from the `name` attribute
pub original_name: String,
/// Class name, from the `classname` attribute
pub classname: Option<String>,
/// Timestamp when the test case was run
#[serde(with = "time::serde::rfc3339::option")]
pub timestamp: Option<OffsetDateTime>,
/// Run time in seconds
pub time: f64,
/// Status of the test case
pub status: TestStatus,
/// stdout output from the `system-out` element
pub system_out: Option<String>,
/// stderr output from the `system-err` element
pub system_err: Option<String>,
/// Previous test attempts, from `rerunFailure` and `flakyFailure` element
pub retries: Vec<Retry>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub enum TestStatus {
#[default]
Success,
Error(Message),
Failure(Message),
Flaky,
Skipped,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Message {
pub message: String,
pub text: String,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Retry {
/// Timestamp when the retry was run
#[serde(with = "time::serde::rfc3339::option")]
pub timestamp: Option<OffsetDateTime>,
/// Run time in seconds
pub time: f64,
/// Status of the retry
pub status: TestStatus,
/// stdout output from the `system-out` element
pub system_out: Option<String>,
/// stderr output from the `system-err` element
pub system_err: Option<String>,
}
impl TestSuites {
/// Fill up `self` with attributes from the XML tag
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
QName(b"tests") => self.tests = try_from_attribute_value_u64(a.value)?,
QName(b"errors") => self.errors = try_from_attribute_value_u64(a.value)?,
QName(b"failures") => self.failures = try_from_attribute_value_u64(a.value)?,
QName(b"skipped") => self.skipped = try_from_attribute_value_u64(a.value)?,
QName(b"name") => self.name = try_from_attribute_value_string(a.value)?,
_ => {}
};
}
Ok(())
}
/// New [`TestSuites`] from empty XML tag
fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
let mut ts = Self::default();
ts.parse_attributes(e)?;
Ok(ts)
}
/// New [`TestSuites`] from XML tree
fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
let mut ts = Self::default();
ts.parse_attributes(e)?;
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testsuites") => break,
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testrun") => break,
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testsuite") => {
ts.suites.push(TestSuite::from_reader(e, r)?);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testsuite") => {
ts.suites.push(TestSuite::new_empty(e)?);
}
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof("testsuites".to_string()).into())
}
Err(err) => return Err(err.into()),
_ => (),
}
}
ts.flaky = ts.suites.iter().map(|s| s.flaky).sum();
Ok(ts)
}
}
impl TestSuite {
/// Fill up `self` with attributes from the XML tag
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
QName(b"tests") => self.tests = try_from_attribute_value_u64(a.value)?,
QName(b"errors") => self.errors = try_from_attribute_value_u64(a.value)?,
QName(b"failures") => self.failures = try_from_attribute_value_u64(a.value)?,
QName(b"skipped") => self.skipped = try_from_attribute_value_u64(a.value)?,
QName(b"name") => self.name = try_from_attribute_value_string(a.value)?,
QName(b"timestamp") => {
self.timestamp = Some(try_from_attribute_value_timestamp(a.value)?)
}
_ => {}
};
}
Ok(())
}
/// New [`TestSuite`] from empty XML tag
fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
let mut ts = Self::default();
ts.parse_attributes(e)?;
Ok(ts)
}
/// New [`TestSuite`] from XML tree
fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
let mut ts = Self::default();
ts.parse_attributes(e)?;
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testsuite") => break,
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testcase") => {
ts.cases.push(TestCase::from_reader(e, r)?);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testcase") => {
ts.cases.push(TestCase::new_empty(e)?);
}
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof("testsuite".to_string()).into())
}
Err(err) => return Err(err.into()),
_ => (),
}
}
ts.flaky = ts
.cases
.iter()
.filter(|c| matches!(c.status, TestStatus::Flaky))
.count() as u64;
Ok(ts)
}
}
impl TestCase {
/// Fill up `self` with attributes from the XML tag
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
QName(b"timestamp") => {
self.timestamp = Some(try_from_attribute_value_timestamp(a.value)?)
}
QName(b"name") => self.original_name = try_from_attribute_value_string(a.value)?,
QName(b"classname") => {
self.classname = Some(try_from_attribute_value_string(a.value)?)
}
_ => {}
}
}
if let Some(cn) = self.classname.as_ref() {
self.name = format!("{}::{}", cn, self.original_name);
} else {
self.name.clone_from(&self.original_name);
}
Ok(())
}
/// New [`TestCase`] from empty XML tag
fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
let mut tc = Self::default();
tc.parse_attributes(e)?;
Ok(tc)
}
/// New [`TestCase`] from XML tree
fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
let mut tc = Self::default();
tc.parse_attributes(e)?;
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf)? {
XMLEvent::End(ref e) if e.name() == QName(b"testcase") => break,
XMLEvent::Start(ref e) if e.name() == QName(b"skipped") => {
tc.status = TestStatus::Skipped;
}
XMLEvent::Empty(ref e) if e.name() == QName(b"skipped") => {
tc.status = TestStatus::Skipped;
}
XMLEvent::Start(ref e) if e.name() == QName(b"failure") => {
let msg = Message::from_reader(e, r)?;
tc.status = TestStatus::Failure(msg);
}
XMLEvent::Empty(ref e) if e.name() == QName(b"failure") => {
let msg = Message::new_empty(e)?;
tc.status = TestStatus::Failure(msg);
}
XMLEvent::Start(ref e) if e.name() == QName(b"error") => {
let msg = Message::from_reader(e, r)?;
tc.status = TestStatus::Error(msg);
}
XMLEvent::Empty(ref e) if e.name() == QName(b"error") => {
let msg = Message::new_empty(e)?;
tc.status = TestStatus::Error(msg);
}
XMLEvent::Start(ref e) if e.name() == QName(b"system-out") => {
tc.system_out = parse_system(e, r)?;
}
XMLEvent::Start(ref e) if e.name() == QName(b"system-err") => {
tc.system_err = parse_system(e, r)?;
}
XMLEvent::Empty(ref e) if e.name() == QName(b"rerunFailure") => {
tc.retries.push(Retry::new_empty(e)?);
}
XMLEvent::Start(ref e) if e.name() == QName(b"rerunFailure") => {
tc.retries.push(Retry::from_reader(e, r)?);
}
XMLEvent::Empty(ref e) if e.name() == QName(b"flakyFailure") => {
tc.status = TestStatus::Flaky;
tc.retries.push(Retry::new_empty(e)?);
}
XMLEvent::Start(ref e) if e.name() == QName(b"flakyFailure") => {
tc.status = TestStatus::Flaky;
tc.retries.push(Retry::from_reader(e, r)?);
}
XMLEvent::Eof => return Err(XMLError::UnexpectedEof("testcase".to_string()).into()),
_ => (),
}
}
Ok(tc)
}
}
impl Message {
/// Fill up `self` with attributes from the XML tag
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
if let QName(b"message") = a.key {
self.message = try_from_attribute_value_string(a.value)?
}
}
Ok(())
}
/// New [`Message`] from empty XML tag
fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
let mut tf = Self::default();
tf.parse_attributes(e)?;
Ok(tf)
}
/// New [`Message`] from XML tree
fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
let name = e.name();
let mut msg = Self::default();
msg.parse_attributes(e)?;
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == name => break,
Ok(XMLEvent::Text(e)) => {
msg.text += e.unescape()?.trim();
}
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof("failure".to_string()).into())
}
Err(err) => return Err(err.into()),
_ => (),
}
}
Ok(msg)
}
}
impl Retry {
/// Fill up `self` with attributes from the XML tag
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
QName(b"timestamp") => {
self.timestamp = Some(try_from_attribute_value_timestamp(a.value)?)
}
_ => {}
};
}
Ok(())
}
fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
let mut rt = Self::default();
rt.parse_attributes(e)?;
Ok(rt)
}
fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
let name = e.name();
let mut rt = Self::default();
rt.parse_attributes(e)?;
let mut msg = Message::new_empty(e)?;
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == name => break,
Ok(XMLEvent::Text(e)) => {
msg.text += e.unescape()?.trim();
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"system-out") => {
rt.system_out = parse_system(e, r)?;
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"system-err") => {
rt.system_err = parse_system(e, r)?;
}
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof("failure".to_string()).into())
}
Err(err) => return Err(err.into()),
_ => (),
}
}
rt.status = TestStatus::Failure(msg);
Ok(rt)
}
}
/// Try to decode attribute value as [`f64`]
fn try_from_attribute_value_f64(value: Cow<[u8]>) -> Result<f64, Error> {
match str::from_utf8(&value)? {
"" => Ok(f64::default()),
s => Ok(s.parse::<f64>()?),
}
}
/// Try to decode attribute value as [`u64`]
fn try_from_attribute_value_u64(value: Cow<[u8]>) -> Result<u64, Error> {
match str::from_utf8(&value)? {
"" => Ok(u64::default()),
s => Ok(s.parse::<u64>()?),
}
}
/// Try to decode and unescape attribute value as [`String`]
fn try_from_attribute_value_string(value: Cow<[u8]>) -> Result<String, Error> {
let s = str::from_utf8(&value)?;
let u = unescape(s)?;
Ok(u.to_string())
}
/// Try to decode and unescape attribute value as [`String`]
fn try_from_attribute_value_timestamp(value: Cow<[u8]>) -> Result<OffsetDateTime, Error> {
let s = str::from_utf8(&value)?;
let t = OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)?;
Ok(t)
}
/// Parse a chunk of xml as system-out or system-err
fn parse_system<B: BufRead>(
orig: &XMLBytesStart,
r: &mut XMLReader<B>,
) -> Result<Option<String>, Error> {
let mut buf = Vec::new();
let mut res = None;
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == orig.name() => break,
Ok(XMLEvent::Text(e)) => {
res = Some(e.unescape()?.to_string());
}
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof(format!("{:?}", orig.name())).into());
}
Err(err) => return Err(err.into()),
_ => (),
}
}
Ok(res)
}
/// Creates a [`TestSuites`](struct.TestSuites.html) structure from a JUnit XML data read from `reader`
///
/// # Example
/// ```
/// use std::io::Cursor;
/// let xml = r#"
/// <testsuite tests="3" failures="1">
/// <testcase classname="foo1" name="ASuccessfulTest"/>
/// <testcase classname="foo2" name="AnotherSuccessfulTest"/>
/// <testcase classname="foo3" name="AFailingTest">
/// <failure type="NotEnoughFoo"> details about failure </failure>
/// </testcase>
/// </testsuite>
/// "#;
/// let cursor = Cursor::new(xml);
/// let r = junit_parser::from_reader(cursor);
/// assert!(r.is_ok());
/// ```
pub fn from_reader<B: BufRead>(reader: B) -> Result<TestSuites, Error> {
let mut r = XMLReader::from_reader(reader);
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testsuites") => {
return TestSuites::new_empty(e);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testrun") => {
return TestSuites::new_empty(e);
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testsuites") => {
return TestSuites::from_reader(e, &mut r);
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testrun") => {
return TestSuites::from_reader(e, &mut r);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testsuite") => {
let ts = TestSuite::new_empty(e)?;
let mut suites = TestSuites::default();
suites.suites.push(ts);
return Ok(suites);
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testsuite") => {
let ts = TestSuite::from_reader(e, &mut r)?;
let mut suites = TestSuites::default();
suites.suites.push(ts);
return Ok(suites);
}
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof("testsuites".to_string()).into())
}
Err(err) => return Err(err.into()),
_ => (),
}
}
}
/// Creates a [`TestSuites`](struct.TestSuites.html) structure from a JUnit XML data read from a string
pub fn from_str(s: &str) -> Result<TestSuites, Error> {
from_reader(s.as_bytes())
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader, path::PathBuf};
use once_cell::sync::Lazy;
use path_macro::path;
use super::*;
pub static TESTFILES: Lazy<PathBuf> =
Lazy::new(|| path!(env!("CARGO_MANIFEST_DIR") / "testfiles"));
fn parse_test(n: &str) {
let file = File::open(path!(*TESTFILES / format!("{n}.junit.xml"))).unwrap();
let suites = from_reader(BufReader::new(file)).unwrap();
insta::assert_json_snapshot!(format!("parse_{n}"), suites);
}
#[test]
fn parse_simple() {
parse_test("simple")
}
#[test]
fn parse_vite() {
parse_test("vite")
}
#[test]
fn parse_retry() {
parse_test("retry")
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,824 @@
---
source: crates/junit-parser/src/lib.rs
expression: suites
---
{
"name": "vitest tests",
"suites": [
{
"name": "src/lib/server/query/util.test.ts",
"timestamp": "2024-06-04T11:43:17.788Z",
"cases": [
{
"name": "src/lib/server/query/util.test.ts::query builder",
"original_name": "query builder",
"classname": "src/lib/server/query/util.test.ts",
"timestamp": null,
"time": 0.002,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/server/query/util.test.ts::parse search query",
"original_name": "parse search query",
"classname": "src/lib/server/query/util.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/server/query/util.test.ts::mapSortFields",
"original_name": "mapSortFields",
"classname": "src/lib/server/query/util.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
}
],
"time": 0.006,
"tests": 3,
"errors": 0,
"failures": 0,
"skipped": 0,
"flaky": 0
},
{
"name": "src/lib/shared/model/validation.test.ts",
"timestamp": "2024-06-04T11:43:17.789Z",
"cases": [
{
"name": "src/lib/shared/model/validation.test.ts::date string",
"original_name": "date string",
"classname": "src/lib/shared/model/validation.test.ts",
"timestamp": null,
"time": 0.002,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/model/validation.test.ts::filter data",
"original_name": "filter data",
"classname": "src/lib/shared/model/validation.test.ts",
"timestamp": null,
"time": 0.002,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
}
],
"time": 0.004,
"tests": 2,
"errors": 0,
"failures": 0,
"skipped": 0,
"flaky": 0
},
{
"name": "src/lib/shared/util/colors.test.ts",
"timestamp": "2024-06-04T11:43:17.79Z",
"cases": [
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > colorToHex",
"original_name": "color conversion > colorToHex",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > hexToColor",
"original_name": "color conversion > hexToColor",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > colorToHex",
"original_name": "color conversion > colorToHex",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > hexToColor",
"original_name": "color conversion > hexToColor",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > colorToHex",
"original_name": "color conversion > colorToHex",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > hexToColor",
"original_name": "color conversion > hexToColor",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > colorToHex",
"original_name": "color conversion > colorToHex",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > hexToColor",
"original_name": "color conversion > hexToColor",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > colorToHex",
"original_name": "color conversion > colorToHex",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > hexToColor",
"original_name": "color conversion > hexToColor",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > colorToHex",
"original_name": "color conversion > colorToHex",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > hexToColor",
"original_name": "color conversion > hexToColor",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > colorToHex",
"original_name": "color conversion > colorToHex",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > hexToColor",
"original_name": "color conversion > hexToColor",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > colorToHex",
"original_name": "color conversion > colorToHex",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > hexToColor",
"original_name": "color conversion > hexToColor",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > colorToHex",
"original_name": "color conversion > colorToHex",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > hexToColor",
"original_name": "color conversion > hexToColor",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > colorToHex",
"original_name": "color conversion > colorToHex",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/colors.test.ts::color conversion > hexToColor",
"original_name": "color conversion > hexToColor",
"classname": "src/lib/shared/util/colors.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
}
],
"time": 0.005,
"tests": 20,
"errors": 0,
"failures": 0,
"skipped": 0,
"flaky": 0
},
{
"name": "src/lib/shared/util/date.test.ts",
"timestamp": "2024-06-04T11:43:17.792Z",
"cases": [
{
"name": "src/lib/shared/util/date.test.ts::formatDate",
"original_name": "formatDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.014,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::dateFromYMD",
"original_name": "dateFromYMD",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::dateFromYMD",
"original_name": "dateFromYMD",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::utcDateToYMD",
"original_name": "utcDateToYMD",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::dateToYMD",
"original_name": "dateToYMD",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.002,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::humanDate",
"original_name": "humanDate",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::parse daterange ''",
"original_name": "parse daterange ''",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::parse daterange '..'",
"original_name": "parse daterange '..'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::parse daterange 'foo..bar'",
"original_name": "parse daterange 'foo..bar'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::parse daterange '2024-04-15'",
"original_name": "parse daterange '2024-04-15'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::parse daterange '2024-04-13..2024-04-20'",
"original_name": "parse daterange '2024-04-13..2024-04-20'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::parse daterange '2024-04-13..'",
"original_name": "parse daterange '2024-04-13..'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::parse daterange '..2024-04-20'",
"original_name": "parse daterange '..2024-04-20'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '..2024-04-14'",
"original_name": "shiftDateRange '..2024-04-14'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '..2024-04-14'",
"original_name": "shiftDateRange '..2024-04-14'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '2024-04-08..'",
"original_name": "shiftDateRange '2024-04-08..'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '2024-04-08..'",
"original_name": "shiftDateRange '2024-04-08..'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '2024-04-08..2024-04-14'",
"original_name": "shiftDateRange '2024-04-08..2024-04-14'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '2024-04-08..2024-04-14'",
"original_name": "shiftDateRange '2024-04-08..2024-04-14'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '2024-04-13..2024-04-16'",
"original_name": "shiftDateRange '2024-04-13..2024-04-16'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '2024-04-13..2024-04-16'",
"original_name": "shiftDateRange '2024-04-13..2024-04-16'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '2024-04-13..2024-04-13'",
"original_name": "shiftDateRange '2024-04-13..2024-04-13'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '2024-04-13..2024-04-13'",
"original_name": "shiftDateRange '2024-04-13..2024-04-13'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '2024-04-08..2024-04-14'",
"original_name": "shiftDateRange '2024-04-08..2024-04-14'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '2024-04-08..2024-04-14'",
"original_name": "shiftDateRange '2024-04-08..2024-04-14'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::shiftDateRange '..2024-04-14'",
"original_name": "shiftDateRange '..2024-04-14'",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.001,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/date.test.ts::dateFromHuman",
"original_name": "dateFromHuman",
"classname": "src/lib/shared/util/date.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
}
],
"time": 0.027,
"tests": 39,
"errors": 0,
"failures": 0,
"skipped": 0,
"flaky": 0
},
{
"name": "src/lib/shared/util/diff.test.ts",
"timestamp": "2024-06-04T11:43:17.795Z",
"cases": [
{
"name": "src/lib/shared/util/diff.test.ts::versions diff",
"original_name": "versions diff",
"classname": "src/lib/shared/util/diff.test.ts",
"timestamp": null,
"time": 0.003,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
}
],
"time": 0.003,
"tests": 1,
"errors": 0,
"failures": 0,
"skipped": 0,
"flaky": 0
},
{
"name": "src/lib/shared/util/util.test.ts",
"timestamp": "2024-06-04T11:43:17.795Z",
"cases": [
{
"name": "src/lib/shared/util/util.test.ts::getQueryUrl",
"original_name": "getQueryUrl",
"classname": "src/lib/shared/util/util.test.ts",
"timestamp": null,
"time": 0.005,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
},
{
"name": "src/lib/shared/util/util.test.ts::normalizeLineEndings",
"original_name": "normalizeLineEndings",
"classname": "src/lib/shared/util/util.test.ts",
"timestamp": null,
"time": 0.0,
"status": "Success",
"system_out": null,
"system_err": null,
"retries": []
}
],
"time": 0.005,
"tests": 2,
"errors": 0,
"failures": 0,
"skipped": 0,
"flaky": 0
}
],
"time": 1.371,
"tests": 67,
"errors": 0,
"failures": 0,
"skipped": 0,
"flaky": 0
}

View file

@ -2,6 +2,7 @@
.viewer > pre { .viewer > pre {
padding: 10px 20px; padding: 10px 20px;
font-size: 14px;
} }
pre, code { pre, code {
@ -9,7 +10,7 @@ pre, code {
background-color: #1c1c1c; background-color: #1c1c1c;
} }
.markup { .prose {
margin: 20px 20px 0 20px; margin: 20px 20px 0 20px;
max-width: 800px; max-width: 800px;
word-wrap: break-word; word-wrap: break-word;
@ -17,169 +18,170 @@ pre, code {
font-size: 16px; font-size: 16px;
line-height: 1.5 !important; line-height: 1.5 !important;
} }
.markup > :first-child { .prose > :first-child {
margin-top: 0 !important; margin-top: 0 !important;
} }
.markup > :last-child { .prose > :last-child {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.markup h1, .prose h1,
.markup h2, .prose h2,
.markup h3, .prose h3,
.markup h4, .prose h4,
.markup h5, .prose h5,
.markup h6 { .prose h6 {
font-weight: 600; font-weight: 600;
margin-top: 24px; margin-top: 24px;
margin-bottom: 16px; margin-bottom: 16px;
line-height: 1.25; line-height: 1.25;
} }
.markup h1 tt, .prose h1 tt,
.markup h1 code, .prose h1 code,
.markup h2 tt, .prose h2 tt,
.markup h2 code, .prose h2 code,
.markup h3 tt, .prose h3 tt,
.markup h3 code, .prose h3 code,
.markup h4 tt, .prose h4 tt,
.markup h4 code, .prose h4 code,
.markup h5 tt, .prose h5 tt,
.markup h5 code, .prose h5 code,
.markup h6 tt, .prose h6 tt,
.markup h6 code { .prose h6 code {
font-size: inherit; font-size: inherit;
} }
.markup h1 { .prose h1 {
border-bottom: 1px solid var(--color-secondary); border-bottom: 1px solid var(--color-secondary);
padding-bottom: 0.3em; padding-bottom: 0.3em;
font-size: 2em; font-size: 2em;
} }
.markup h2 { .prose h2 {
border-bottom: 1px solid var(--color-secondary); border-bottom: 1px solid var(--color-secondary);
padding-bottom: 0.3em; padding-bottom: 0.3em;
font-size: 1.5em; font-size: 1.5em;
} }
.markup h3 { .prose h3 {
font-size: 1.25em; font-size: 1.25em;
} }
.markup h4 { .prose h4 {
font-size: 1em; font-size: 1em;
} }
.markup h5 { .prose h5 {
font-size: 0.875em; font-size: 0.875em;
} }
.markup h6 { .prose h6 {
color: var(--color-text-light); color: var(--color-text-light);
font-size: 0.85em; font-size: 0.85em;
} }
.markup p, .prose p,
.markup blockquote, .prose blockquote,
.markup details, .prose details,
.markup ul, .prose ul,
.markup ol, .prose ol,
.markup dl, .prose dl,
.markup table, .prose table,
.markup pre { .prose pre {
margin-top: 0; margin-top: 0;
margin-bottom: 16px; margin-bottom: 16px;
} }
.markup hr { .prose hr {
background-color: var(--color-secondary); background-color: var(--color-secondary);
border: 0; border: 0;
height: 4px; height: 4px;
margin: 16px 0; margin: 16px 0;
padding: 0; padding: 0;
} }
.markup ul, .prose ul,
.markup ol { .prose ol {
padding-left: 2em; padding-left: 2em;
} }
.markup ul ul, .prose ul ul,
.markup ul ol, .prose ul ol,
.markup ol ol, .prose ol ol,
.markup ol ul { .prose ol ul {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }
.markup ol ol, .prose ol ol,
.markup ul ol { .prose ul ol {
list-style-type: lower-roman; list-style-type: lower-roman;
} }
.markup li > p { .prose li > p {
margin-top: 16px; margin-top: 16px;
} }
.markup li + li { .prose li + li {
margin-top: 0.25em; margin-top: 0.25em;
} }
.markup dl { .prose dl {
padding: 0; padding: 0;
} }
.markup dl dt { .prose dl dt {
font-size: 1em; font-size: 1em;
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
margin-top: 16px; margin-top: 16px;
padding: 0; padding: 0;
} }
.markup dl dd { .prose dl dd {
margin-bottom: 16px; margin-bottom: 16px;
padding: 0 16px; padding: 0 16px;
} }
.markup blockquote { .prose blockquote {
color: var(--color-text-light); color: var(--color-text-light);
border-left: 4px solid var(--color-secondary); border-left: 4px solid var(--color-secondary);
margin-left: 0; margin-left: 0;
padding: 0 15px; padding: 0 15px;
} }
.markup blockquote > :first-child { .prose blockquote > :first-child {
margin-top: 0; margin-top: 0;
} }
.markup blockquote > :last-child { .prose blockquote > :last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.markup table { .prose table {
width: max-content; width: max-content;
max-width: 100%; max-width: 100%;
display: block; display: block;
overflow: auto; overflow: auto;
border-collapse: collapse;
} }
.markup table th { .prose table th {
font-weight: 600; font-weight: 600;
} }
.markup table th, .prose table th,
.markup table td { .prose table td {
border: 1px solid var(--color-secondary) !important; border: 1px solid var(--color-secondary) !important;
padding: 6px 13px !important; padding: 6px 13px !important;
} }
.markup table tr { .prose table tr {
border-top: 1px solid var(--color-secondary); border-top: 1px solid var(--color-secondary);
} }
.markup table tr:nth-child(2n) { .prose table tr:nth-child(2n) {
background-color: var(--color-secondary); background-color: var(--color-secondary);
} }
.markup img, .prose img,
.markup video { .prose video {
box-sizing: initial; box-sizing: initial;
max-width: 100%; max-width: 100%;
} }
.markup img[align="right"], .prose img[align="right"],
.markup video[align="right"] { .prose video[align="right"] {
padding-left: 20px; padding-left: 20px;
} }
.markup img[align="left"], .prose img[align="left"],
.markup video[align="left"] { .prose video[align="left"] {
padding-right: 28px; padding-right: 28px;
} }
.markup code { .prose code {
white-space: break-spaces; white-space: break-spaces;
border-radius: 4px; border-radius: 4px;
margin: 0; margin: 0;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
font-size: 85%; font-size: 85%;
} }
.markup code br { .prose code br {
display: none; display: none;
} }
.markup pre { .prose pre {
border-radius: 4px; border-radius: 4px;
padding: 8px; padding: 8px;
line-height: 1.45; line-height: 1.45;
@ -187,13 +189,16 @@ pre, code {
word-break: normal; word-break: normal;
word-wrap: normal; word-wrap: normal;
} }
.markup pre code:before, .prose pre code {
.markup pre code:after { padding: 0;
}
.prose pre code:before,
.prose pre code:after {
content: normal; content: normal;
} }
.markup .ui.list .list, .prose .ui.list .list,
.markup ol.ui.list ol, .prose ol.ui.list ol,
.markup ul.ui.list ul { .prose ul.ui.list ul {
padding-left: 2em; padding-left: 2em;
} }

View file

@ -72,7 +72,7 @@ pub(crate) const STYLE_CONTENT_PATH: &str = "/content1.css";
const FAVICON_BYTES: &[u8; 268] = include_bytes!("../resources/favicon.ico"); const FAVICON_BYTES: &[u8; 268] = include_bytes!("../resources/favicon.ico");
const STYLE_MAIN_BYTES: &[u8; 4057] = include_bytes!("../resources/style.css"); const STYLE_MAIN_BYTES: &[u8; 4057] = include_bytes!("../resources/style.css");
const STYLE_CONTENT_BYTES: &[u8; 10063] = include_bytes!("../resources/content.css"); const STYLE_CONTENT_BYTES: &[u8; 10079] = include_bytes!("../resources/content.css");
impl App { impl App {
pub fn new() -> Self { pub fn new() -> Self {

View file

@ -249,8 +249,12 @@ impl CacheEntry {
.entries() .entries()
.iter() .iter()
.filter_map(|entry| { .filter_map(|entry| {
let name = entry.filename().as_str().ok()?;
if name.ends_with('/') {
return None;
}
Some(( Some((
entry.filename().as_str().ok()?.to_owned(), name.to_owned(),
FileEntry { FileEntry {
header_offset: entry.header_offset().try_into().ok()?, header_offset: entry.header_offset().try_into().ok()?,
uncompressed_size: entry.uncompressed_size().try_into().ok()?, uncompressed_size: entry.uncompressed_size().try_into().ok()?,
@ -269,7 +273,7 @@ impl CacheEntry {
} }
pub fn get_file(&self, path: &str, url_query: &str) -> Result<GetFileResult> { pub fn get_file(&self, path: &str, url_query: &str) -> Result<GetFileResult> {
let path = path.trim_start_matches('/'); let path = path.trim_matches('/');
let mut index_path: Option<Cow<str>> = None; let mut index_path: Option<Cow<str>> = None;
if path.is_empty() { if path.is_empty() {
@ -304,7 +308,7 @@ impl CacheEntry {
} }
// Directory listing // Directory listing
let path_as_dir: Cow<str> = if path.is_empty() || path.ends_with('/') { let path_as_dir: Cow<str> = if path.is_empty() {
path.into() path.into()
} else { } else {
format!("{path}/").into() format!("{path}/").into()

View file

@ -90,7 +90,7 @@ impl Default for ConfigData {
repo_blacklist: QueryFilterList::default(), repo_blacklist: QueryFilterList::default(),
repo_whitelist: QueryFilterList::default(), repo_whitelist: QueryFilterList::default(),
site_aliases: HashMap::new(), site_aliases: HashMap::new(),
viewer_max_size: Some(NonZeroU32::new(100_000).unwrap()), viewer_max_size: Some(NonZeroU32::new(500_000).unwrap()),
} }
} }
} }

View file

@ -269,10 +269,17 @@ impl IntoResponse for ErrorJson {
} }
#[cfg(test)] #[cfg(test)]
mod tests { pub(crate) mod tests {
use std::path::PathBuf;
use http::{header, HeaderMap}; use http::{header, HeaderMap};
use once_cell::sync::Lazy;
use path_macro::path;
use rstest::rstest; use rstest::rstest;
pub static TESTFILES: Lazy<PathBuf> =
Lazy::new(|| path!(env!("CARGO_MANIFEST_DIR") / "tests" / "testfiles"));
#[rstest] #[rstest]
#[case("", false)] #[case("", false)]
#[case("br", false)] #[case("br", false)]

46
src/viewer/junit.rs Normal file
View file

@ -0,0 +1,46 @@
use crate::error::Error;
use super::Viewer;
/// JUnit format documentation: https://llg.cubic.org/docs/junit/
pub struct JunitViewer;
impl Viewer for JunitViewer {
fn id(&self) -> &'static str {
"junit"
}
fn name(&self) -> &'static str {
"JUnit"
}
fn is_applicable(&self, filename: &str, ext: &str) -> bool {
ext == "xml" && filename.contains("junit")
}
fn try_render(&self, filename: &str, _ext: &str, data: &str) -> Result<String, Error> {
let suites = junit_parser::from_str(data).map_err(|e| {
tracing::error!("could not parse junit report {filename}: {e}");
Error::ViewerNotApplicable
})?;
dbg!(&suites);
Ok(String::new())
}
}
#[cfg(test)]
mod tests {
use path_macro::path;
use crate::{util::tests::TESTFILES, viewer::Viewer};
use super::JunitViewer;
#[test]
fn t1() {
let data =
std::fs::read_to_string(path!(*TESTFILES / "junit" / "simple.junit.xml")).unwrap();
let html = JunitViewer.try_render("", "", &data).unwrap();
println!("{html}");
}
}

View file

@ -37,13 +37,20 @@ impl Viewer for MarkdownViewer {
} }
fn try_render(&self, _filename: &str, _ext: &str, data: &str) -> Result<String, Error> { fn try_render(&self, _filename: &str, _ext: &str, data: &str) -> Result<String, Error> {
let options = comrak::Options::default(); let mut options = comrak::Options::default();
options.extension.autolink = true;
options.extension.table = true;
options.extension.tasklist = true;
options.extension.strikethrough = true;
options.extension.multiline_block_quotes = true;
options.extension.superscript = true;
let mut plugins = comrak::Plugins::default(); let mut plugins = comrak::Plugins::default();
plugins.render.codefence_syntax_highlighter = Some(&self.adapter); plugins.render.codefence_syntax_highlighter = Some(&self.adapter);
let html = comrak::markdown_to_html_with_plugins(data, &options, &plugins); let html = comrak::markdown_to_html_with_plugins(data, &options, &plugins);
Ok(format!("<div class=\"markup\">{html}</div>")) Ok(format!("<div class=\"prose\">{html}</div>"))
} }
} }

View file

@ -5,6 +5,7 @@ use syntect::parsing::SyntaxSet;
use crate::{error::Error, templates::ViewerLink}; use crate::{error::Error, templates::ViewerLink};
mod code; mod code;
mod junit;
mod markdown; mod markdown;
pub trait Viewer: Sync + Send { pub trait Viewer: Sync + Send {
@ -16,7 +17,7 @@ pub trait Viewer: Sync + Send {
} }
pub struct Viewers { pub struct Viewers {
viewers: [Box<dyn Viewer>; 2], viewers: [Box<dyn Viewer>; 3],
} }
pub struct RenderRes { pub struct RenderRes {
@ -29,6 +30,7 @@ impl Viewers {
let ss = Arc::new(SyntaxSet::load_defaults_newlines()); let ss = Arc::new(SyntaxSet::load_defaults_newlines());
Self { Self {
viewers: [ viewers: [
Box::new(junit::JunitViewer),
Box::new(markdown::MarkdownViewer::new(ss.clone())), Box::new(markdown::MarkdownViewer::new(ss.clone())),
Box::new(code::CodeViewer::new(ss)), Box::new(code::CodeViewer::new(ss)),
], ],

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff