Compare commits
6 commits
ec65b6d03f
...
445bc43c96
Author | SHA1 | Date | |
---|---|---|---|
445bc43c96 | |||
194499b276 | |||
134bdaa34b | |||
4aaff462bb | |||
d9f9d6edca | |||
806a2dda9a |
41 changed files with 2237 additions and 153 deletions
|
@ -16,7 +16,13 @@ jobs:
|
||||||
- name: 📎 Clippy
|
- name: 📎 Clippy
|
||||||
run: cargo clippy --all -- -D warnings
|
run: cargo clippy --all -- -D warnings
|
||||||
- name: 🧪 Test
|
- name: 🧪 Test
|
||||||
run: cargo test
|
run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci
|
||||||
|
- name: 💌 Upload test report
|
||||||
|
if: always()
|
||||||
|
uses: https://code.forgejo.org/actions/upload-artifact
|
||||||
|
with:
|
||||||
|
name: test
|
||||||
|
path: target/nextest/ci/junit.xml
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: cimaster-latest
|
runs-on: cimaster-latest
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
||||||
/dist
|
/dist
|
||||||
/.env
|
/.env
|
||||||
*.snap.new
|
*.snap.new
|
||||||
|
/tests/testfiles/sites_data
|
||||||
|
|
|
@ -10,3 +10,11 @@ repos:
|
||||||
- id: cargo-fmt
|
- id: cargo-fmt
|
||||||
- id: cargo-clippy
|
- id: cargo-clippy
|
||||||
args: ["--all", "--tests", "--", "-D", "warnings"]
|
args: ["--all", "--tests", "--", "-D", "warnings"]
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: compress-res
|
||||||
|
name: Compress resources
|
||||||
|
language: system
|
||||||
|
entry: zopfli
|
||||||
|
files: "^resources/.+.css$"
|
||||||
|
|
451
Cargo.lock
generated
451
Cargo.lock
generated
|
@ -146,6 +146,7 @@ dependencies = [
|
||||||
"async_zip",
|
"async_zip",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
|
"axum-test",
|
||||||
"comrak",
|
"comrak",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"envy",
|
"envy",
|
||||||
|
@ -153,9 +154,10 @@ dependencies = [
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"governor",
|
"governor",
|
||||||
"headers",
|
"headers",
|
||||||
"hex",
|
"http 1.1.0",
|
||||||
"http",
|
"httpdate",
|
||||||
"humansize",
|
"humansize",
|
||||||
|
"insta",
|
||||||
"junit-parser",
|
"junit-parser",
|
||||||
"mime",
|
"mime",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
|
@ -170,12 +172,14 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rstest",
|
"rstest",
|
||||||
|
"scraper",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-env",
|
"serde-env",
|
||||||
"serde-hex",
|
"serde-hex",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"syntect",
|
"syntect",
|
||||||
|
"temp_testdir",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
@ -238,6 +242,12 @@ version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "auto-future"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
@ -254,7 +264,7 @@ dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
@ -269,6 +279,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_path_to_error",
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
"sync_wrapper 1.0.1",
|
"sync_wrapper 1.0.1",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
|
@ -286,7 +297,7 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"mime",
|
"mime",
|
||||||
|
@ -309,7 +320,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"mime",
|
"mime",
|
||||||
|
@ -321,6 +332,34 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-test"
|
||||||
|
version = "15.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6eaf3651cc3b15185c6033db9cc40676b02ffaa69278679c5f018de2bcae598"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"auto-future",
|
||||||
|
"axum",
|
||||||
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"http 1.1.0",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"mime",
|
||||||
|
"pretty_assertions",
|
||||||
|
"reserve-port",
|
||||||
|
"rust-multipart-rfc7578_2",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"smallvec 1.13.2",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.72"
|
version = "0.3.72"
|
||||||
|
@ -523,6 +562,16 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
|
dependencies = [
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
@ -573,6 +622,29 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cssparser"
|
||||||
|
version = "0.31.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be"
|
||||||
|
dependencies = [
|
||||||
|
"cssparser-macros",
|
||||||
|
"dtoa-short",
|
||||||
|
"itoa",
|
||||||
|
"phf 0.11.2",
|
||||||
|
"smallvec 1.13.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cssparser-macros"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.66",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.9"
|
version = "0.20.9"
|
||||||
|
@ -687,6 +759,12 @@ version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00"
|
checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
@ -710,6 +788,21 @@ 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 = "dtoa-short"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
|
||||||
|
dependencies = [
|
||||||
|
"dtoa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ego-tree"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encode_unicode"
|
name = "encode_unicode"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
|
@ -814,6 +907,16 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futf"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
|
||||||
|
dependencies = [
|
||||||
|
"mac",
|
||||||
|
"new_debug_unreachable",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
|
@ -922,6 +1025,15 @@ dependencies = [
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fxhash"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
|
@ -932,6 +1044,15 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getopts"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
@ -986,7 +1107,7 @@ dependencies = [
|
||||||
"fnv",
|
"fnv",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -1009,7 +1130,7 @@ dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"bytes",
|
"bytes",
|
||||||
"headers-core",
|
"headers-core",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"mime",
|
"mime",
|
||||||
"sha1",
|
"sha1",
|
||||||
|
@ -1021,7 +1142,7 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"http",
|
"http 1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1030,12 +1151,6 @@ version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hex"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hmac"
|
name = "hmac"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
@ -1045,6 +1160,31 @@ dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html5ever"
|
||||||
|
version = "0.26.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"mac",
|
||||||
|
"markup5ever",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -1063,7 +1203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
|
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1074,7 +1214,7 @@ checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
@ -1116,7 +1256,7 @@ dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
|
@ -1134,7 +1274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
|
checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"rustls",
|
"rustls",
|
||||||
|
@ -1169,7 +1309,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
@ -1357,6 +1497,26 @@ dependencies = [
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markup5ever"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"phf 0.10.1",
|
||||||
|
"phf_codegen",
|
||||||
|
"string_cache",
|
||||||
|
"string_cache_codegen",
|
||||||
|
"tendril",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
@ -1428,6 +1588,12 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "new_debug_unreachable"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "no-std-compat"
|
name = "no-std-compat"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
@ -1633,6 +1799,86 @@ version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared 0.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||||
|
dependencies = [
|
||||||
|
"phf_macros",
|
||||||
|
"phf_shared 0.11.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_codegen"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator 0.10.0",
|
||||||
|
"phf_shared 0.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared 0.10.0",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared 0.11.2",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_macros"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator 0.11.2",
|
||||||
|
"phf_shared 0.11.2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.66",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.5"
|
version = "1.1.5"
|
||||||
|
@ -1689,6 +1935,22 @@ version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "precomputed-hash"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
|
||||||
|
dependencies = [
|
||||||
|
"diff",
|
||||||
|
"yansi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.1.25"
|
version = "0.1.25"
|
||||||
|
@ -1890,7 +2152,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
@ -1927,6 +2189,16 @@ dependencies = [
|
||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reserve-port"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9838134a2bfaa8e1f40738fcc972ac799de6e0e06b5157acb95fc2b05a0ea283"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.8"
|
version = "0.17.8"
|
||||||
|
@ -1970,6 +2242,22 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-multipart-rfc7578_2"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03b748410c0afdef2ebbe3685a6a862e2ee937127cdaae623336a459451c8d57"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http 0.2.12",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"rand",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
|
@ -2100,6 +2388,22 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scraper"
|
||||||
|
version = "0.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b80b33679ff7a0ea53d37f3b39de77ea0c75b12c5805ac43ec0c33b3051af1b"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"cssparser",
|
||||||
|
"ego-tree",
|
||||||
|
"getopts",
|
||||||
|
"html5ever",
|
||||||
|
"once_cell",
|
||||||
|
"selectors",
|
||||||
|
"tendril",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
|
@ -2123,6 +2427,25 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "selectors"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.5.0",
|
||||||
|
"cssparser",
|
||||||
|
"derive_more",
|
||||||
|
"fxhash",
|
||||||
|
"log",
|
||||||
|
"new_debug_unreachable",
|
||||||
|
"phf 0.10.1",
|
||||||
|
"phf_codegen",
|
||||||
|
"precomputed-hash",
|
||||||
|
"servo_arc",
|
||||||
|
"smallvec 1.13.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
|
@ -2204,6 +2527,15 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "servo_arc"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44"
|
||||||
|
dependencies = [
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
|
@ -2250,6 +2582,12 @@ version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640"
|
checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
|
@ -2309,6 +2647,38 @@ dependencies = [
|
||||||
"lock_api",
|
"lock_api",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stable_deref_trait"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "string_cache"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
|
||||||
|
dependencies = [
|
||||||
|
"new_debug_unreachable",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
"phf_shared 0.10.0",
|
||||||
|
"precomputed-hash",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "string_cache_codegen"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator 0.10.0",
|
||||||
|
"phf_shared 0.10.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
@ -2375,6 +2745,12 @@ dependencies = [
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "temp_testdir"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "921f1e9c427802414907a48b21a6504ff6b3a15a1a3cf37e699590949ad9befc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.10.1"
|
version = "3.10.1"
|
||||||
|
@ -2387,6 +2763,17 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tendril"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
|
||||||
|
dependencies = [
|
||||||
|
"futf",
|
||||||
|
"mac",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.61"
|
version = "1.0.61"
|
||||||
|
@ -2465,9 +2852,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.37.0"
|
version = "1.38.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -2484,9 +2871,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.2.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2567,6 +2954,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2577,7 +2965,7 @@ checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http 1.1.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
@ -2604,6 +2992,7 @@ version = "0.1.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
|
@ -2744,6 +3133,12 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf-8"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -3123,6 +3518,12 @@ dependencies = [
|
||||||
"lzma-sys",
|
"lzma-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yansi"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yansi-term"
|
name = "yansi-term"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
|
@ -36,7 +36,6 @@ flate2 = "1.0.30"
|
||||||
futures-lite = "2.3.0"
|
futures-lite = "2.3.0"
|
||||||
governor = "0.6.3"
|
governor = "0.6.3"
|
||||||
headers = "0.4.0"
|
headers = "0.4.0"
|
||||||
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" }
|
junit-parser = { path = "crates/junit-parser" }
|
||||||
|
@ -79,8 +78,14 @@ yarte = "0.15.7"
|
||||||
yarte_helpers = "0.15.8"
|
yarte_helpers = "0.15.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
axum-test = "15.0.1"
|
||||||
|
flate2 = "1.0.30"
|
||||||
|
httpdate = "1.0.3"
|
||||||
|
insta = { version = "1.39.0", features = ["json"] }
|
||||||
proptest = "1.4.0"
|
proptest = "1.4.0"
|
||||||
rstest = { version = "0.20.0", default-features = false }
|
rstest = { version = "0.20.0", default-features = false }
|
||||||
|
scraper = "0.19.0"
|
||||||
|
temp_testdir = "0.2.3"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "crates/*"]
|
members = [".", "crates/*"]
|
||||||
|
|
3
Justfile
3
Justfile
|
@ -1,6 +1,9 @@
|
||||||
test:
|
test:
|
||||||
cargo test
|
cargo test
|
||||||
|
|
||||||
|
compress-res:
|
||||||
|
cd resources && zopfli *.css
|
||||||
|
|
||||||
release:
|
release:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -e
|
||||||
|
|
|
@ -317,6 +317,47 @@ impl TestCase {
|
||||||
}
|
}
|
||||||
Ok(tc)
|
Ok(tc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn status_txt(&self) -> Cow<'static, str> {
|
||||||
|
match self.status {
|
||||||
|
TestStatus::Success => "Success".into(),
|
||||||
|
TestStatus::Error(_) => "Error".into(),
|
||||||
|
TestStatus::Failure(_) => {
|
||||||
|
if self.retries.is_empty() {
|
||||||
|
"Failure".into()
|
||||||
|
} else {
|
||||||
|
format!("Failure (after {} retries)", self.retries.len()).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TestStatus::Flaky => format!(
|
||||||
|
"Flaky (passed after {} failed attempt{})",
|
||||||
|
self.retries.len(),
|
||||||
|
if self.retries.len() == 1 { "s" } else { "" }
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
TestStatus::Skipped => "Skipped".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestStatus {
|
||||||
|
pub fn id(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
TestStatus::Success => "success",
|
||||||
|
TestStatus::Error(_) => "error",
|
||||||
|
TestStatus::Failure(_) => "failure",
|
||||||
|
TestStatus::Flaky => "flaky",
|
||||||
|
TestStatus::Skipped => "skipped",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message(&self) -> Option<&Message> {
|
||||||
|
match self {
|
||||||
|
TestStatus::Error(msg) => Some(msg),
|
||||||
|
TestStatus::Failure(msg) => Some(msg),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
.viewer > pre {
|
.viewer > pre {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre, code {
|
pre,
|
||||||
|
code {
|
||||||
color: #cccccc;
|
color: #cccccc;
|
||||||
background-color: #1c1c1c;
|
background-color: #1c1c1c;
|
||||||
}
|
}
|
||||||
|
@ -50,12 +52,12 @@ pre, code {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
.prose h1 {
|
.prose h1 {
|
||||||
border-bottom: 1px solid var(--color-secondary);
|
border-bottom: 1px solid var(--color-border2);
|
||||||
padding-bottom: 0.3em;
|
padding-bottom: 0.3em;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
.prose h2 {
|
.prose h2 {
|
||||||
border-bottom: 1px solid var(--color-secondary);
|
border-bottom: 1px solid var(--color-border2);
|
||||||
padding-bottom: 0.3em;
|
padding-bottom: 0.3em;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
@ -456,3 +458,242 @@ pre, code {
|
||||||
.markup.untracked.git_gutter {
|
.markup.untracked.git_gutter {
|
||||||
color: #696d70;
|
color: #696d70;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.junit {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.junit {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
#preview-margin {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.junit > div:not(:last-child) {
|
||||||
|
border-right: solid 1px var(--color-border);
|
||||||
|
}
|
||||||
|
#junit-suites,
|
||||||
|
#junit-cases {
|
||||||
|
min-width: 300px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
#junit-preview {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#junit-preview h2 {
|
||||||
|
border-bottom: 2px solid var(--color-status);
|
||||||
|
}
|
||||||
|
|
||||||
|
#junit-preview h2 i {
|
||||||
|
color: var(--color-status);
|
||||||
|
}
|
||||||
|
|
||||||
|
.junit ul {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.junit ul > li,
|
||||||
|
.colsubtitle {
|
||||||
|
border-bottom: 1px dashed var(--color-border2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colsubtitle > button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 2px solid var(--color-status, var(--color-btn));
|
||||||
|
}
|
||||||
|
.colsubtitle button.active {
|
||||||
|
background-color: var(--color-status, var(--color-btn));
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coltitle {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colsubtitle {
|
||||||
|
padding: 0 8px 8px 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.junit li > button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px 8px;
|
||||||
|
text-align: left;
|
||||||
|
background-color: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.junit li button.active {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--color-a-hov);
|
||||||
|
}
|
||||||
|
|
||||||
|
#junit-cases.filtered li > button > span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badges > *:not(:last-child):after {
|
||||||
|
content: "•";
|
||||||
|
margin: 0 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pvcontent {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.junit li[data-status="success"] {
|
||||||
|
background-color: #00800035;
|
||||||
|
}
|
||||||
|
.junit li[data-status="failure"] {
|
||||||
|
background-color: #a6000035;
|
||||||
|
}
|
||||||
|
.junit li[data-status="error"] {
|
||||||
|
background-color: #67000035;
|
||||||
|
}
|
||||||
|
.junit li[data-status="skipped"] {
|
||||||
|
background-color: #33333335;
|
||||||
|
}
|
||||||
|
.junit li[data-status="flaky"] {
|
||||||
|
background-color: #d3641a35;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-status="success"] {
|
||||||
|
--color-status: #008000;
|
||||||
|
}
|
||||||
|
[data-status="failure"] {
|
||||||
|
--color-status: #a60000;
|
||||||
|
}
|
||||||
|
[data-status="error"] {
|
||||||
|
--color-status: #670000;
|
||||||
|
}
|
||||||
|
[data-status="flaky"] {
|
||||||
|
--color-status: #d3641a;
|
||||||
|
}
|
||||||
|
[data-status="skipped"] {
|
||||||
|
--color-status: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icons from https://css.gg */
|
||||||
|
.gg-check-o {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
transform: scale(var(--ggs, 1));
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
||||||
|
.gg-check-o::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
left: 3px;
|
||||||
|
top: -1px;
|
||||||
|
width: 6px;
|
||||||
|
height: 10px;
|
||||||
|
border-color: currentColor;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
border-style: solid;
|
||||||
|
transform-origin: bottom left;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-close-o {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
transform: scale(var(--ggs, 1));
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-close-o::after,
|
||||||
|
.gg-close-o::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
border-radius: 5px;
|
||||||
|
top: 8px;
|
||||||
|
left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-close-o::after {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-block {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
transform: scale(var(--ggs, 1));
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-block::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
border-radius: 5px;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
top: 5px;
|
||||||
|
left: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-danger {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
transform: scale(var(--ggs, 1));
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-danger::after,
|
||||||
|
.gg-danger::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-danger::after {
|
||||||
|
top: 2px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-danger::before {
|
||||||
|
height: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
}
|
||||||
|
|
BIN
resources/content.css.gz
Normal file
BIN
resources/content.css.gz
Normal file
Binary file not shown.
|
@ -6,6 +6,10 @@
|
||||||
--color-text: #000;
|
--color-text: #000;
|
||||||
--color-text-light: #888;
|
--color-text-light: #888;
|
||||||
--color-border: #ccc;
|
--color-border: #ccc;
|
||||||
|
--color-border2: #bbb;
|
||||||
|
--color-btn: #006ed3;
|
||||||
|
--color-a: #006ed3;
|
||||||
|
--color-a-hov: #319cff;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
@ -14,11 +18,11 @@ body {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: #006ed3;
|
color: var(--color-a);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
a:hover, a.selected {
|
a:hover, a.selected {
|
||||||
color: #319cff;
|
color: var(--color-a-hov);
|
||||||
}
|
}
|
||||||
header, #summary, .content {
|
header, #summary, .content {
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
@ -41,16 +45,15 @@ header h1 {
|
||||||
}
|
}
|
||||||
header h1 a {
|
header h1 a {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
margin: 0 4px;
|
}
|
||||||
|
header h1 .sep {
|
||||||
|
margin: 0 0.2em;
|
||||||
}
|
}
|
||||||
footer a:hover,
|
footer a:hover,
|
||||||
header h1 a:hover,
|
header h1 a:hover,
|
||||||
a.selected {
|
a.selected {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
header h1 a:first-child {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
main {
|
main {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -75,7 +78,7 @@ main {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
#list tr {
|
#list tr {
|
||||||
border-bottom: 1px dashed #dadada;
|
border-bottom: 1px dashed var(--color-border2);
|
||||||
}
|
}
|
||||||
#list tbody tr:hover {
|
#list tbody tr:hover {
|
||||||
background-color: #ffffec;
|
background-color: #ffffec;
|
||||||
|
@ -129,19 +132,25 @@ main {
|
||||||
.query-input {
|
.query-input {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
height: 32px;
|
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
background-color: #006ed3;
|
|
||||||
color: #fff;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: unset;
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
background-color: var(--color-btn);
|
||||||
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
opacity: 0.7;
|
filter: brightness(80%);
|
||||||
|
}
|
||||||
|
button:active {
|
||||||
|
filter: brightness(70%);
|
||||||
}
|
}
|
||||||
footer {
|
footer {
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
|
@ -179,9 +188,6 @@ p {
|
||||||
th:nth-child(2) {
|
th:nth-child(2) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
h1 a {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#filter {
|
#filter {
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
|
@ -189,11 +195,17 @@ p {
|
||||||
.expired {
|
.expired {
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
* {
|
* {
|
||||||
--color-secondary: #082437;
|
--color-secondary: #082437;
|
||||||
--color-text: #dddddd;
|
--color-text: #ddd;
|
||||||
--color-border: #212121;
|
--color-border: #212121;
|
||||||
|
--color-border2: #333;
|
||||||
|
--color-a: #009dff;
|
||||||
|
--color-a-hov: #62b2fd;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
background-color: #101010;
|
background-color: #101010;
|
||||||
|
@ -207,17 +219,6 @@ p {
|
||||||
#list tbody tr:hover {
|
#list tbody tr:hover {
|
||||||
background-color: #252525;
|
background-color: #252525;
|
||||||
}
|
}
|
||||||
a {
|
|
||||||
color: #5796d1;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a:hover,
|
|
||||||
h1 a:hover, a.selected {
|
|
||||||
color: #62b2fd;
|
|
||||||
}
|
|
||||||
#list tr {
|
|
||||||
border-bottom: 1px dashed rgba(255, 255, 255, 0.12);
|
|
||||||
}
|
|
||||||
#filter {
|
#filter {
|
||||||
background-color: #151515;
|
background-color: #151515;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|
BIN
resources/style.css.gz
Normal file
BIN
resources/style.css.gz
Normal file
Binary file not shown.
112
src/app.rs
112
src/app.rs
|
@ -36,11 +36,12 @@ use crate::{
|
||||||
templates::{self, ArtifactItem, LinkItem},
|
templates::{self, ArtifactItem, LinkItem},
|
||||||
util::{self, ErrorJson, ResponseBuilderExt},
|
util::{self, ErrorJson, ResponseBuilderExt},
|
||||||
viewer::Viewers,
|
viewer::Viewers,
|
||||||
App,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub struct App;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
pub struct AppState {
|
||||||
i: Arc<AppInner>,
|
i: Arc<AppInner>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,12 +68,17 @@ pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
// Stylesheets are saved with immutable cache header. If they are changed in the future,
|
// Stylesheets are saved with immutable cache header. If they are changed in the future,
|
||||||
// the number in the path should be incremented
|
// the number in the path should be incremented
|
||||||
pub(crate) const STYLE_MAIN_PATH: &str = "/style1.css";
|
pub(crate) const STYLE_MAIN_PATH: &str = "/style2.css";
|
||||||
pub(crate) const STYLE_CONTENT_PATH: &str = "/content1.css";
|
pub(crate) const STYLE_CONTENT_PATH: &str = "/content2.css";
|
||||||
|
|
||||||
const FAVICON_BYTES: &[u8; 268] = include_bytes!("../resources/favicon.ico");
|
const FAVICON_BYTES: &[u8] = include_bytes!("../resources/favicon.ico");
|
||||||
const STYLE_MAIN_BYTES: &[u8; 4057] = include_bytes!("../resources/style.css");
|
const STYLE_MAIN_BYTES: &[u8] = include_bytes!("../resources/style.css");
|
||||||
const STYLE_CONTENT_BYTES: &[u8; 10079] = include_bytes!("../resources/content.css");
|
const STYLE_CONTENT_BYTES: &[u8] = include_bytes!("../resources/content.css");
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
const STYLE_MAIN_BYTES_GZ: &[u8] = include_bytes!("../resources/style.css.gz");
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
const STYLE_CONTENT_BYTES_GZ: &[u8] = include_bytes!("../resources/content.css.gz");
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
@ -94,13 +100,26 @@ impl App {
|
||||||
.await?;
|
.await?;
|
||||||
tracing::info!("Listening on port {port}");
|
tracing::info!("Listening on port {port}");
|
||||||
|
|
||||||
let real_ip_header = state.i.cfg.load().real_ip_header.clone();
|
let router = Self::router(state);
|
||||||
let router = Router::new()
|
|
||||||
// Prevent search indexing since artifactview serves temporary artifacts
|
axum::serve(
|
||||||
.route(
|
listener,
|
||||||
"/robots.txt",
|
router.into_make_service_with_connect_info::<SocketAddr>(),
|
||||||
get(|| async { "# PLEASE dont scrape this website.\n# All of the data here is fetched from the public GitHub/Gitea APIs, this app is open source and it is not running on some Fortune 500 company server. \n\nUser-agent: *\nDisallow: /\n" }),
|
|
||||||
)
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn router(state: AppState) -> Router {
|
||||||
|
let real_ip_header = state.i.cfg.load().real_ip_header.clone();
|
||||||
|
Router::new()
|
||||||
|
// Prevent search indexing since artifactview serves temporary artifacts
|
||||||
|
.route("/robots.txt", get(|| async {
|
||||||
|
Response::builder()
|
||||||
|
.typed_header(headers::ContentType::text_utf8())
|
||||||
|
.cache()
|
||||||
|
.body::<Body>("# PLEASE dont scrape this website.\n# All of the data here is fetched from the public GitHub/Gitea APIs, this app is open source and it is not running on some Fortune 500 company server. \n\nUser-agent: *\nDisallow: /\n".into()).unwrap()
|
||||||
|
}))
|
||||||
// Put the API in the .well-known folder, since it is disabled for pages
|
// Put the API in the .well-known folder, since it is disabled for pages
|
||||||
.route("/.well-known/api/artifacts", get(Self::get_artifacts))
|
.route("/.well-known/api/artifacts", get(Self::get_artifacts))
|
||||||
.route("/.well-known/api/artifact", get(Self::get_artifact))
|
.route("/.well-known/api/artifact", get(Self::get_artifact))
|
||||||
|
@ -116,18 +135,21 @@ impl App {
|
||||||
.layer(
|
.layer(
|
||||||
TraceLayer::new_for_http()
|
TraceLayer::new_for_http()
|
||||||
.make_span_with(move |request: &Request<Body>| {
|
.make_span_with(move |request: &Request<Body>| {
|
||||||
let ip = util::get_ip_address(request, real_ip_header.as_deref()).map(|ip| ip.to_string()).unwrap_or_default();
|
let ip = util::get_ip_address(request, real_ip_header.as_deref())
|
||||||
tracing::error_span!("request", url = util::full_url_from_request(request), ip)
|
.map(|ip| ip.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
tracing::error_span!(
|
||||||
|
"request",
|
||||||
|
url = util::full_url_from_request(request),
|
||||||
|
ip
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.on_response(DefaultOnResponse::new().level(tracing::Level::INFO)),
|
.on_response(DefaultOnResponse::new().level(tracing::Level::INFO)),
|
||||||
)
|
)
|
||||||
.layer(SetResponseHeaderLayer::appending(http::header::X_CONTENT_TYPE_OPTIONS, http::HeaderValue::from_static("nosniff")));
|
.layer(SetResponseHeaderLayer::appending(
|
||||||
axum::serve(
|
http::header::X_CONTENT_TYPE_OPTIONS,
|
||||||
listener,
|
http::HeaderValue::from_static("nosniff"),
|
||||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
))
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_page(
|
async fn get_page(
|
||||||
|
@ -139,7 +161,7 @@ impl App {
|
||||||
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
||||||
|
|
||||||
if subdomain.is_empty() {
|
if subdomain.is_empty() {
|
||||||
Self::get_homepage(state, uri).await
|
Self::get_homepage(state, uri, request.headers()).await
|
||||||
} else {
|
} else {
|
||||||
let query = ArtifactQuery::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
|
let query = ArtifactQuery::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
|
||||||
state.i.cfg.check_filterlist(&query)?;
|
state.i.cfg.check_filterlist(&query)?;
|
||||||
|
@ -229,15 +251,19 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_homepage(state: AppState, uri: Uri) -> Result<Response<Body>, Error> {
|
async fn get_homepage(
|
||||||
|
state: AppState,
|
||||||
|
uri: Uri,
|
||||||
|
hdrs: &HeaderMap,
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
if uri.path() == FAVICON_PATH {
|
if uri.path() == FAVICON_PATH {
|
||||||
return Self::favicon();
|
return Self::favicon();
|
||||||
}
|
}
|
||||||
if uri.path() == STYLE_MAIN_PATH {
|
if uri.path() == STYLE_MAIN_PATH {
|
||||||
return Self::stylesheet(STYLE_MAIN_BYTES.as_slice());
|
return Self::stylesheet(hdrs, STYLE_MAIN_BYTES, STYLE_MAIN_BYTES_GZ);
|
||||||
}
|
}
|
||||||
if uri.path() == STYLE_CONTENT_PATH {
|
if uri.path() == STYLE_CONTENT_PATH {
|
||||||
return Self::stylesheet(STYLE_CONTENT_BYTES.as_slice());
|
return Self::stylesheet(hdrs, STYLE_CONTENT_BYTES, STYLE_CONTENT_BYTES_GZ);
|
||||||
}
|
}
|
||||||
if uri.path() != "/" {
|
if uri.path() != "/" {
|
||||||
return Err(Error::NotFound("path".into()));
|
return Err(Error::NotFound("path".into()));
|
||||||
|
@ -354,6 +380,7 @@ impl App {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.typed_header(ContentType::html())
|
.typed_header(ContentType::html())
|
||||||
.typed_header(headers::LastModified::from(entry.last_modified))
|
.typed_header(headers::LastModified::from(entry.last_modified))
|
||||||
|
.cache()
|
||||||
.body(tmpl.to_string().into())?)
|
.body(tmpl.to_string().into())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -527,34 +554,51 @@ impl App {
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.typed_header(headers::ContentType::from_str("image/x-icon").unwrap())
|
.typed_header(headers::ContentType::from_str("image/x-icon").unwrap())
|
||||||
.cache_immutable()
|
.cache_immutable()
|
||||||
.body(FAVICON_BYTES.as_slice().into())?)
|
.body(FAVICON_BYTES.into())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stylesheet(content: &'static [u8]) -> Result<Response<Body>, Error> {
|
#[allow(unused_variables)]
|
||||||
Ok(Response::builder()
|
fn stylesheet(
|
||||||
|
hdrs: &HeaderMap,
|
||||||
|
content: &'static [u8],
|
||||||
|
content_gz: &'static [u8],
|
||||||
|
) -> Result<Response<Body>, Error> {
|
||||||
|
let resp = Response::builder()
|
||||||
.typed_header(headers::ContentType::from(mime::TEXT_CSS))
|
.typed_header(headers::ContentType::from(mime::TEXT_CSS))
|
||||||
.cache_immutable()
|
.cache_immutable();
|
||||||
.body(content.into())?)
|
|
||||||
|
// Dont serve compressed stylesheets in debug mode to allow live changes
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
if util::accepts_gzip(hdrs) {
|
||||||
|
return Ok(resp
|
||||||
|
.typed_header(headers::ContentEncoding::gzip())
|
||||||
|
.body(content_gz.into())?);
|
||||||
|
}
|
||||||
|
Ok(resp.body(content.into())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new() -> Result<Self, Error> {
|
pub fn new() -> Result<Self, Error> {
|
||||||
let cfg = Config::new()?;
|
let cfg = Config::new()?;
|
||||||
|
Ok(Self::from_cfg(cfg))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_cfg(cfg: Config) -> Self {
|
||||||
let cache = Cache::new(cfg.clone());
|
let cache = Cache::new(cfg.clone());
|
||||||
let api = ArtifactApi::new(cfg.clone());
|
let api = ArtifactApi::new(cfg.clone());
|
||||||
Ok(Self {
|
Self {
|
||||||
i: Arc::new(AppInner {
|
i: Arc::new(AppInner {
|
||||||
cfg,
|
cfg,
|
||||||
cache,
|
cache,
|
||||||
api,
|
api,
|
||||||
viewers: Viewers::new(),
|
viewers: Viewers::new(),
|
||||||
}),
|
}),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run garbage collection in the background if necessary
|
/// Run garbage collection in the background if necessary
|
||||||
pub fn garbage_collect(&self) {
|
fn garbage_collect(&self) {
|
||||||
let state = self.clone();
|
let state = self.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = state.i.cache.garbage_collect().await {
|
if let Err(e) = state.i.cache.garbage_collect().await {
|
||||||
|
|
|
@ -288,6 +288,7 @@ mod tests {
|
||||||
use super::ArtifactApi;
|
use super::ArtifactApi;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
async fn fetch_forgejo() {
|
async fn fetch_forgejo() {
|
||||||
let query = ArtifactQuery::from_subdomain(
|
let query = ArtifactQuery::from_subdomain(
|
||||||
"code-thetadev-de--hsa--visitenbuch--32-1",
|
"code-thetadev-de--hsa--visitenbuch--32-1",
|
||||||
|
@ -302,6 +303,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
async fn fetch_github() {
|
async fn fetch_github() {
|
||||||
let query = ArtifactQuery::from_subdomain(
|
let query = ArtifactQuery::from_subdomain(
|
||||||
"github-com--actions--upload-artifact--8805345396-1440556464",
|
"github-com--actions--upload-artifact--8805345396-1440556464",
|
||||||
|
|
215
src/cache.rs
215
src/cache.rs
|
@ -16,7 +16,7 @@ use mime::Mime;
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use quick_cache::sync::Cache as QuickCache;
|
use quick_cache::sync::Cache as QuickCache;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_hex::{SerHex, Strict};
|
use serde_hex::{SerHex, SerHexOpt};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
artifact_api::ArtifactApi,
|
artifact_api::ArtifactApi,
|
||||||
|
@ -54,8 +54,11 @@ pub struct FileEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GetEntryResult {
|
pub struct GetEntryResult {
|
||||||
|
/// Cached zip file metadata
|
||||||
pub entry: Arc<CacheEntry>,
|
pub entry: Arc<CacheEntry>,
|
||||||
|
/// Path to the cached zip file
|
||||||
pub zip_path: PathBuf,
|
pub zip_path: PathBuf,
|
||||||
|
/// True if the entry was just downloaded
|
||||||
pub downloaded: bool,
|
pub downloaded: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,10 +78,11 @@ pub struct GetFileResultFile {
|
||||||
pub struct IndexEntry {
|
pub struct IndexEntry {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub size: u32,
|
pub size: u32,
|
||||||
#[serde(with = "SerHex::<Strict>")]
|
#[serde(with = "SerHex::<serde_hex::Strict>")]
|
||||||
pub crc32: u32,
|
pub crc32: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
pub struct Listing {
|
pub struct Listing {
|
||||||
pub entries: Vec<ListingEntry>,
|
pub entries: Vec<ListingEntry>,
|
||||||
pub n_files: usize,
|
pub n_files: usize,
|
||||||
|
@ -86,16 +90,21 @@ pub struct Listing {
|
||||||
pub has_parent: bool,
|
pub has_parent: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
pub struct ListingEntry {
|
pub struct ListingEntry {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub size: Size,
|
pub size: Size,
|
||||||
pub crc32: String,
|
pub crc32: Crc32,
|
||||||
pub is_dir: bool,
|
pub is_dir: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
pub struct Size(pub u32);
|
pub struct Size(pub u32);
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Crc32(#[serde(with = "SerHexOpt::<serde_hex::Strict>")] pub Option<u32>);
|
||||||
|
|
||||||
impl Cache {
|
impl Cache {
|
||||||
pub fn new(cfg: Config) -> Self {
|
pub fn new(cfg: Config) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -384,7 +393,7 @@ impl CacheEntry {
|
||||||
name: n.to_owned(),
|
name: n.to_owned(),
|
||||||
url: format!("{n}{path}"),
|
url: format!("{n}{path}"),
|
||||||
size: 0.into(),
|
size: 0.into(),
|
||||||
crc32: "-".to_string(),
|
crc32: Crc32(None),
|
||||||
is_dir: true,
|
is_dir: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -392,7 +401,7 @@ impl CacheEntry {
|
||||||
name: n.to_owned(),
|
name: n.to_owned(),
|
||||||
url: format!("{n}{path}"),
|
url: format!("{n}{path}"),
|
||||||
size: entry.uncompressed_size.into(),
|
size: entry.uncompressed_size.into(),
|
||||||
crc32: hex::encode(entry.crc32.to_le_bytes()),
|
crc32: Crc32(Some(entry.crc32)),
|
||||||
is_dir: false,
|
is_dir: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -425,3 +434,199 @@ impl From<u32> for Size {
|
||||||
Self(value)
|
Self(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{net::Ipv4Addr, str::FromStr};
|
||||||
|
|
||||||
|
use rstest::{fixture, rstest};
|
||||||
|
use temp_testdir::TempDir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct TdCache {
|
||||||
|
cache: Cache,
|
||||||
|
api: ArtifactApi,
|
||||||
|
td: TempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TdCache {
|
||||||
|
async fn get_entry(&self, subdomain: &str) -> Result<GetEntryResult> {
|
||||||
|
self.cache
|
||||||
|
.get_entry(
|
||||||
|
&self.api,
|
||||||
|
&ArtifactQuery::from_subdomain(subdomain, &HashMap::new()).unwrap(),
|
||||||
|
&IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fixture]
|
||||||
|
fn cache() -> TdCache {
|
||||||
|
let td = TempDir::default();
|
||||||
|
util::tests::setup_cache_dir(&td);
|
||||||
|
let cfg = Config::from_data(crate::ConfigData {
|
||||||
|
cache_dir: td.to_path_buf(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let cache = Cache::new(cfg.clone());
|
||||||
|
let api = ArtifactApi::new(cfg);
|
||||||
|
TdCache { cache, api, td }
|
||||||
|
}
|
||||||
|
|
||||||
|
const S1: &str = "codeberg-org--thetadev--artifactview-test--1-1";
|
||||||
|
const Z1: &str = "codeberg-org--thetadev--artifactview-test--1-1.zip";
|
||||||
|
const S2: &str = "codeberg-org--thetadev--artifactview-test--1-2";
|
||||||
|
const Z2: &str = "codeberg-org--thetadev--artifactview-test--1-2.zip";
|
||||||
|
const S3: &str = "codeberg-org--thetadev--artifactview-test--1-3";
|
||||||
|
const Z3: &str = "codeberg-org--thetadev--artifactview-test--1-3.zip";
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_entry(cache: TdCache) {
|
||||||
|
let entry = cache.get_entry(S1).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(entry.entry.name, "view");
|
||||||
|
assert_eq!(entry.zip_path, path!(cache.td / Z1));
|
||||||
|
assert!(!entry.downloaded);
|
||||||
|
|
||||||
|
let files = entry.entry.get_files();
|
||||||
|
let mut filenames = files.iter().map(|f| f.name.as_str()).collect::<Vec<_>>();
|
||||||
|
filenames.sort();
|
||||||
|
assert_eq!(
|
||||||
|
filenames,
|
||||||
|
[
|
||||||
|
".well-known/test.txt",
|
||||||
|
"README.md",
|
||||||
|
"example.rs",
|
||||||
|
"junit/hello.junit.xml",
|
||||||
|
"junit/retry.junit.xml",
|
||||||
|
"junit/simple.junit.xml",
|
||||||
|
"robots.txt",
|
||||||
|
"sites/index.html",
|
||||||
|
"sites/style.css"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn garbage_collect(cache: TdCache) {
|
||||||
|
let ago = SystemTime::now() - Duration::from_secs(13 * 3600);
|
||||||
|
let file = std::fs::File::open(path!(cache.td / Z1)).unwrap();
|
||||||
|
file.set_times(FileTimes::new().set_accessed(ago)).unwrap();
|
||||||
|
let file = std::fs::File::open(path!(cache.td / Z2)).unwrap();
|
||||||
|
file.set_times(FileTimes::new().set_accessed(ago)).unwrap();
|
||||||
|
|
||||||
|
// Access artifact 1, artifact 2 should be deleted
|
||||||
|
cache.get_entry(S1).await.unwrap();
|
||||||
|
|
||||||
|
cache.cache.garbage_collect().await.unwrap();
|
||||||
|
|
||||||
|
assert!(path!(cache.td / Z1).is_file());
|
||||||
|
assert!(path!(cache.td / format!("{S1}.name")).is_file());
|
||||||
|
assert!(path!(cache.td / Z3).is_file());
|
||||||
|
assert!(path!(cache.td / format!("{S3}.name")).is_file());
|
||||||
|
assert!(!path!(cache.td / Z2).is_file());
|
||||||
|
assert!(!path!(cache.td / format!("{S2}.name")).is_file());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_file(cache: TdCache) {
|
||||||
|
let entry = cache.get_entry(S1).await.unwrap();
|
||||||
|
let res = entry.entry.get_file("example.rs", "").unwrap();
|
||||||
|
if let GetFileResult::File(file) = res {
|
||||||
|
assert_eq!(file.filename, Some("example.rs".to_string()));
|
||||||
|
assert_eq!(file.file.crc32, 0x2013120c);
|
||||||
|
assert_eq!(file.status, StatusCode::OK);
|
||||||
|
assert_eq!(file.mime, Some(Mime::from_str("text/x-rust").unwrap()));
|
||||||
|
} else {
|
||||||
|
panic!("no file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_file_spa(cache: TdCache) {
|
||||||
|
let entry = cache.get_entry(S3).await.unwrap();
|
||||||
|
let res = entry.entry.get_file("foo/bar", "").unwrap();
|
||||||
|
if let GetFileResult::File(file) = res {
|
||||||
|
assert_eq!(file.filename, None);
|
||||||
|
assert_eq!(file.file.crc32, 0xBE336584);
|
||||||
|
assert_eq!(file.status, StatusCode::OK);
|
||||||
|
assert_eq!(file.mime, Some(Mime::from_str("text/html").unwrap()));
|
||||||
|
} else {
|
||||||
|
panic!("no file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_file_404(cache: TdCache) {
|
||||||
|
let entry = cache.get_entry(S2).await.unwrap();
|
||||||
|
let res = entry.entry.get_file("foo/bar", "").unwrap();
|
||||||
|
if let GetFileResult::File(file) = res {
|
||||||
|
assert_eq!(file.filename, None);
|
||||||
|
assert_eq!(file.file.crc32, 0x69F73F18);
|
||||||
|
assert_eq!(file.status, StatusCode::NOT_FOUND);
|
||||||
|
assert_eq!(file.mime, Some(Mime::from_str("text/html").unwrap()));
|
||||||
|
} else {
|
||||||
|
panic!("no file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("", &[
|
||||||
|
".well-known/",
|
||||||
|
"junit/",
|
||||||
|
"sites/",
|
||||||
|
"README.md",
|
||||||
|
"example.rs",
|
||||||
|
"robots.txt"
|
||||||
|
])]
|
||||||
|
#[case("C=N&O=D", &[
|
||||||
|
"sites/",
|
||||||
|
"junit/",
|
||||||
|
".well-known/",
|
||||||
|
"robots.txt",
|
||||||
|
"example.rs",
|
||||||
|
"README.md",
|
||||||
|
])]
|
||||||
|
#[case("C=S&O=A", &[
|
||||||
|
".well-known/",
|
||||||
|
"junit/",
|
||||||
|
"sites/",
|
||||||
|
"robots.txt",
|
||||||
|
"example.rs",
|
||||||
|
"README.md",
|
||||||
|
])]
|
||||||
|
#[case("C=S&O=D", &[
|
||||||
|
".well-known/",
|
||||||
|
"junit/",
|
||||||
|
"sites/",
|
||||||
|
"README.md",
|
||||||
|
"example.rs",
|
||||||
|
"robots.txt"
|
||||||
|
])]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_file_listing(cache: TdCache, #[case] query: &str, #[case] expect: &[&str]) {
|
||||||
|
let entry = cache.get_entry(S1).await.unwrap();
|
||||||
|
let res = entry.entry.get_file("", query).unwrap();
|
||||||
|
if let GetFileResult::Listing(listing) = res {
|
||||||
|
let filenames = listing
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.name.as_str())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(filenames, expect);
|
||||||
|
assert_eq!(listing.n_dirs, 3);
|
||||||
|
assert_eq!(listing.n_files, 3);
|
||||||
|
assert!(!listing.has_parent);
|
||||||
|
} else {
|
||||||
|
panic!("no listing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ pub enum Error {
|
||||||
Inaccessible,
|
Inaccessible,
|
||||||
#[error("This artifact has already expired")]
|
#[error("This artifact has already expired")]
|
||||||
Expired,
|
Expired,
|
||||||
#[error("timeout")]
|
#[error("This action took too long")]
|
||||||
Timeout(#[from] tokio::time::error::Elapsed),
|
Timeout(#[from] tokio::time::error::Elapsed),
|
||||||
#[error("Method not allowed")]
|
#[error("Method not allowed")]
|
||||||
MethodNotAllowed,
|
MethodNotAllowed,
|
||||||
|
|
|
@ -9,4 +9,6 @@ mod templates;
|
||||||
mod util;
|
mod util;
|
||||||
mod viewer;
|
mod viewer;
|
||||||
|
|
||||||
pub struct App;
|
pub use app::{App, AppState};
|
||||||
|
pub use config::{Config, ConfigData};
|
||||||
|
pub use error::Error;
|
||||||
|
|
|
@ -10,6 +10,7 @@ use crate::{
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Query to select an artifact
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct ArtifactQuery {
|
pub struct ArtifactQuery {
|
||||||
/// Forge host
|
/// Forge host
|
||||||
|
@ -26,6 +27,7 @@ pub struct ArtifactQuery {
|
||||||
pub artifact: u64,
|
pub artifact: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Query to select a CI run (set of artifacts)
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct RunQuery {
|
pub struct RunQuery {
|
||||||
/// Forge host
|
/// Forge host
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
artifact_api::Artifact,
|
artifact_api::Artifact,
|
||||||
cache::{ListingEntry, Size},
|
cache::{Crc32, ListingEntry, Size},
|
||||||
config::Config,
|
config::Config,
|
||||||
query::{Query, QueryRef},
|
query::{Query, QueryRef},
|
||||||
};
|
};
|
||||||
|
use junit_parser::TestSuites;
|
||||||
use yarte::{Render, Template};
|
use yarte::{Render, Template};
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
@ -58,6 +59,12 @@ pub struct Preview<'a> {
|
||||||
pub body: &'a str,
|
pub body: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "junit")]
|
||||||
|
pub struct Junit {
|
||||||
|
pub suites: TestSuites,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ViewerLink {
|
pub struct ViewerLink {
|
||||||
pub id: &'static str,
|
pub id: &'static str,
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
|
@ -98,3 +105,44 @@ impl Render for Size {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Render for Crc32 {
|
||||||
|
fn render(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self.0 {
|
||||||
|
Some(crc) => write!(f, "{crc:08x}"),
|
||||||
|
None => f.write_str("—"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(src = "{{ rendered }}")]
|
||||||
|
struct RenderTemplate<T: Render> {
|
||||||
|
rendered: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crc32() {
|
||||||
|
let tmpl = RenderTemplate {
|
||||||
|
rendered: Crc32(Some(0xc538cf99)),
|
||||||
|
};
|
||||||
|
assert_eq!(tmpl.to_string(), "c538cf99");
|
||||||
|
|
||||||
|
let tmpl = RenderTemplate {
|
||||||
|
rendered: Crc32(None),
|
||||||
|
};
|
||||||
|
assert_eq!(tmpl.to_string(), "—");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn size() {
|
||||||
|
let tmpl = RenderTemplate {
|
||||||
|
rendered: Size(1000),
|
||||||
|
};
|
||||||
|
assert_eq!(tmpl.to_string(), "1 kB");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
47
src/util.rs
47
src/util.rs
|
@ -241,6 +241,22 @@ pub fn parse_url(input: &str) -> Result<(&str, std::str::Split<char>)> {
|
||||||
Ok((host, parts))
|
Ok((host, parts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn time_to_ms(time: f64) -> u64 {
|
||||||
|
(time * 1000.0) as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the extension from a filename for selecting a viewer
|
||||||
|
pub fn filename_ext(filename: &str) -> &str {
|
||||||
|
let mut rsplit = filename.rsplit('.');
|
||||||
|
let ext = rsplit.next().unwrap();
|
||||||
|
if filename.starts_with('.') && rsplit.next().map(str::is_empty).unwrap_or(true) {
|
||||||
|
// Dotfile without extension (e.g. .bashrc)
|
||||||
|
filename
|
||||||
|
} else {
|
||||||
|
ext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct ErrorJson {
|
pub struct ErrorJson {
|
||||||
status: u16,
|
status: u16,
|
||||||
|
@ -270,7 +286,7 @@ impl IntoResponse for ErrorJson {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use http::{header, HeaderMap};
|
use http::{header, HeaderMap};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
@ -280,6 +296,25 @@ pub(crate) mod tests {
|
||||||
pub static TESTFILES: Lazy<PathBuf> =
|
pub static TESTFILES: Lazy<PathBuf> =
|
||||||
Lazy::new(|| path!(env!("CARGO_MANIFEST_DIR") / "tests" / "testfiles"));
|
Lazy::new(|| path!(env!("CARGO_MANIFEST_DIR") / "tests" / "testfiles"));
|
||||||
|
|
||||||
|
static SITEDIR: Lazy<PathBuf> = Lazy::new(|| {
|
||||||
|
let sitedir = path!(*TESTFILES / "sites_data");
|
||||||
|
if !sitedir.is_dir() {
|
||||||
|
std::process::Command::new(path!(*TESTFILES / "sites" / "make_zip.sh"))
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
sitedir
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn setup_cache_dir(dir: &Path) {
|
||||||
|
for entry in std::fs::read_dir(SITEDIR.as_path()).unwrap() {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
if entry.file_type().unwrap().is_file() {
|
||||||
|
std::fs::copy(entry.path(), path!(dir / entry.file_name())).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case("", false)]
|
#[case("", false)]
|
||||||
#[case("br", false)]
|
#[case("br", false)]
|
||||||
|
@ -327,4 +362,14 @@ pub(crate) mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("hello.txt", "txt")]
|
||||||
|
#[case(".bashrc", ".bashrc")]
|
||||||
|
#[case("Makefile", "Makefile")]
|
||||||
|
#[case("", "")]
|
||||||
|
fn filename_ext(#[case] filename: &str, #[case] expect: &str) {
|
||||||
|
let res = super::filename_ext(filename);
|
||||||
|
assert_eq!(res, expect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::sync::Arc;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use syntect::{
|
use syntect::{
|
||||||
html::{ClassStyle, ClassedHTMLGenerator},
|
html::{ClassStyle, ClassedHTMLGenerator},
|
||||||
|
@ -12,11 +12,19 @@ use super::Viewer;
|
||||||
|
|
||||||
pub struct CodeViewer {
|
pub struct CodeViewer {
|
||||||
ss: Arc<SyntaxSet>,
|
ss: Arc<SyntaxSet>,
|
||||||
|
smap: HashMap<String, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CodeViewer {
|
impl CodeViewer {
|
||||||
pub fn new(ss: Arc<SyntaxSet>) -> Self {
|
pub fn new(ss: Arc<SyntaxSet>) -> Self {
|
||||||
Self { ss }
|
let smap = ss
|
||||||
|
.syntaxes()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(i, s)| s.file_extensions.iter().map(move |ext| (ext.to_owned(), i)))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
Self { ss, smap }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,15 +37,13 @@ impl Viewer for CodeViewer {
|
||||||
"Code"
|
"Code"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_applicable(&self, _filename: &str, _ext: &str) -> bool {
|
fn is_applicable(&self, _filename: &str, ext: &str) -> bool {
|
||||||
true
|
self.smap.contains_key(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 syntax = self
|
let i = self.smap.get(ext).ok_or(Error::ViewerNotApplicable)?;
|
||||||
.ss
|
let syntax = &self.ss.syntaxes()[*i];
|
||||||
.find_syntax_by_extension(ext)
|
|
||||||
.ok_or(Error::ViewerNotApplicable)?;
|
|
||||||
|
|
||||||
let mut html_generator =
|
let mut html_generator =
|
||||||
ClassedHTMLGenerator::new_with_class_style(syntax, &self.ss, ClassStyle::Spaced);
|
ClassedHTMLGenerator::new_with_class_style(syntax, &self.ss, ClassStyle::Spaced);
|
||||||
|
@ -54,30 +60,32 @@ impl Viewer for CodeViewer {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
// use super::*;
|
|
||||||
|
|
||||||
/*
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::{
|
|
||||||
fs::File,
|
|
||||||
io::{BufReader, BufWriter, Write},
|
|
||||||
};
|
|
||||||
use syntect::{highlighting::ThemeSet, html::css_for_theme_with_class_style};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get_stylesheet() {
|
fn is_applicable() {
|
||||||
// let ts = ThemeSet::load_defaults();
|
let ss = Arc::new(SyntaxSet::load_defaults_newlines());
|
||||||
|
let cv = CodeViewer::new(ss);
|
||||||
let mut f = BufReader::new(File::open("Monokai.tmTheme").unwrap());
|
assert!(cv.is_applicable("hello.txt", "txt"));
|
||||||
let dark_theme = ThemeSet::load_from_reader(&mut f).unwrap();
|
assert!(cv.is_applicable(".bashrc", ".bashrc"));
|
||||||
|
assert!(!cv.is_applicable("image.jpg", "jpg"));
|
||||||
// create dark color scheme css
|
}
|
||||||
// let dark_theme = &ts.themes["Solarized (dark)"];
|
|
||||||
let css_dark_file = File::create("theme-dark.css").unwrap();
|
#[test]
|
||||||
let mut css_dark_writer = BufWriter::new(&css_dark_file);
|
fn render() {
|
||||||
|
let ss = Arc::new(SyntaxSet::load_defaults_newlines());
|
||||||
let css_dark = css_for_theme_with_class_style(&dark_theme, ClassStyle::Spaced).unwrap();
|
let cv = CodeViewer::new(ss);
|
||||||
writeln!(css_dark_writer, "{}", css_dark).unwrap();
|
let res = cv
|
||||||
|
.try_render(
|
||||||
|
"hello.rs",
|
||||||
|
"rs",
|
||||||
|
r#"fn test() {
|
||||||
|
let x = "World";
|
||||||
|
println!("Hello {x}");
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
insta::assert_snapshot!(res);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::error::Error;
|
use crate::{error::Error, templates};
|
||||||
|
|
||||||
use super::Viewer;
|
use super::Viewer;
|
||||||
|
|
||||||
|
@ -23,8 +23,9 @@ impl Viewer for JunitViewer {
|
||||||
tracing::error!("could not parse junit report {filename}: {e}");
|
tracing::error!("could not parse junit report {filename}: {e}");
|
||||||
Error::ViewerNotApplicable
|
Error::ViewerNotApplicable
|
||||||
})?;
|
})?;
|
||||||
dbg!(&suites);
|
|
||||||
Ok(String::new())
|
let tmpl = templates::Junit { suites };
|
||||||
|
Ok(tmpl.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,15 +33,24 @@ impl Viewer for JunitViewer {
|
||||||
mod tests {
|
mod tests {
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
|
|
||||||
use crate::{util::tests::TESTFILES, viewer::Viewer};
|
use crate::util::tests::TESTFILES;
|
||||||
|
|
||||||
use super::JunitViewer;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t1() {
|
fn is_applicable() {
|
||||||
|
let ju = JunitViewer;
|
||||||
|
assert!(ju.is_applicable("junit.xml", "xml"));
|
||||||
|
assert!(ju.is_applicable("hello.junit.xml", "xml"));
|
||||||
|
assert!(!ju.is_applicable("hello.xml", "xml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render() {
|
||||||
|
let ju = JunitViewer;
|
||||||
let data =
|
let data =
|
||||||
std::fs::read_to_string(path!(*TESTFILES / "junit" / "simple.junit.xml")).unwrap();
|
std::fs::read_to_string(path!(*TESTFILES / "junit" / "hello.junit.xml")).unwrap();
|
||||||
let html = JunitViewer.try_render("", "", &data).unwrap();
|
let res = ju.try_render("hello.junit.xml", "xml", &data).unwrap();
|
||||||
println!("{html}");
|
insta::assert_snapshot!(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,3 +108,33 @@ impl SyntaxHighlighterAdapter for SyntectAdapter {
|
||||||
output.write_all(b"<code>")
|
output.write_all(b"<code>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_applicable() {
|
||||||
|
let ss = Arc::new(SyntaxSet::load_defaults_newlines());
|
||||||
|
let mv = MarkdownViewer::new(ss);
|
||||||
|
assert!(mv.is_applicable("hello.md", "md"));
|
||||||
|
assert!(!mv.is_applicable("hello.txt", "txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render() {
|
||||||
|
let ss = Arc::new(SyntaxSet::load_defaults_newlines());
|
||||||
|
let mv = MarkdownViewer::new(ss);
|
||||||
|
let res = mv
|
||||||
|
.try_render(
|
||||||
|
"hello.md",
|
||||||
|
"md",
|
||||||
|
r#"# Hello World
|
||||||
|
|
||||||
|
this is a small paragraph for *testing*.
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
insta::assert_snapshot!(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
|
|
||||||
use crate::{error::Error, templates::ViewerLink};
|
use crate::{error::Error, templates::ViewerLink, util};
|
||||||
|
|
||||||
mod code;
|
mod code;
|
||||||
mod junit;
|
mod junit;
|
||||||
|
@ -21,7 +21,9 @@ pub struct Viewers {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RenderRes {
|
pub struct RenderRes {
|
||||||
|
/// Body html
|
||||||
pub html: String,
|
pub html: String,
|
||||||
|
/// List of applicable viewers to be inserted into the top bar
|
||||||
pub tmpl_viewers: Vec<ViewerLink>,
|
pub tmpl_viewers: Vec<ViewerLink>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +40,7 @@ impl Viewers {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_render(&self, filename: &str, viewer: &str, data: &str) -> Result<RenderRes, Error> {
|
pub fn try_render(&self, filename: &str, viewer: &str, data: &str) -> Result<RenderRes, Error> {
|
||||||
let ext = filename.rsplit('.').next().unwrap();
|
let ext = util::filename_ext(filename);
|
||||||
|
|
||||||
if !viewer.is_empty() && viewer != "1" {
|
if !viewer.is_empty() && viewer != "1" {
|
||||||
if let Some(viewer) = self.viewers.iter().find(|v| v.id() == viewer) {
|
if let Some(viewer) = self.viewers.iter().find(|v| v.id() == viewer) {
|
||||||
|
@ -88,3 +90,29 @@ impl Viewers {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("test.txt", "", &["code"])]
|
||||||
|
#[case("hello.md", "", &["md", "code"])]
|
||||||
|
#[case("junit.xml", r#"<?xml version="1.0" encoding="UTF-8"?> <testsuites></testsuites>"#, &["junit", "code"])]
|
||||||
|
#[case("img.png", "", &[])]
|
||||||
|
fn render(#[case] filename: &str, #[case] data: &str, #[case] applicable: &[&str]) {
|
||||||
|
let viewers = Viewers::new();
|
||||||
|
let res = viewers.try_render(filename, "1", data);
|
||||||
|
|
||||||
|
if applicable.is_empty() {
|
||||||
|
assert!(matches!(res, Err(Error::ViewerNotApplicable)));
|
||||||
|
} else {
|
||||||
|
let res = res.unwrap();
|
||||||
|
assert!(res.tmpl_viewers[0].selected);
|
||||||
|
let renderers = res.tmpl_viewers.iter().map(|v| v.id).collect::<Vec<_>>();
|
||||||
|
assert_eq!(renderers, applicable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
source: src/viewer/code.rs
|
||||||
|
assertion_line: 89
|
||||||
|
expression: res
|
||||||
|
---
|
||||||
|
<pre><code><span class="source rust"><span class="meta function rust"><span class="meta function rust"><span class="storage type function rust">fn</span> </span><span class="entity name function rust">test</span></span><span class="meta function rust"><span class="meta function parameters rust"><span class="punctuation section parameters begin rust">(</span></span><span class="meta function rust"><span class="meta function parameters rust"><span class="punctuation section parameters end rust">)</span></span></span></span><span class="meta function rust"> </span><span class="meta function rust"><span class="meta block rust"><span class="punctuation section block begin rust">{</span>
|
||||||
|
<span class="storage type rust">let</span> x <span class="keyword operator rust">=</span> <span class="string quoted double rust"><span class="punctuation definition string begin rust">"</span>World<span class="punctuation definition string end rust">"</span></span><span class="punctuation terminator rust">;</span>
|
||||||
|
<span class="support macro rust">println!</span><span class="meta group rust"><span class="punctuation section group begin rust">(</span></span><span class="meta group rust"><span class="string quoted double rust"><span class="punctuation definition string begin rust">"</span>Hello <span class="constant other placeholder rust">{x}</span><span class="punctuation definition string end rust">"</span></span></span><span class="meta group rust"><span class="punctuation section group end rust">)</span></span><span class="punctuation terminator rust">;</span>
|
||||||
|
</span><span class="meta block rust"><span class="punctuation section block end rust">}</span></span></span>
|
||||||
|
</span></code></pre>
|
|
@ -0,0 +1,197 @@
|
||||||
|
---
|
||||||
|
source: src/viewer/junit.rs
|
||||||
|
assertion_line: 54
|
||||||
|
expression: res
|
||||||
|
---
|
||||||
|
<div class="junit">
|
||||||
|
<div id="junit-suites">
|
||||||
|
<p class="coltitle">Test suites:</p>
|
||||||
|
<ul>
|
||||||
|
<li><button class="active">Show all</button></li>
|
||||||
|
|
||||||
|
<li data-status="failure"><button>lib1</button></li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="junit-cases">
|
||||||
|
<p class="coltitle">Test cases:</p>
|
||||||
|
<p id="junit-statusfilter" class="colsubtitle">
|
||||||
|
<button data-status="all">All <b>?</b></button>
|
||||||
|
<button data-status="success">OK <b>?</b></button>
|
||||||
|
<button data-status="failure">Failed <b>?</b></button>
|
||||||
|
<button data-status="error">Error <b>?</b></button>
|
||||||
|
<button data-status="flaky">Flaky <b>?</b></button>
|
||||||
|
<button data-status="skipped">Skipped <b>?</b></button>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li data-suite="lib1" data-status="success">
|
||||||
|
<button><span>lib1::</span>tests::it_works</button>
|
||||||
|
<div class="pvcontent">
|
||||||
|
<h2><i class="gg-check-o"></i> lib1::tests::it_works</h2>
|
||||||
|
<p class="badges"><span>Success</span><span>3ms</span></p><pre><code>
|
||||||
|
running 1 test
|
||||||
|
test tests::it_works ... ok
|
||||||
|
|
||||||
|
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
|
||||||
|
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li data-suite="lib1" data-status="failure">
|
||||||
|
<button><span>lib1::</span>tests::pippi_langstrumpf</button>
|
||||||
|
<div class="pvcontent">
|
||||||
|
<h2><i class="gg-close-o"></i> lib1::tests::pippi_langstrumpf</h2>
|
||||||
|
<p class="badges"><span>Failure</span><span>3ms</span></p><pre><code>thread 'tests::pippi_langstrumpf' panicked at src/lib.rs:18:9:
|
||||||
|
assertion `left == right` failed
|
||||||
|
left: 7
|
||||||
|
right: 9
|
||||||
|
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</code></pre><pre><code>
|
||||||
|
running 1 test
|
||||||
|
test tests::pippi_langstrumpf ... FAILED
|
||||||
|
|
||||||
|
failures:
|
||||||
|
|
||||||
|
failures:
|
||||||
|
tests::pippi_langstrumpf
|
||||||
|
|
||||||
|
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
|
||||||
|
|
||||||
|
</code></pre><pre><code>thread 'tests::pippi_langstrumpf' panicked at src/lib.rs:18:9:
|
||||||
|
assertion `left == right` failed
|
||||||
|
left: 7
|
||||||
|
right: 9
|
||||||
|
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="junit-preview">
|
||||||
|
<div id="preview-margin"></div>
|
||||||
|
<div class="prose">
|
||||||
|
<p class="light">Select a test case to show details</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
|
||||||
|
|
||||||
|
let preview;
|
||||||
|
let previewMargin;
|
||||||
|
let statusFilterBtns = {};
|
||||||
|
let filterSuite = null;
|
||||||
|
let filterStatus = "failure";
|
||||||
|
let lastScrollPos = 0;
|
||||||
|
let previewMarginH = 0;
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
preview = document.querySelector("#junit-preview > .prose");
|
||||||
|
previewMargin = document.getElementById("preview-margin");
|
||||||
|
|
||||||
|
statusFilterBtns = {};
|
||||||
|
const btnElms = document.getElementById("junit-statusfilter").children;
|
||||||
|
for (let i=0; i<btnElms.length; i++) {
|
||||||
|
const elm = btnElms[i];
|
||||||
|
statusFilterBtns[elm.attributes["data-status"].value] = elm;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("#junit-suites li > button").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => filterBySuite(btn));
|
||||||
|
});
|
||||||
|
document.querySelectorAll("#junit-statusfilter > button").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => filterByStatus(btn));
|
||||||
|
});
|
||||||
|
document.querySelectorAll("#junit-cases li > button").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", selectTestCase);
|
||||||
|
});
|
||||||
|
doFilter();
|
||||||
|
|
||||||
|
document.addEventListener("scroll", () => {
|
||||||
|
const delta = lastScrollPos - document.documentElement.scrollTop;
|
||||||
|
if (delta > 0) {
|
||||||
|
previewMarginH = Math.max(previewMarginH - delta, 0);
|
||||||
|
lastScrollPos = document.documentElement.scrollTop;
|
||||||
|
previewMargin.style.marginTop = previewMarginH + "px";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setCls(elm, cls, val) {
|
||||||
|
if (val) elm.classList.add(cls);
|
||||||
|
else elm.classList.remove(cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTestCase(event) {
|
||||||
|
let elm = event.target.closest("li");
|
||||||
|
const pvc = elm.querySelector(".pvcontent");
|
||||||
|
if (pvc && preview) {
|
||||||
|
preview.innerHTML = pvc.innerHTML;
|
||||||
|
preview.parentElement.setAttribute("data-status", elm.attributes["data-status"].value);
|
||||||
|
|
||||||
|
previewMarginH = Math.max(document.documentElement.scrollTop - 72, 20);
|
||||||
|
lastScrollPos = document.documentElement.scrollTop;
|
||||||
|
previewMargin.style.marginTop = previewMarginH + "px";
|
||||||
|
|
||||||
|
resetBtns("junit-cases ul");
|
||||||
|
elm.querySelector("button").classList.add("active");
|
||||||
|
if (window.innerWidth < 1000) preview.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetBtns(id) {
|
||||||
|
document.querySelectorAll(`#${id} .active`).forEach((elm) => {
|
||||||
|
elm.classList.remove("active");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function doFilter() {
|
||||||
|
const nStatus = {all: 0};
|
||||||
|
document.querySelectorAll("#junit-cases li").forEach((elm) => {
|
||||||
|
const status = elm.attributes["data-status"].value;
|
||||||
|
const isSuite = filterSuite === null || filterSuite === elm.attributes["data-suite"].value;
|
||||||
|
const vis = isSuite && (filterStatus === "all" || filterStatus === status);
|
||||||
|
setCls(elm, "hidden", !vis);
|
||||||
|
if (isSuite) {
|
||||||
|
nStatus[status] = nStatus[status] ? nStatus[status]+1 : 1;
|
||||||
|
nStatus.all++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!nStatus[filterStatus]) {
|
||||||
|
filterStatus = "all";
|
||||||
|
doFilter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(statusFilterBtns).forEach(([status, elm]) => {
|
||||||
|
const n = nStatus[status] ?? 0;
|
||||||
|
elm.children[0].textContent = n;
|
||||||
|
if (status === filterStatus) elm.classList.add("active");
|
||||||
|
else elm.classList.remove("active");
|
||||||
|
setCls(elm, "hidden", n === 0);
|
||||||
|
});
|
||||||
|
setCls(document.getElementById("junit-cases"), "filtered", filterSuite);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterBySuite(btn) {
|
||||||
|
const suite = btn.textContent;
|
||||||
|
if (suite) {
|
||||||
|
filterSuite = suite === "Show all" ? null : suite;
|
||||||
|
doFilter();
|
||||||
|
resetBtns("junit-suites");
|
||||||
|
btn.classList.add("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByStatus(btn) {
|
||||||
|
filterStatus = btn.attributes["data-status"].value;
|
||||||
|
doFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @license-end
|
||||||
|
|
||||||
|
</script>
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
source: src/viewer/markdown.rs
|
||||||
|
assertion_line: 138
|
||||||
|
expression: res
|
||||||
|
---
|
||||||
|
<div class="prose"><h1>Hello World</h1>
|
||||||
|
<p>this is a small paragraph for <em>testing</em>.</p>
|
||||||
|
</div>
|
|
@ -27,7 +27,7 @@
|
||||||
placeholder="codeberg.org/username/repo/actions/runs/42"
|
placeholder="codeberg.org/username/repo/actions/runs/42"
|
||||||
style="flex-grow: 1"
|
style="flex-grow: 1"
|
||||||
/>
|
/>
|
||||||
<button type="submit">Browse</button>
|
<button class="btn" type="submit">Browse</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
183
templates/junit.hbs
Normal file
183
templates/junit.hbs
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
<div class="junit">
|
||||||
|
<div id="junit-suites">
|
||||||
|
<p class="coltitle">Test suites:</p>
|
||||||
|
<ul>
|
||||||
|
<li><button class="active">Show all</button></li>
|
||||||
|
{{#each suites.suites}}
|
||||||
|
<li data-status="{{#if errors == 0 && failures == 0}}success{{else}}failure{{/if}}"><button>{{name}}</button></li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="junit-cases">
|
||||||
|
<p class="coltitle">Test cases:</p>
|
||||||
|
<p id="junit-statusfilter" class="colsubtitle">
|
||||||
|
<button data-status="all">All <b>?</b></button>
|
||||||
|
<button data-status="success">OK <b>?</b></button>
|
||||||
|
<button data-status="failure">Failed <b>?</b></button>
|
||||||
|
<button data-status="error">Error <b>?</b></button>
|
||||||
|
<button data-status="flaky">Flaky <b>?</b></button>
|
||||||
|
<button data-status="skipped">Skipped <b>?</b></button>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{{~#each suites.suites ~}}{{ let suite_name = &name }}
|
||||||
|
{{#each cases}}
|
||||||
|
<li data-suite="{{ suite_name }}" data-status="{{ status.id() }}">
|
||||||
|
<button>{{#if let Some(cn) = &classname}}<span>{{ cn }}::</span>{{/if}}{{ original_name }}</button>
|
||||||
|
<div class="pvcontent">
|
||||||
|
<h2><i class="gg-{{#if status.id() == "success"}}check-o{{else if status.id() == "skipped"}}block{{else if status.id() == "flaky"}}danger{{else}}close-o{{/if}}"></i> {{name}}</h2>
|
||||||
|
<p class="badges"><span>{{ this.status_txt() }}</span><span>{{ crate::util::time_to_ms(time) }}ms</span></p>
|
||||||
|
{{~#if let Some(msg) = status.message() ~}}
|
||||||
|
<pre><code>{{msg.message}}{{msg.text}}</code></pre>
|
||||||
|
{{~/if}}
|
||||||
|
{{~#if let Some(ref stdout) = system_out ~}}
|
||||||
|
<pre><code>{{stdout}}</code></pre>
|
||||||
|
{{~/if}}
|
||||||
|
{{~#if let Some(ref stderr) = system_err ~}}
|
||||||
|
<pre><code>{{stderr}}</code></pre>
|
||||||
|
{{~/if}}
|
||||||
|
{{~#each retries ~}}
|
||||||
|
<h3>Failed attempt #{{index}}</h3>
|
||||||
|
<p class="badges"><span>{{ crate::util::time_to_ms(time) }}ms</span></p>
|
||||||
|
{{~#if let Some(msg) = status.message() ~}}
|
||||||
|
<pre><code>{{msg.message}}{{msg.text}}</code></pre>
|
||||||
|
{{~/if}}
|
||||||
|
{{~#if let Some(ref stdout) = system_out ~}}
|
||||||
|
<pre><code>{{stdout}}</code></pre>
|
||||||
|
{{~/if}}
|
||||||
|
{{~#if let Some(ref stderr) = system_err ~}}
|
||||||
|
<pre><code>{{stderr}}</code></pre>
|
||||||
|
{{~/if}}
|
||||||
|
{{~/each}}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
{{~/each}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="junit-preview">
|
||||||
|
<div id="preview-margin"></div>
|
||||||
|
<div class="prose">
|
||||||
|
<p class="light">Select a test case to show details</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
|
||||||
|
|
||||||
|
let preview;
|
||||||
|
let previewMargin;
|
||||||
|
let statusFilterBtns = {};
|
||||||
|
let filterSuite = null;
|
||||||
|
let filterStatus = "failure";
|
||||||
|
let lastScrollPos = 0;
|
||||||
|
let previewMarginH = 0;
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
preview = document.querySelector("#junit-preview > .prose");
|
||||||
|
previewMargin = document.getElementById("preview-margin");
|
||||||
|
|
||||||
|
statusFilterBtns = {};
|
||||||
|
const btnElms = document.getElementById("junit-statusfilter").children;
|
||||||
|
for (let i=0; i<btnElms.length; i++) {
|
||||||
|
const elm = btnElms[i];
|
||||||
|
statusFilterBtns[elm.attributes["data-status"].value] = elm;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("#junit-suites li > button").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => filterBySuite(btn));
|
||||||
|
});
|
||||||
|
document.querySelectorAll("#junit-statusfilter > button").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => filterByStatus(btn));
|
||||||
|
});
|
||||||
|
document.querySelectorAll("#junit-cases li > button").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", selectTestCase);
|
||||||
|
});
|
||||||
|
doFilter();
|
||||||
|
|
||||||
|
document.addEventListener("scroll", () => {
|
||||||
|
const delta = lastScrollPos - document.documentElement.scrollTop;
|
||||||
|
if (delta > 0) {
|
||||||
|
previewMarginH = Math.max(previewMarginH - delta, 0);
|
||||||
|
lastScrollPos = document.documentElement.scrollTop;
|
||||||
|
previewMargin.style.marginTop = previewMarginH + "px";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setCls(elm, cls, val) {
|
||||||
|
if (val) elm.classList.add(cls);
|
||||||
|
else elm.classList.remove(cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTestCase(event) {
|
||||||
|
let elm = event.target.closest("li");
|
||||||
|
const pvc = elm.querySelector(".pvcontent");
|
||||||
|
if (pvc && preview) {
|
||||||
|
preview.innerHTML = pvc.innerHTML;
|
||||||
|
preview.parentElement.setAttribute("data-status", elm.attributes["data-status"].value);
|
||||||
|
|
||||||
|
previewMarginH = Math.max(document.documentElement.scrollTop - 72, 20);
|
||||||
|
lastScrollPos = document.documentElement.scrollTop;
|
||||||
|
previewMargin.style.marginTop = previewMarginH + "px";
|
||||||
|
|
||||||
|
resetBtns("junit-cases ul");
|
||||||
|
elm.querySelector("button").classList.add("active");
|
||||||
|
if (window.innerWidth < 1000) preview.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetBtns(id) {
|
||||||
|
document.querySelectorAll(`#${id} .active`).forEach((elm) => {
|
||||||
|
elm.classList.remove("active");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function doFilter() {
|
||||||
|
const nStatus = {all: 0};
|
||||||
|
document.querySelectorAll("#junit-cases li").forEach((elm) => {
|
||||||
|
const status = elm.attributes["data-status"].value;
|
||||||
|
const isSuite = filterSuite === null || filterSuite === elm.attributes["data-suite"].value;
|
||||||
|
const vis = isSuite && (filterStatus === "all" || filterStatus === status);
|
||||||
|
setCls(elm, "hidden", !vis);
|
||||||
|
if (isSuite) {
|
||||||
|
nStatus[status] = nStatus[status] ? nStatus[status]+1 : 1;
|
||||||
|
nStatus.all++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!nStatus[filterStatus]) {
|
||||||
|
filterStatus = "all";
|
||||||
|
doFilter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(statusFilterBtns).forEach(([status, elm]) => {
|
||||||
|
const n = nStatus[status] ?? 0;
|
||||||
|
elm.children[0].textContent = n;
|
||||||
|
if (status === filterStatus) elm.classList.add("active");
|
||||||
|
else elm.classList.remove("active");
|
||||||
|
setCls(elm, "hidden", n === 0);
|
||||||
|
});
|
||||||
|
setCls(document.getElementById("junit-cases"), "filtered", filterSuite);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterBySuite(btn) {
|
||||||
|
const suite = btn.textContent;
|
||||||
|
if (suite) {
|
||||||
|
filterSuite = suite === "Show all" ? null : suite;
|
||||||
|
doFilter();
|
||||||
|
resetBtns("junit-suites");
|
||||||
|
btn.classList.add("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByStatus(btn) {
|
||||||
|
filterStatus = btn.attributes["data-status"].value;
|
||||||
|
doFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @license-end
|
||||||
|
|
||||||
|
</script>
|
|
@ -4,9 +4,7 @@
|
||||||
{{> partial/fileIcons }}
|
{{> partial/fileIcons }}
|
||||||
<header>
|
<header>
|
||||||
{{> partial/logoLink }}
|
{{> partial/logoLink }}
|
||||||
<h1>
|
<h1>{{#each path_components}}<a href="{{this.url}}">{{this.name}}</a><span class="sep">/</span>{{/each}}</h1>
|
||||||
{{#each path_components}}<a href="{{url}}">{{name}}</a>{{/each}}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div class="metadata">
|
<div class="metadata">
|
||||||
|
|
|
@ -4,10 +4,7 @@
|
||||||
{{~/partial/header }}
|
{{~/partial/header }}
|
||||||
<header>
|
<header>
|
||||||
{{> partial/logoLink }}
|
{{> partial/logoLink }}
|
||||||
<h1>
|
<h1>{{#each path_components}}<a href="{{url}}">{{name}}</a><span class="sep">/</span>{{/each}}<span>{{filename}}</span></h1>
|
||||||
{{#each path_components}}<a href="{{url}}">{{name}}</a> /{{/each}}
|
|
||||||
<span>{{filename}}</span>
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
@ -18,7 +15,7 @@
|
||||||
<span>{{size}}</span>
|
<span>{{size}}</span>
|
||||||
<a href="{{run_url}}" target="_blank" rel="noopener noreferrer">CI run</a>
|
<a href="{{run_url}}" target="_blank" rel="noopener noreferrer">CI run</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div id="viewers">
|
||||||
{{#each viewers}}<a {{#if selected}}class="selected"{{/if}} href="?viewer={{id}}">{{name}}</a>{{/each}}
|
{{#each viewers}}<a {{#if selected}}class="selected"{{/if}} href="?viewer={{id}}">{{name}}</a>{{/each}}
|
||||||
<a href="{{filename}}">Raw</a>
|
<a href="{{filename}}">Raw</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,10 +4,7 @@
|
||||||
{{> partial/fileIcons }}
|
{{> partial/fileIcons }}
|
||||||
<header>
|
<header>
|
||||||
{{> partial/logoLink }}
|
{{> partial/logoLink }}
|
||||||
<h1>
|
<h1><a href="/?url={{run_url}}">{{run_name}}</a><span class="sep">/</span></h1>
|
||||||
<a href="/?url={{run_url}}">{{run_name}}</a>
|
|
||||||
/
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div class="metadata">
|
<div class="metadata">
|
||||||
|
|
40
tests/testfiles/junit/hello.junit.xml
Normal file
40
tests/testfiles/junit/hello.junit.xml
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<testsuites name="nextest-run" tests="2" failures="1" errors="0" uuid="80dd57da-f066-4d37-9b3c-d1d98c5789bd" timestamp="2024-06-11T20:32:12.532+02:00" time="0.007">
|
||||||
|
<testsuite name="lib1" tests="2" disabled="0" errors="0" failures="1">
|
||||||
|
<testcase name="tests::it_works" classname="lib1" timestamp="2024-06-11T20:32:12.532+02:00" time="0.003">
|
||||||
|
<system-out>
|
||||||
|
running 1 test
|
||||||
|
test tests::it_works ... ok
|
||||||
|
|
||||||
|
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
|
||||||
|
|
||||||
|
</system-out>
|
||||||
|
<system-err></system-err>
|
||||||
|
</testcase>
|
||||||
|
<testcase name="tests::pippi_langstrumpf" classname="lib1" timestamp="2024-06-11T20:32:12.532+02:00" time="0.003">
|
||||||
|
<failure type="test failure">thread 'tests::pippi_langstrumpf' panicked at src/lib.rs:18:9:
|
||||||
|
assertion `left == right` failed
|
||||||
|
left: 7
|
||||||
|
right: 9
|
||||||
|
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</failure>
|
||||||
|
<system-out>
|
||||||
|
running 1 test
|
||||||
|
test tests::pippi_langstrumpf ... FAILED
|
||||||
|
|
||||||
|
failures:
|
||||||
|
|
||||||
|
failures:
|
||||||
|
tests::pippi_langstrumpf
|
||||||
|
|
||||||
|
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
|
||||||
|
|
||||||
|
</system-out>
|
||||||
|
<system-err>thread 'tests::pippi_langstrumpf' panicked at src/lib.rs:18:9:
|
||||||
|
assertion `left == right` failed
|
||||||
|
left: 7
|
||||||
|
right: 9
|
||||||
|
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||||
|
</system-err>
|
||||||
|
</testcase>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
1
tests/testfiles/sites/.well-known/test.txt
Normal file
1
tests/testfiles/sites/.well-known/test.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This file should NOT be served for security reasons
|
10
tests/testfiles/sites/200.html
Normal file
10
tests/testfiles/sites/200.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SPA test</title>
|
||||||
|
<link rel='stylesheet' type='text/css' href='./style.css'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello SPA</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
tests/testfiles/sites/404.html
Normal file
10
tests/testfiles/sites/404.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Error 404</title>
|
||||||
|
<link rel='stylesheet' type='text/css' href='./style.css'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Not found</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
17
tests/testfiles/sites/example.rs
Normal file
17
tests/testfiles/sites/example.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
struct Point {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let point = Point { x: 1, y: 2 };
|
||||||
|
|
||||||
|
let serialized = serde_json::to_string(&point).unwrap();
|
||||||
|
println!("serialized = {}", serialized);
|
||||||
|
|
||||||
|
let deserialized: Point = serde_json::from_str(&serialized).unwrap();
|
||||||
|
println!("deserialized = {:?}", deserialized);
|
||||||
|
}
|
10
tests/testfiles/sites/index.html
Normal file
10
tests/testfiles/sites/index.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Artifactview test</title>
|
||||||
|
<link rel='stylesheet' type='text/css' href='./style.css'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
37
tests/testfiles/sites/make_zip.sh
Executable file
37
tests/testfiles/sites/make_zip.sh
Executable file
|
@ -0,0 +1,37 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Create artifacts for testing
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
TARGET="../sites_data"
|
||||||
|
# http://codeberg-org--thetadev--artifactview-test--1-1.localhost:3000
|
||||||
|
T_VIEW="codeberg-org--thetadev--artifactview-test--1-1"
|
||||||
|
# http://codeberg-org--thetadev--artifactview-test--1-2.localhost:3000
|
||||||
|
T_404="codeberg-org--thetadev--artifactview-test--1-2"
|
||||||
|
# http://codeberg-org--thetadev--artifactview-test--1-3.localhost:3000
|
||||||
|
T_SPA="codeberg-org--thetadev--artifactview-test--1-3"
|
||||||
|
|
||||||
|
mkdir -p $TARGET
|
||||||
|
rm -f $TARGET/*
|
||||||
|
|
||||||
|
zip --no-dir-entries $TARGET/$T_SPA index.html style.css 200.html
|
||||||
|
zip --no-dir-entries $TARGET/$T_404 index.html style.css 404.html
|
||||||
|
|
||||||
|
zip --no-dir-entries -r $TARGET/$T_VIEW robots.txt .well-known
|
||||||
|
zip --no-dir-entries --junk-paths $TARGET/$T_VIEW example.rs ../../../README.md
|
||||||
|
|
||||||
|
(
|
||||||
|
cd ..
|
||||||
|
zip --no-dir-entries -r sites_data/$T_VIEW junit sites/index.html sites/style.css
|
||||||
|
)
|
||||||
|
|
||||||
|
printf "404" > $TARGET/$T_404.name
|
||||||
|
printf "spa" > $TARGET/$T_SPA.name
|
||||||
|
printf "view" > $TARGET/$T_VIEW.name
|
||||||
|
|
||||||
|
if [ -n "$LTST" ]; then
|
||||||
|
mkdir -p /tmp/artifactview
|
||||||
|
cp $TARGET/* /tmp/artifactview
|
||||||
|
echo "copied artifacts for local testing"
|
||||||
|
fi
|
1
tests/testfiles/sites/robots.txt
Normal file
1
tests/testfiles/sites/robots.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This file should NOT be served for security reasons
|
5
tests/testfiles/sites/style.css
Normal file
5
tests/testfiles/sites/style.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
432
tests/tests.rs
Normal file
432
tests/tests.rs
Normal file
|
@ -0,0 +1,432 @@
|
||||||
|
use std::{
|
||||||
|
io::Read,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use axum_test::{TestRequest, TestResponse, TestServer};
|
||||||
|
use headers::HeaderMapExt;
|
||||||
|
use http::{header, HeaderName, HeaderValue, StatusCode};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use path_macro::path;
|
||||||
|
use rstest::{fixture, rstest};
|
||||||
|
|
||||||
|
use artifactview::{App, AppState, Config, ConfigData};
|
||||||
|
use scraper::{selectable::Selectable, ElementRef, Html, Selector};
|
||||||
|
use temp_testdir::TempDir;
|
||||||
|
|
||||||
|
static TESTFILES: Lazy<PathBuf> =
|
||||||
|
Lazy::new(|| path!(env!("CARGO_MANIFEST_DIR") / "tests" / "testfiles"));
|
||||||
|
|
||||||
|
static SITEDIR: Lazy<PathBuf> = Lazy::new(|| {
|
||||||
|
let sitedir = path!(*TESTFILES / "sites_data");
|
||||||
|
if !sitedir.is_dir() {
|
||||||
|
std::process::Command::new(path!(*TESTFILES / "sites" / "make_zip.sh"))
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
sitedir
|
||||||
|
});
|
||||||
|
|
||||||
|
static CACHEDIR: Lazy<TempDir> = Lazy::new(|| {
|
||||||
|
let td = TempDir::default();
|
||||||
|
setup_cache_dir(&td);
|
||||||
|
td
|
||||||
|
});
|
||||||
|
|
||||||
|
const S1: &str = "codeberg-org--thetadev--artifactview-test--1-1";
|
||||||
|
const S2: &str = "codeberg-org--thetadev--artifactview-test--1-2";
|
||||||
|
const S3: &str = "codeberg-org--thetadev--artifactview-test--1-3";
|
||||||
|
|
||||||
|
fn setup_cache_dir(dir: &Path) {
|
||||||
|
for entry in std::fs::read_dir(SITEDIR.as_path()).unwrap() {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
if entry.file_type().unwrap().is_file() {
|
||||||
|
std::fs::copy(entry.path(), path!(dir / entry.file_name())).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestAv {
|
||||||
|
server: TestServer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fixture]
|
||||||
|
fn server() -> TestAv {
|
||||||
|
let router = App::router(AppState::from_cfg(
|
||||||
|
Config::from_data(ConfigData {
|
||||||
|
cache_dir: CACHEDIR.to_path_buf(),
|
||||||
|
no_https: true,
|
||||||
|
real_ip_header: Some("x-forwarded-for".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut server = TestServer::new(router).unwrap();
|
||||||
|
|
||||||
|
server.add_header(
|
||||||
|
HeaderName::from_static("x-forwarded-for"),
|
||||||
|
HeaderValue::from_static("127.0.0.1"),
|
||||||
|
);
|
||||||
|
|
||||||
|
TestAv { server }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestAv {
|
||||||
|
fn get(&self, subdomain: &str, path: &str) -> TestRequest {
|
||||||
|
self.server.get(path).add_header(
|
||||||
|
header::HOST,
|
||||||
|
if subdomain.is_empty() {
|
||||||
|
HeaderValue::from_static("localhost:3000")
|
||||||
|
} else {
|
||||||
|
HeaderValue::from_str(&format!("{subdomain}.localhost:3000")).unwrap()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_html(&self, subdomain: &str, path: &str) -> Html {
|
||||||
|
let resp = self.get(subdomain, path).await;
|
||||||
|
resp.assert_status_ok();
|
||||||
|
assert_eq!(resp.header(header::CONTENT_TYPE), "text/html");
|
||||||
|
scraper::Html::parse_document(&resp.text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct FileEntry {
|
||||||
|
name: String,
|
||||||
|
size: String,
|
||||||
|
crc32: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
struct Link {
|
||||||
|
text: String,
|
||||||
|
href: String,
|
||||||
|
selected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_from_elm(elm: &ElementRef<'_>) -> String {
|
||||||
|
elm.text()
|
||||||
|
.fold(String::new(), |acc, s| acc + s)
|
||||||
|
.trim()
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_link(elm: &ElementRef<'_>) -> Link {
|
||||||
|
assert_eq!(elm.value().name(), "a");
|
||||||
|
Link {
|
||||||
|
text: text_from_elm(elm),
|
||||||
|
href: elm.attr("href").expect("href").to_owned(),
|
||||||
|
selected: elm
|
||||||
|
.value()
|
||||||
|
.has_class("selected", scraper::CaseSensitivity::CaseSensitive),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_cache_immutable(resp: &TestResponse) {
|
||||||
|
assert_eq!(
|
||||||
|
resp.header(header::CACHE_CONTROL),
|
||||||
|
"max-age=31536000,public,immutable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn homepage(server: TestAv) {
|
||||||
|
let doc = server.get_html("", "/").await;
|
||||||
|
let elm = doc
|
||||||
|
.select(&Selector::parse("title").unwrap())
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(text_from_elm(&elm), "Artifactview");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(S1, "/example.rs", true, "example.rs", "text/x-rust")]
|
||||||
|
#[case(S1, "/sites/", true, "index.html", "text/html")]
|
||||||
|
#[case(S1, "/foo.txt", false, "", "text/html")]
|
||||||
|
#[case(S1, "/.well-known/test.txt", false, "", "text/html")]
|
||||||
|
// 404 fallback
|
||||||
|
#[case(S2, "/foo.txt", false, "404.html", "text/html")]
|
||||||
|
// SPA
|
||||||
|
#[case(S3, "/", true, "index.html", "text/html")]
|
||||||
|
#[case(S3, "/foo.txt", true, "200.html", "text/html")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_file(
|
||||||
|
server: TestAv,
|
||||||
|
#[case] sd: &str,
|
||||||
|
#[case] path: &str,
|
||||||
|
#[case] found: bool,
|
||||||
|
#[case] exp_path: &str,
|
||||||
|
#[case] mime: &str,
|
||||||
|
) {
|
||||||
|
let resp = server.get(sd, path).await;
|
||||||
|
assert_eq!(resp.header(header::CONTENT_TYPE), mime);
|
||||||
|
|
||||||
|
if found {
|
||||||
|
resp.assert_status_ok();
|
||||||
|
assert_cache_immutable(&resp);
|
||||||
|
} else {
|
||||||
|
resp.assert_status_not_found();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exp_path.is_empty() {
|
||||||
|
let expect = std::fs::read_to_string(path!(*TESTFILES / "sites" / exp_path)).unwrap();
|
||||||
|
assert_eq!(resp.text(), expect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn robots_txt(server: TestAv) {
|
||||||
|
let resp1 = server.get("", "/robots.txt").await;
|
||||||
|
let resp2 = server.get(S1, "/robots.txt").await;
|
||||||
|
|
||||||
|
assert!(resp1.text().contains("\nUser-agent: *\nDisallow: /\n"));
|
||||||
|
assert!(resp2.text().contains("\nUser-agent: *\nDisallow: /\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn stylesheet(server: TestAv) {
|
||||||
|
let html = server.get_html(S1, "/README.md?viewer=1").await;
|
||||||
|
for path in html
|
||||||
|
.select(&Selector::parse("link[rel=\"stylesheet\"]").unwrap())
|
||||||
|
.map(|elm| {
|
||||||
|
elm.attr("href")
|
||||||
|
.expect("href")
|
||||||
|
.strip_prefix("http://localhost:3000")
|
||||||
|
.expect("localhost url")
|
||||||
|
})
|
||||||
|
{
|
||||||
|
let resp = server.get("", path).await;
|
||||||
|
resp.assert_status_ok();
|
||||||
|
assert_eq!(resp.header(header::CONTENT_TYPE), "text/css");
|
||||||
|
assert_cache_immutable(&resp);
|
||||||
|
|
||||||
|
// Remove running number from stylesheet path
|
||||||
|
let fname = path
|
||||||
|
.strip_prefix('/')
|
||||||
|
.unwrap()
|
||||||
|
.strip_suffix(".css")
|
||||||
|
.unwrap()
|
||||||
|
.trim_end_matches(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
|
||||||
|
let expect = std::fs::read_to_string(path!(
|
||||||
|
env!("CARGO_MANIFEST_DIR") / "resources" / format!("{fname}.css")
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resp.text(), expect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_listing(doc: &Html) -> Vec<FileEntry> {
|
||||||
|
let sel_col = Selector::parse("td").unwrap();
|
||||||
|
doc.select(&Selector::parse(".file").unwrap())
|
||||||
|
.map(|elm| {
|
||||||
|
let icn = elm
|
||||||
|
.select(&Selector::parse("svg > use").unwrap())
|
||||||
|
.next()
|
||||||
|
.expect("icon");
|
||||||
|
let (k, icn_id) = icn.value().attrs().next().expect("icon id");
|
||||||
|
assert_eq!(k, "href");
|
||||||
|
assert!(matches!(icn_id, "#file" | "#folder"));
|
||||||
|
|
||||||
|
let mut parts = elm.select(&sel_col).map(|elm| text_from_elm(&elm));
|
||||||
|
|
||||||
|
let name = parts.next().expect("name");
|
||||||
|
let folder = icn_id == "#folder";
|
||||||
|
assert_eq!(name.ends_with('/'), folder);
|
||||||
|
|
||||||
|
FileEntry {
|
||||||
|
name,
|
||||||
|
size: parts.next().expect("size"),
|
||||||
|
crc32: parts.next().expect("crc32"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("/", &[
|
||||||
|
".well-known/",
|
||||||
|
"junit/",
|
||||||
|
"sites/",
|
||||||
|
"README.md",
|
||||||
|
"example.rs",
|
||||||
|
"robots.txt"
|
||||||
|
])]
|
||||||
|
#[case("/?C=N&O=D", &[
|
||||||
|
"sites/",
|
||||||
|
"junit/",
|
||||||
|
".well-known/",
|
||||||
|
"robots.txt",
|
||||||
|
"example.rs",
|
||||||
|
"README.md",
|
||||||
|
])]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn listing(server: TestAv, #[case] path: &str, #[case] expect: &[&str]) {
|
||||||
|
let doc = server.get_html(S1, path).await;
|
||||||
|
let files = parse_listing(&doc);
|
||||||
|
let file_names = files.iter().map(|f| f.name.to_owned()).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(file_names, expect);
|
||||||
|
|
||||||
|
let example_rs = files
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.name == "example.rs")
|
||||||
|
.expect("example.rs");
|
||||||
|
assert_eq!(example_rs.size, "406 B");
|
||||||
|
assert_eq!(example_rs.crc32, "2013120c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
// JUnit
|
||||||
|
#[case("/junit/hello.junit.xml?viewer=1", "junit", &[("junit", "JUnit"), ("code", "Code")])]
|
||||||
|
#[case("/junit/hello.junit.xml?viewer=junit", "junit", &[("junit", "JUnit"), ("code", "Code")])]
|
||||||
|
#[case("/junit/hello.junit.xml?viewer=code", "code", &[("junit", "JUnit"), ("code", "Code")])]
|
||||||
|
#[case("/junit/hello.junit.xml?viewer=md", "", &[])]
|
||||||
|
#[case("/example.rs?viewer=1", "code", &[("code", "Code")])]
|
||||||
|
#[case("/README.md?viewer=1", "md", &[("md", "Markdown"), ("code", "Code")])]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn viewer(
|
||||||
|
server: TestAv,
|
||||||
|
#[case] path: &str,
|
||||||
|
#[case] vid: &str,
|
||||||
|
#[case] exp_viewers: &[(&str, &str)],
|
||||||
|
) {
|
||||||
|
if vid.is_empty() {
|
||||||
|
// Raw file
|
||||||
|
let resp = server.get(S1, path).await;
|
||||||
|
resp.assert_status_ok();
|
||||||
|
assert_eq!(resp.header(header::CONTENT_TYPE), "text/xml");
|
||||||
|
assert_cache_immutable(&resp);
|
||||||
|
} else {
|
||||||
|
let doc = server.get_html(S1, path).await;
|
||||||
|
let viewers = doc
|
||||||
|
.select(&Selector::parse("#viewers a").unwrap())
|
||||||
|
.map(|elm| parse_link(&elm))
|
||||||
|
.filter(|ln| ln.text != "Raw")
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let viewer_names = viewers
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
(
|
||||||
|
v.href.strip_prefix("?viewer=").expect("link prefix"),
|
||||||
|
v.text.as_str(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(viewer_names, exp_viewers);
|
||||||
|
|
||||||
|
// Selected viewer
|
||||||
|
let mut sel_iter = viewers.iter().filter(|ln| ln.selected);
|
||||||
|
let selected = sel_iter.next().expect("selected");
|
||||||
|
assert_eq!(selected.href, format!("?viewer={vid}"));
|
||||||
|
assert!(sel_iter.next().is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn if_modified(server: TestAv) {
|
||||||
|
let resp = server.get(S1, "/README.md").await;
|
||||||
|
let lastmod = SystemTime::from(
|
||||||
|
resp.headers()
|
||||||
|
.typed_get::<headers::LastModified>()
|
||||||
|
.expect("last modified"),
|
||||||
|
);
|
||||||
|
let bef_lastmod = lastmod - Duration::from_secs(1);
|
||||||
|
|
||||||
|
// if-modified-since
|
||||||
|
let resp = server
|
||||||
|
.get(S1, "/README.md")
|
||||||
|
.add_header(
|
||||||
|
header::IF_MODIFIED_SINCE,
|
||||||
|
HeaderValue::from_str(&httpdate::HttpDate::from(lastmod).to_string()).unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
resp.assert_status(StatusCode::NOT_MODIFIED);
|
||||||
|
|
||||||
|
let resp = server
|
||||||
|
.get(S1, "/README.md")
|
||||||
|
.add_header(
|
||||||
|
header::IF_MODIFIED_SINCE,
|
||||||
|
HeaderValue::from_str(&httpdate::HttpDate::from(bef_lastmod).to_string()).unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
resp.assert_status_ok();
|
||||||
|
|
||||||
|
// if-unmodified-since
|
||||||
|
let resp = server
|
||||||
|
.get(S1, "/README.md")
|
||||||
|
.add_header(
|
||||||
|
header::IF_UNMODIFIED_SINCE,
|
||||||
|
HeaderValue::from_str(&httpdate::HttpDate::from(lastmod).to_string()).unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
resp.assert_status_ok();
|
||||||
|
|
||||||
|
let resp = server
|
||||||
|
.get(S1, "/README.md")
|
||||||
|
.add_header(
|
||||||
|
header::IF_UNMODIFIED_SINCE,
|
||||||
|
HeaderValue::from_str(&httpdate::HttpDate::from(bef_lastmod).to_string()).unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
resp.assert_status(StatusCode::PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn range_request(server: TestAv) {
|
||||||
|
let resp = server
|
||||||
|
.get(S1, "/example.rs")
|
||||||
|
.add_header(header::RANGE, HeaderValue::from_static("bytes=10-99"))
|
||||||
|
.await;
|
||||||
|
resp.assert_status(StatusCode::PARTIAL_CONTENT);
|
||||||
|
assert_cache_immutable(&resp);
|
||||||
|
assert_eq!(resp.header(header::CONTENT_TYPE), "text/x-rust");
|
||||||
|
assert_eq!(resp.header(header::CONTENT_LENGTH), "90");
|
||||||
|
assert_eq!(resp.header(header::CONTENT_RANGE), "bytes 10-99/406");
|
||||||
|
|
||||||
|
let expect = std::fs::read_to_string(path!(*TESTFILES / "sites" / "example.rs")).unwrap();
|
||||||
|
assert_eq!(resp.text(), &expect[10..100]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn range_request_start(server: TestAv) {
|
||||||
|
let resp = server
|
||||||
|
.get(S1, "/example.rs")
|
||||||
|
.add_header(header::RANGE, HeaderValue::from_static("bytes=100-"))
|
||||||
|
.await;
|
||||||
|
resp.assert_status(StatusCode::PARTIAL_CONTENT);
|
||||||
|
assert_cache_immutable(&resp);
|
||||||
|
assert_eq!(resp.header(header::CONTENT_TYPE), "text/x-rust");
|
||||||
|
assert_eq!(resp.header(header::CONTENT_LENGTH), "306");
|
||||||
|
assert_eq!(resp.header(header::CONTENT_RANGE), "bytes 100-405/406");
|
||||||
|
|
||||||
|
let expect = std::fs::read_to_string(path!(*TESTFILES / "sites" / "example.rs")).unwrap();
|
||||||
|
assert_eq!(resp.text(), &expect[100..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn compressed(server: TestAv) {
|
||||||
|
let resp = server
|
||||||
|
.get(S1, "/example.rs")
|
||||||
|
.add_header(header::ACCEPT_ENCODING, HeaderValue::from_static("gzip"))
|
||||||
|
.await;
|
||||||
|
let bts = resp.into_bytes().to_vec();
|
||||||
|
let mut gz = flate2::read::GzDecoder::new(&bts[..]);
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
gz.read_to_string(&mut buf).unwrap();
|
||||||
|
|
||||||
|
let expect = std::fs::read_to_string(path!(*TESTFILES / "sites" / "example.rs")).unwrap();
|
||||||
|
assert_eq!(buf, expect);
|
||||||
|
}
|
Loading…
Reference in a new issue