From 800f703214e2a9ff36dc3762d74decdb6215cefe Mon Sep 17 00:00:00 2001 From: CosmicHorror Date: Tue, 16 Apr 2024 21:25:20 -0600 Subject: [PATCH] refactor(tests): Drop wiremock (#320) * docs: Describe what we use deps for * refactor(tests): Switch `wiremock` for a custom impl --- Cargo.lock | 289 ++++----------------------------------- Cargo.toml | 180 +++++++++++++++++------- src/interpreter/tests.rs | 61 ++------- src/main.rs | 1 + src/test_utils.rs | 111 +++++++++++++++ 5 files changed, 283 insertions(+), 359 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9508f01d..19b690a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "ash" version = "0.37.3+1.3.251" @@ -169,16 +175,6 @@ dependencies = [ "libloading 0.7.4", ] -[[package]] -name = "assert-json-diff" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "async-broadcast" version = "0.7.0" @@ -565,6 +561,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + [[package]] name = "clap" version = "4.5.4" @@ -913,24 +915,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" -[[package]] -name = "deadpool" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" -dependencies = [ - "async-trait", - "deadpool-runtime", - "num_cpus", - "tokio", -] - -[[package]] -name = "deadpool-runtime" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63dfa964fe2a66f3fde91fc70b267fe193d822c7e603e2a675a49a7f46ad3f49" - [[package]] name = "deranged" version = "0.3.11" @@ -1419,48 +1403,12 @@ dependencies = [ "new_debug_unreachable", ] -[[package]] -name = "futures" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", - "futures-sink", -] - [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" -[[package]] -name = "futures-executor" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - [[package]] name = "futures-io" version = "0.3.30" @@ -1480,17 +1428,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "futures-macro" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.58", -] - [[package]] name = "futures-sink" version = "0.3.30" @@ -1509,10 +1446,8 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ - "futures-channel", "futures-core", "futures-io", - "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1669,25 +1604,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c" -[[package]] -name = "h2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap 2.2.6", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "half" version = "2.4.0" @@ -1788,46 +1704,6 @@ dependencies = [ "syn 2.0.58", ] -[[package]] -name = "http" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - [[package]] name = "httpdate" version = "1.0.3" @@ -1850,43 +1726,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "hyper" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "socket2", - "tokio", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1993,6 +1832,7 @@ dependencies = [ "syntect", "taffy", "tempfile", + "tiny_http", "toml", "tracing", "tracing-subscriber", @@ -2001,7 +1841,6 @@ dependencies = [ "ureq", "wgpu", "winit", - "wiremock", ] [[package]] @@ -3790,16 +3629,6 @@ dependencies = [ "wayland-backend", ] -[[package]] -name = "socket2" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "spin" version = "0.9.8" @@ -4101,6 +3930,18 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -4116,47 +3957,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tokio" -version = "1.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.58", -] - -[[package]] -name = "tokio-util" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - [[package]] name = "toml" version = "0.8.12" @@ -4274,12 +4074,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "ttf-parser" version = "0.19.2" @@ -4580,15 +4374,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -5271,30 +5056,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "wiremock" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec874e1eef0df2dcac546057fe5e29186f09c378181cd7b635b4b7bcc98e9d81" -dependencies = [ - "assert-json-diff", - "async-trait", - "base64 0.21.7", - "deadpool", - "futures", - "http", - "http-body-util", - "hyper", - "hyper-util", - "log", - "once_cell", - "regex", - "serde", - "serde_json", - "tokio", - "url", -] - [[package]] name = "x11-clipboard" version = "0.9.2" diff --git a/Cargo.toml b/Cargo.toml index 7b726fc4..e1d0a355 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ exclude = [ "/ci/*", "/.github/*", "/assets/manual_test_data/*", + "/typos.toml", ] keywords = ["markdown", "viewer", "gpu"] @@ -28,55 +29,150 @@ x11 = ["copypasta/x11", "winit/x11"] wayland = ["copypasta/wayland", "winit/wayland"] [dependencies] -winit = { version = "0.28.7", default-features = false } -wgpu = "0.16" -bytemuck = { version = "1.15.0", features = [ "derive" ] } -lyon = "1.0.1" -comrak = { version = "0.22.0", default-features = false, features = ["shortcodes", "syntect"] } -open = "5.1.2" -html5ever = "0.27.0" -image = "0.24.9" -clap = { version = "4.4.18", features = ["cargo", "derive"] } -copypasta = { version = "0.10.1", default-features = false } -resvg = "0.37.0" +# `anstream` and `anstyle` are both terminal helper crates used for our custom +# panic hook (and already used by `clap`, so it's a "free" dependency) +anstream = "0.6.13" +anstyle = "1.0.6" +# Easier error handling anyhow = "1.0.81" +# System preferred color scheme detection +dark-light = "1.1.1" +# System specific directories dirs = "5.0.1" -serde = { version = "1.0.197", features = ["derive"] } -toml = "0.8.12" +# Used to open the config file with `$ inlyne config open` +edit = "0.1.5" +# Faster hash for the text cache +fxhash = "0.2.1" +# GPU text rendering +glyphon = "0.3" +# Used to values used in YAML frontmatter when converting to HTML +html-escape = "0.2.13" +# Parsing the HTML document that the markdown+html was converted into +html5ever = "0.27.0" +# Provides some extra helpers that we use for our custom panic hook +human-panic = "1.2.3" +# Generic image decoding +image = "0.24.9" +# 2D GPU graphics rendering +lyon = "1.0.1" +# Images are compressed to in-memory lz4 blobs +lz4_flex = "0.11.3" +# Generic metrics facade for our metrics recording/emission infra +metrics = "0.22.3" +# File event notifications for the live reloading feature notify = "6.1.1" -dark-light = "1.1.1" -# We only decompress our own compressed data, so disable `safe-decode` and -# `checked-decode` -lz4_flex = { version = "0.11.3", default-features = false, features = ["frame", "safe-encode", "std"] } +# Used to open external links like in the user's browser +open = "5.1.2" +# Some alternative atomics that are slightly more ergonoics than `std`'s +parking_lot = "0.12.1" +# Dead simple way to handle some async operations pollster = "0.3.0" +# Used to get a handle to the display, so that we can setup a clipboard +raw-window-handle = "0.5.2" +# SVG rendering +resvg = "0.37.0" +# Parses the optional YAML frontmatter (replace with just a yaml parser) serde_yaml = "0.9.34" -indexmap = { version = "2.2.6", features = ["serde"] } -html-escape = "0.2.13" -fxhash = "0.2.1" -twox-hash = "1.6.3" -taffy = "0.3.18" -syntect = "5.2.0" +# Easy `Debug` formatting changes used to keep snapshot tests more succinct smart-debug = "0.0.3" +# Helps power our syntax highlighting +syntect = "5.2.0" +# Some CSS layout algos that we use as a pretty decent alternative to us +# lacking HTML ones +taffy = "0.3.18" +# For parsing our config file +toml = "0.8.12" +# In application tracing (aka logging on steroids) +tracing = "0.1.40" +# Extra syntax and theme definitions for `syntect` two-face = "0.3.0" +# More text hashing... +twox-hash = "1.6.3" +# HTTP client for requesting images from urls +ureq = "2.9.6" +# Cross platform GPU magic sauce +wgpu = "0.16" + +# Used for casting types to GPU compatible formats +[dependencies.bytemuck] +version = "1.15.0" +features = ["derive"] + +# Command line arg parsing +[dependencies.clap] +version = "4.4.18" +features = ["cargo", "derive"] + +# Converts our markdown+html to pure HTML +[dependencies.comrak] +version = "0.22.0" +default-features = false +features = ["shortcodes", "syntect"] + +# Clipboard handling +[dependencies.copypasta] +version = "0.10.1" +default-features = false + # NOTE: We need `fontconfig` enabled to pick up fonts on some systems, but # `glyphon` doesn't provide any way set that feature for `fontdb`, so we have to # set the feature for the transitive dep while manually making sure that we keep # the versions in sync ;-; -fontdb = { version = "0.14.1", features = ["fontconfig"] } -human-panic = "1.2.3" -notify-debouncer-full = { version = "0.3.1", default-features = false } -tracing = "0.1.40" -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -glyphon = "0.3" -string_cache = { version = "0.8.7", default-features = false } -raw-window-handle = "0.5.2" -edit = "0.1.5" -anstream = "0.6.13" -anstyle = "1.0.6" -metrics = "0.22.3" -metrics-util = { version = "0.16.3", default-features = false, features = ["registry", "summary"] } -parking_lot = "0.12.1" -ureq = "2.9.6" +[dependencies.fontdb] +version = "0.14.1" +features = ["fontconfig"] + +# Common dep used from yaml that can probably be replaced with a `Vec<(_, _)>` +[dependencies.indexmap] +version = "2.2.6" +features = ["serde"] + +# Metrics helpers used for our custom metric logger +[dependencies.metrics-util] +version = "0.16.3" +default-features = false +features = ["registry", "summary"] + +# Debouncer that papers over some issues with various ways that editors save +# files +[dependencies.notify-debouncer-full] +version = "0.3.1" +default-features = false + +# For dealing with both TOML and YAML +[dependencies.serde] +version = "1.0.197" +features = ["derive"] + +# Common dep used by `html5ever` +[dependencies.string_cache] +version = "0.8.7" +default-features = false + +# Our specific tracing implementation +[dependencies.tracing-subscriber] +version = "0.3.18" +features = ["env-filter"] + +# Cross-platform window handling +[dependencies.winit] +version = "0.28.7" +default-features = false + +[dev-dependencies] +# Succinct and more readable binary blobs +base64 = "0.22.0" +# Used to update file's modified time for tests +filetime = "0.2.23" +# Snapshot testing +insta = "1.38.0" +# Assertions displayed as diffs which is immensely helpful for some of our large +# values +pretty_assertions = "1.4.0" +# Throwaway files/dirs for isolated test environments +tempfile = "3.10.1" +# Use for setting up a local http server to test image requests in isolation +tiny_http = "0.12.0" [target.'cfg(inlyne_tcp_metrics)'.dependencies] metrics-exporter-tcp = "0.9.0" @@ -94,14 +190,6 @@ debug = true inherits = "release" lto = true -[dev-dependencies] -base64 = "0.22.0" -filetime = "0.2.23" -insta = "1.38.0" -pretty_assertions = "1.4.0" -tempfile = "3.10.1" -wiremock = "0.6.0" - # Selectively bump up opt level for some dependencies to improve dev build perf [profile.dev.package] ttf-parser.opt-level = 2 diff --git a/src/interpreter/tests.rs b/src/interpreter/tests.rs index 027a03d6..6b60ab66 100644 --- a/src/interpreter/tests.rs +++ b/src/interpreter/tests.rs @@ -11,14 +11,13 @@ use super::{HtmlInterpreter, ImageCallback, WindowInteractor}; use crate::color::{Theme, ThemeDefaults}; use crate::image::{Image, ImageData}; use crate::opts::ResolvedTheme; -use crate::test_utils::init_test_log; +use crate::test_utils::{init_test_log, mock_file_server, File}; use crate::utils::Align; use crate::{Element, ImageCache}; use base64::prelude::*; use syntect::highlighting::Theme as SyntectTheme; use wgpu::TextureFormat; -use wiremock::{matchers, Mock, MockServer, ResponseTemplate}; // We use a dummy window with an internal counter that keeps track of when rendering a single md // document is finished @@ -415,55 +414,14 @@ snapshot_interpreted_elements!( (num_is_bold, NUM_IS_BOLD), ); -struct File { - url_path: String, - mime: String, - bytes: Vec, -} - -impl File { - fn new(url_path: &str, mime: &str, bytes: &[u8]) -> Self { - Self { - url_path: url_path.to_owned(), - mime: mime.to_owned(), - bytes: bytes.to_owned(), - } - } -} - -/// Spin up a server, so we can test network requests without external services -fn mock_file_server(files: &[File]) -> (MockServer, String) { - let setup_server = async { - let mock_server = MockServer::start().await; - - for file in files { - let File { - url_path, - mime, - bytes, - } = file; - Mock::given(matchers::method("GET")) - .and(matchers::path(url_path)) - .respond_with(ResponseTemplate::new(200).set_body_raw(bytes.to_owned(), mime)) - .mount(&mock_server) - .await; - } - - mock_server - }; - let server = pollster::block_on(setup_server); - - let server_url = server.uri(); - (server, server_url) -} - #[test] fn centered_image_with_size_align_and_link() { init_test_log(); let logo = include_bytes!("../../assets/test_data/bun_logo.png"); let logo_path = "/bun_logo.png"; - let (_server, server_url) = mock_file_server(&[File::new(logo_path, "image/png", logo)]); + let files = vec![File::new(logo_path, "image/png", logo)]; + let (_server, server_url) = mock_file_server(files); let logo_url = server_url + logo_path; let text = format!( @@ -488,8 +446,11 @@ fn image_loading_fails_gracefully() { let json = r#"{"im": "not an image"}"#; let json_path = "/snapshot.png"; - let (_server, server_url) = - mock_file_server(&[File::new(json_path, "application/json", json.as_bytes())]); + let (_server, server_url) = mock_file_server(vec![File::new( + json_path, + "application/json", + json.as_bytes(), + )]); let json_url = server_url + json_path; let text = format!("![This actually returns JSON 😈]({json_url})"); @@ -538,11 +499,13 @@ fn picture_dark_light() { (light_path, B64_SINGLE_PIXEL_WEBP_000), (default_path, B64_SINGLE_PIXEL_WEBP_999), ] + .into_iter() .map(|(path, b64_bytes)| { let bytes = BASE64_STANDARD.decode(b64_bytes).unwrap(); File::new(path, webp_mime, &bytes) - }); - let (_server, server_url) = mock_file_server(&files); + }) + .collect(); + let (_server, server_url) = mock_file_server(files); let dark_url = format!("{server_url}{dark_path}"); let light_url = format!("{server_url}{light_path}"); let default_url = format!("{server_url}{default_path}"); diff --git a/src/main.rs b/src/main.rs index 49679c18..2ab4c33c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,7 @@ pub mod positioner; pub mod renderer; pub mod selection; pub mod table; +#[cfg(test)] pub mod test_utils; pub mod text; pub mod utils; diff --git a/src/test_utils.rs b/src/test_utils.rs index 46fff985..3f309e8d 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1,3 +1,12 @@ +use std::{ + sync::{ + mpsc::{sync_channel, Receiver, SyncSender}, + Arc, + }, + thread, +}; + +use tiny_http::{Header, Method, Request, Response, Server}; use tracing_subscriber::prelude::*; pub fn init_test_log() { @@ -14,3 +23,105 @@ pub fn init_test_log() { ) .try_init(); } + +/// Spin up a server, so we can test network requests without external services +pub fn mock_file_server(files: Vec) -> (FileServer, String) { + let server = FileServer::spawn(files); + let url = server.url().to_owned(); + (server, url) +} + +pub struct File { + pub url_path: String, + pub mime: String, + pub bytes: Vec, +} + +impl File { + pub fn new(url_path: &str, mime: &str, bytes: &[u8]) -> Self { + Self { + url_path: url_path.to_owned(), + mime: mime.to_owned(), + bytes: bytes.to_owned(), + } + } +} + +pub struct FileServer { + url: String, + shutdown_send: SyncSender<()>, +} + +impl FileServer { + // Spawn the server + // |-> Move one handle to a shutdown thread + // |-> Move The other handle to a request handler thread + // | \-> Each request gets handled on a newly spawned thread + // \-> Return a server guard that shuts down on `drop()` + pub fn spawn>>(files: Files) -> Self { + let files = files.into(); + // Bind to the ephemeral port and then get the actual resolved address + let server = Server::http("127.0.0.1:0").unwrap(); + let ip = server + .server_addr() + .to_ip() + .expect("Provided addr is an ip"); + // We're using an `::http()` server + let url = format!("http://{ip}"); + + let server = Arc::new(server); + let (shutdown_send, shutdown_recv) = sync_channel(1); + + Self::spawn_router(Arc::clone(&server), files); + Self::spawn_shutdown(server, shutdown_recv); + + Self { url, shutdown_send } + } + + fn spawn_shutdown(server: Arc, shutdown_recv: Receiver<()>) { + thread::spawn(move || { + if let Ok(()) = shutdown_recv.recv() { + // Unblock the `.incoming_requests()` + server.unblock(); + } + }); + } + + fn spawn_router(server: Arc, files: Arc<[File]>) { + thread::spawn(move || { + for req in server.incoming_requests() { + let req_files = Arc::clone(&files); + thread::spawn(|| Self::handle_req(req, req_files)); + } + // Time to shutdown now + }); + } + + fn handle_req(req: Request, files: Arc<[File]>) { + match req.method() { + Method::Get => { + let path = req.url(); + match files.iter().find(|file| file.url_path == path) { + Some(file) => { + let header = + Header::from_bytes(b"Content-Type", file.mime.as_bytes()).unwrap(); + let resp = Response::from_data(file.bytes.clone()).with_header(header); + let _ = req.respond(resp); + } + None => _ = req.respond(Response::empty(404)), + } + } + _ => _ = req.respond(Response::empty(404)), + } + } + + pub fn url(&self) -> &str { + &self.url + } +} + +impl Drop for FileServer { + fn drop(&mut self) { + let _ = self.shutdown_send.send(()); + } +}