diff --git a/src/db/add_package.rs b/src/db/add_package.rs index 673107da3..27e90fa45 100644 --- a/src/db/add_package.rs +++ b/src/db/add_package.rs @@ -175,17 +175,14 @@ pub(crate) fn add_build_into_database( ) -> Result { debug!("Adding build into database"); let rows = conn.query( - "INSERT INTO builds (rid, rustc_version, - cratesfyi_version, - build_status, output) - VALUES ($1, $2, $3, $4, $5) - RETURNING id", + "INSERT INTO builds (rid, rustc_version, cratesfyi_version, build_status) + VALUES ($1, $2, $3, $4) + RETURNING id", &[ &release_id, &res.rustc_version, &res.docsrs_version, &res.successful, - &res.build_log, ], )?; Ok(rows[0].get(0)) diff --git a/src/docbuilder/rustwide_builder.rs b/src/docbuilder/rustwide_builder.rs index d4dd8d3e5..83d0e2bea 100644 --- a/src/docbuilder/rustwide_builder.rs +++ b/src/docbuilder/rustwide_builder.rs @@ -322,7 +322,7 @@ impl RustwideBuilder { let local_storage = tempfile::Builder::new().prefix("docsrs-docs").tempdir()?; - let res = build_dir + let successful = build_dir .build(&self.toolchain, &krate, self.prepare_sandbox(&limits)) .run(|build| { use docsrs_metadata::BuildTargets; @@ -410,11 +410,13 @@ impl RustwideBuilder { github_repo, )?; - if let Some(doc_coverage) = res.result.doc_coverage { + if let Some(doc_coverage) = res.doc_coverage { add_doc_coverage(&mut conn, release_id, doc_coverage)?; } - add_build_into_database(&mut conn, release_id, &res.result)?; + let build_id = add_build_into_database(&mut conn, release_id, &res.result)?; + let build_log_path = format!("build-logs/{}/{}.txt", build_id, default_target); + self.storage.store_one(build_log_path, res.build_log)?; // Some crates.io crate data is mutable, so we proactively update it during a release match self.index.api().get_crate_data(name) { @@ -422,13 +424,13 @@ impl RustwideBuilder { Err(err) => warn!("{:#?}", err), } - Ok(res) + Ok(res.result.successful) })?; build_dir.purge()?; krate.purge_from_cache(&self.workspace)?; local_storage.close()?; - Ok(res.result.successful) + Ok(successful) } fn build_target( @@ -556,13 +558,13 @@ impl RustwideBuilder { Ok(FullBuildResult { result: BuildResult { - build_log: storage.to_string(), rustc_version: self.rustc_version.clone(), docsrs_version: format!("docsrs {}", crate::BUILD_VERSION), successful, - doc_coverage, }, + doc_coverage, cargo_metadata, + build_log: storage.to_string(), target: target.to_string(), }) } @@ -705,6 +707,8 @@ struct FullBuildResult { result: BuildResult, target: String, cargo_metadata: CargoMetadata, + doc_coverage: Option, + build_log: String, } #[derive(Clone, Copy)] @@ -724,7 +728,5 @@ pub(crate) struct DocCoverage { pub(crate) struct BuildResult { pub(crate) rustc_version: String, pub(crate) docsrs_version: String, - pub(crate) build_log: String, pub(crate) successful: bool, - pub(crate) doc_coverage: Option, } diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 422e39138..178b73e24 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -149,9 +149,6 @@ impl Storage { // Store all files in `root_dir` into the backend under `prefix`. // - // If the environment is configured with S3 credentials, this will upload to S3; - // otherwise, this will store files in the database. - // // This returns (map, set). pub(crate) fn store_all( &self, @@ -199,10 +196,36 @@ impl Storage { self.store_inner(blobs.into_iter().map(Ok)) } + // Store file into the backend at the given path (also used to detect mime type), returns the + // chosen compression algorithm + pub(crate) fn store_one( + &self, + path: impl Into, + content: impl Into>, + ) -> Result { + let path = path.into(); + let content = content.into(); + let alg = CompressionAlgorithm::default(); + let content = compress(&*content, alg)?; + let mime = detect_mime(&path)?.to_owned(); + + self.store_inner(std::iter::once(Ok(Blob { + path, + mime, + content, + compression: Some(alg), + // this field is ignored by the backend + date_updated: Utc::now(), + })))?; + + Ok(alg) + } + fn store_inner( &self, - mut blobs: impl Iterator>, + blobs: impl IntoIterator>, ) -> Result<(), Error> { + let mut blobs = blobs.into_iter(); self.transaction(|trans| { loop { let batch: Vec<_> = blobs @@ -249,13 +272,13 @@ trait StorageTransaction { fn complete(self: Box) -> Result<(), Error>; } -fn detect_mime(file_path: &Path) -> Result<&'static str, Error> { - let mime = mime_guess::from_path(file_path) +fn detect_mime(file_path: impl AsRef) -> Result<&'static str, Error> { + let mime = mime_guess::from_path(file_path.as_ref()) .first_raw() .unwrap_or("text/plain"); Ok(match mime { "text/plain" | "text/troff" | "text/x-markdown" | "text/x-rust" | "text/x-toml" => { - match file_path.extension().and_then(OsStr::to_str) { + match file_path.as_ref().extension().and_then(OsStr::to_str) { Some("md") => "text/markdown", Some("rs") => "text/rust", Some("markdown") => "text/markdown", diff --git a/src/test/fakes.rs b/src/test/fakes.rs index 93a63c7a4..e4058db68 100644 --- a/src/test/fakes.rs +++ b/src/test/fakes.rs @@ -14,7 +14,7 @@ pub(crate) struct FakeRelease<'a> { db: &'a TestDatabase, storage: Arc, package: MetadataPackage, - build_result: BuildResult, + builds: Vec, /// name, content source_files: Vec<(&'a str, &'a [u8])>, /// name, content @@ -28,6 +28,13 @@ pub(crate) struct FakeRelease<'a> { /// This stores the content, while `package.readme` stores the filename readme: Option<&'a str>, github_stats: Option, + doc_coverage: Option, +} + +pub(crate) struct FakeBuild { + s3_build_log: Option, + db_build_log: Option, + result: BuildResult, } const DEFAULT_CONTENT: &[u8] = @@ -68,13 +75,7 @@ impl<'a> FakeRelease<'a> { .cloned() .collect::>>(), }, - build_result: BuildResult { - rustc_version: "rustc 2.0.0-nightly (000000000 1970-01-01)".into(), - docsrs_version: "docs.rs 1.0.0 (000000000 1970-01-01)".into(), - build_log: "It works!".into(), - successful: true, - doc_coverage: None, - }, + builds: vec![], source_files: Vec::new(), rustdoc_files: Vec::new(), doc_targets: Vec::new(), @@ -89,6 +90,7 @@ impl<'a> FakeRelease<'a> { has_examples: false, readme: None, github_stats: None, + doc_coverage: None, } } @@ -129,10 +131,24 @@ impl<'a> FakeRelease<'a> { self } - pub(crate) fn build_result_successful(mut self, new: bool) -> Self { - self.has_docs = new; - self.build_result.successful = new; - self + /// Shortcut to add a single unsuccessful build with default data + // TODO: How should `has_docs` actually be handled? + pub(crate) fn build_result_failed(self) -> Self { + assert!( + self.builds.is_empty(), + "cannot use custom builds with build_result_failed" + ); + Self { + has_docs: false, + builds: vec![FakeBuild::default().successful(false)], + ..self + } + } + + pub(crate) fn builds(self, builds: Vec) -> Self { + assert!(self.builds.is_empty()); + assert!(!builds.is_empty()); + Self { builds, ..self } } pub(crate) fn yanked(mut self, new: bool) -> Self { @@ -198,20 +214,11 @@ impl<'a> FakeRelease<'a> { self } - pub(crate) fn coverage( - mut self, - documented_items: i32, - total_items: i32, - total_items_needing_examples: i32, - items_with_examples: i32, - ) -> Self { - self.build_result.doc_coverage = Some(DocCoverage { - total_items, - documented_items, - total_items_needing_examples, - items_with_examples, - }); - self + pub(crate) fn doc_coverage(self, doc_coverage: DocCoverage) -> Self { + Self { + doc_coverage: Some(doc_coverage), + ..self + } } pub(crate) fn features(mut self, features: HashMap>) -> Self { @@ -236,7 +243,7 @@ impl<'a> FakeRelease<'a> { } /// Returns the release_id - pub(crate) fn create(self) -> Result { + pub(crate) fn create(mut self) -> Result { use std::fs; use std::path::Path; @@ -286,7 +293,13 @@ impl<'a> FakeRelease<'a> { let (source_meta, mut algs) = upload_files("source", &self.source_files, None)?; log::debug!("added source files {}", source_meta); - if self.build_result.successful { + // If the test didn't add custom builds, inject a default one + if self.builds.is_empty() { + self.builds.push(FakeBuild::default()); + } + let last_build_result = &self.builds.last().unwrap().result; + + if last_build_result.successful { let index = [&package.name, "index.html"].join("/"); if package.is_library() && !rustdoc_files.iter().any(|(path, _)| path == &index) { rustdoc_files.push((&index, DEFAULT_CONTENT)); @@ -312,12 +325,13 @@ impl<'a> FakeRelease<'a> { if let Some(markdown) = self.readme { fs::write(crate_dir.join("README.md"), markdown)?; } + let default_target = self.default_target.unwrap_or(docsrs_metadata::HOST_TARGET); let release_id = crate::db::add_package_into_database( &mut db.conn(), &package, crate_dir, - &self.build_result, - self.default_target.unwrap_or("x86_64-unknown-linux-gnu"), + last_build_result, + default_target, source_meta, self.doc_targets, &self.registry_release_data, @@ -331,8 +345,10 @@ impl<'a> FakeRelease<'a> { &package.name, &self.registry_crate_data, )?; - crate::db::add_build_into_database(&mut db.conn(), release_id, &self.build_result)?; - if let Some(coverage) = self.build_result.doc_coverage { + for build in &self.builds { + build.create(&mut db.conn(), &*storage, release_id, default_target)?; + } + if let Some(coverage) = self.doc_coverage { crate::db::add_doc_coverage(&mut db.conn(), release_id, coverage)?; } @@ -363,3 +379,94 @@ impl FakeGithubStats { Ok(id) } } + +impl FakeBuild { + pub(crate) fn rustc_version(self, rustc_version: impl Into) -> Self { + Self { + result: BuildResult { + rustc_version: rustc_version.into(), + ..self.result + }, + ..self + } + } + + pub(crate) fn docsrs_version(self, docsrs_version: impl Into) -> Self { + Self { + result: BuildResult { + docsrs_version: docsrs_version.into(), + ..self.result + }, + ..self + } + } + + pub(crate) fn s3_build_log(self, build_log: impl Into) -> Self { + Self { + s3_build_log: Some(build_log.into()), + ..self + } + } + + pub(crate) fn db_build_log(self, build_log: impl Into) -> Self { + Self { + db_build_log: Some(build_log.into()), + ..self + } + } + + pub(crate) fn no_s3_build_log(self) -> Self { + Self { + s3_build_log: None, + ..self + } + } + + pub(crate) fn successful(self, successful: bool) -> Self { + Self { + result: BuildResult { + successful, + ..self.result + }, + ..self + } + } + + fn create( + &self, + conn: &mut Client, + storage: &Storage, + release_id: i32, + default_target: &str, + ) -> Result<(), Error> { + let build_id = crate::db::add_build_into_database(conn, release_id, &self.result)?; + + if let Some(db_build_log) = self.db_build_log.as_deref() { + conn.query( + "UPDATE builds SET output = $2 WHERE id = $1", + &[&build_id, &db_build_log], + )?; + } + + if let Some(s3_build_log) = self.s3_build_log.as_deref() { + let path = format!("build-logs/{}/{}.txt", build_id, default_target); + storage.store_one(path, s3_build_log)?; + } + + Ok(()) + } +} + +impl Default for FakeBuild { + fn default() -> Self { + Self { + s3_build_log: Some("It works!".into()), + db_build_log: None, + result: BuildResult { + rustc_version: "rustc 2.0.0-nightly (000000000 1970-01-01)".into(), + docsrs_version: "docs.rs 1.0.0 (000000000 1970-01-01)".into(), + successful: true, + }, + } + } +} diff --git a/src/test/mod.rs b/src/test/mod.rs index 10c021c57..5e839ecc4 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,5 +1,6 @@ mod fakes; +pub(crate) use self::fakes::FakeBuild; use crate::db::{Pool, PoolClient}; use crate::storage::{Storage, StorageKind}; use crate::web::Server; diff --git a/src/web/build_details.rs b/src/web/build_details.rs new file mode 100644 index 000000000..758a5a295 --- /dev/null +++ b/src/web/build_details.rs @@ -0,0 +1,201 @@ +use crate::{ + db::Pool, + impl_webpage, + web::{file::File, page::WebPage, MetaData, Nope}, + Config, Storage, +}; +use chrono::{DateTime, Utc}; +use iron::{IronResult, Request, Response}; +use router::Router; +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub(crate) struct BuildDetails { + id: i32, + rustc_version: String, + docsrs_version: String, + build_status: bool, + build_time: DateTime, + output: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +struct BuildDetailsPage { + metadata: MetaData, + build_details: BuildDetails, +} + +impl_webpage! { + BuildDetailsPage = "crate/build_details.html", +} + +pub fn build_details_handler(req: &mut Request) -> IronResult { + let storage = extension!(req, Storage); + let config = extension!(req, Config); + let router = extension!(req, Router); + let name = cexpect!(req, router.find("name")); + let version = cexpect!(req, router.find("version")); + let id: i32 = ctry!(req, cexpect!(req, router.find("id")).parse()); + + let mut conn = extension!(req, Pool).get()?; + + let row = ctry!( + req, + conn.query_opt( + "SELECT + builds.rustc_version, + builds.cratesfyi_version, + builds.build_status, + builds.build_time, + builds.output, + releases.default_target + FROM builds + INNER JOIN releases ON releases.id = builds.rid + INNER JOIN crates ON releases.crate_id = crates.id + WHERE builds.id = $1 AND crates.name = $2 AND releases.version = $3", + &[&id, &name, &version] + ) + ); + + let build_details = if let Some(row) = row { + let output = if let Some(output) = row.get("output") { + output + } else { + let target: String = row.get("default_target"); + let path = format!("build-logs/{}/{}.txt", id, target); + let file = ctry!(req, File::from_path(&storage, &path, &config)); + ctry!(req, String::from_utf8(file.0.content)) + }; + BuildDetails { + id, + rustc_version: row.get("rustc_version"), + docsrs_version: row.get("cratesfyi_version"), + build_status: row.get("build_status"), + build_time: row.get("build_time"), + output, + } + } else { + return Err(Nope::BuildNotFound.into()); + }; + + BuildDetailsPage { + metadata: cexpect!(req, MetaData::from_crate(&mut conn, &name, &version)), + build_details, + } + .into_response(req) +} + +#[cfg(test)] +mod tests { + use crate::test::{wrapper, FakeBuild}; + use kuchiki::traits::TendrilSink; + + #[test] + fn db_build_logs() { + wrapper(|env| { + env.fake_release() + .name("foo") + .version("0.1.0") + .builds(vec![FakeBuild::default() + .no_s3_build_log() + .db_build_log("A build log")]) + .create()?; + + let page = kuchiki::parse_html().one( + env.frontend() + .get("/crate/foo/0.1.0/builds") + .send()? + .text()?, + ); + + let node = page.select("ul > li a.release").unwrap().next().unwrap(); + let attrs = node.attributes.borrow(); + let url = attrs.get("href").unwrap(); + + let page = kuchiki::parse_html().one(env.frontend().get(url).send()?.text()?); + + let log = page.select("pre").unwrap().next().unwrap().text_contents(); + + assert!(log.contains("A build log")); + + Ok(()) + }); + } + + #[test] + fn s3_build_logs() { + wrapper(|env| { + env.fake_release() + .name("foo") + .version("0.1.0") + .builds(vec![FakeBuild::default().s3_build_log("A build log")]) + .create()?; + + let page = kuchiki::parse_html().one( + env.frontend() + .get("/crate/foo/0.1.0/builds") + .send()? + .text()?, + ); + + let node = page.select("ul > li a.release").unwrap().next().unwrap(); + let attrs = node.attributes.borrow(); + let url = attrs.get("href").unwrap(); + + let page = kuchiki::parse_html().one(env.frontend().get(url).send()?.text()?); + + let log = page.select("pre").unwrap().next().unwrap().text_contents(); + + assert!(log.contains("A build log")); + + Ok(()) + }); + } + + #[test] + fn both_build_logs() { + wrapper(|env| { + env.fake_release() + .name("foo") + .version("0.1.0") + .builds(vec![FakeBuild::default() + .s3_build_log("A build log") + .db_build_log("Another build log")]) + .create()?; + + let page = kuchiki::parse_html().one( + env.frontend() + .get("/crate/foo/0.1.0/builds") + .send()? + .text()?, + ); + + let node = page.select("ul > li a.release").unwrap().next().unwrap(); + let attrs = node.attributes.borrow(); + let url = attrs.get("href").unwrap(); + + let page = kuchiki::parse_html().one(env.frontend().get(url).send()?.text()?); + + let log = page.select("pre").unwrap().next().unwrap().text_contents(); + + // Relatively arbitrarily the DB is prioritised + assert!(log.contains("Another build log")); + + Ok(()) + }); + } + + #[test] + fn non_existing_build() { + wrapper(|env| { + env.fake_release().name("foo").version("0.1.0").create()?; + + let res = env.frontend().get("/crate/foo/0.1.0/builds/42").send()?; + assert_eq!(res.status(), 404); + // TODO: blocked on https://github.com/rust-lang/docs.rs/issues/55 + // assert!(res.text()?.contains("no such build")); + + Ok(()) + }); + } +} diff --git a/src/web/builds.rs b/src/web/builds.rs index a03c70dc5..9f16a121b 100644 --- a/src/web/builds.rs +++ b/src/web/builds.rs @@ -21,14 +21,12 @@ pub(crate) struct Build { docsrs_version: String, build_status: bool, build_time: DateTime, - output: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] struct BuildsPage { metadata: MetaData, builds: Vec, - build_details: Option, limits: Limits, } @@ -40,7 +38,6 @@ pub fn build_list_handler(req: &mut Request) -> IronResult { let router = extension!(req, Router); let name = cexpect!(req, router.find("name")); let version = cexpect!(req, router.find("version")); - let req_build_id: i32 = router.find("id").unwrap_or("0").parse().unwrap_or(0); let mut conn = extension!(req, Pool).get()?; let limits = ctry!(req, Limits::for_crate(&mut conn, name)); @@ -57,8 +54,7 @@ pub fn build_list_handler(req: &mut Request) -> IronResult { builds.rustc_version, builds.cratesfyi_version, builds.build_status, - builds.build_time, - builds.output + builds.build_time FROM builds INNER JOIN releases ON releases.id = builds.rid INNER JOIN crates ON releases.crate_id = crates.id @@ -68,36 +64,18 @@ pub fn build_list_handler(req: &mut Request) -> IronResult { ) ); - let mut build_details = None; - // FIXME: getting builds.output may cause performance issues when release have tons of builds - let mut builds = query + let builds: Vec<_> = query .into_iter() - .map(|row| { - let id: i32 = row.get("id"); - - let build = Build { - id, - rustc_version: row.get("rustc_version"), - docsrs_version: row.get("cratesfyi_version"), - build_status: row.get("build_status"), - build_time: row.get("build_time"), - output: row.get("output"), - }; - - if id == req_build_id { - build_details = Some(build.clone()); - } - - build + .map(|row| Build { + id: row.get("id"), + rustc_version: row.get("rustc_version"), + docsrs_version: row.get("cratesfyi_version"), + build_status: row.get("build_status"), + build_time: row.get("build_time"), }) - .collect::>(); + .collect(); if req.url.path().join("/").ends_with(".json") { - // Remove build output from build list for json output - for build in builds.iter_mut() { - build.output = None; - } - let mut resp = Response::with((status::Ok, serde_json::to_string(&builds).unwrap())); resp.headers.set(ContentType::json()); resp.headers.set(Expires(HttpDate(time::now()))); @@ -113,9 +91,189 @@ pub fn build_list_handler(req: &mut Request) -> IronResult { BuildsPage { metadata: cexpect!(req, MetaData::from_crate(&mut conn, &name, &version)), builds, - build_details, limits, } .into_response(req) } } + +#[cfg(test)] +mod tests { + use crate::test::{wrapper, FakeBuild}; + use chrono::{DateTime, Duration, Utc}; + use kuchiki::traits::TendrilSink; + + #[test] + fn build_list() { + wrapper(|env| { + env.fake_release() + .name("foo") + .version("0.1.0") + .builds(vec![ + FakeBuild::default() + .rustc_version("rustc 1.0.0") + .docsrs_version("docs.rs 1.0.0"), + FakeBuild::default() + .successful(false) + .rustc_version("rustc 2.0.0") + .docsrs_version("docs.rs 2.0.0"), + FakeBuild::default() + .rustc_version("rustc 3.0.0") + .docsrs_version("docs.rs 3.0.0"), + ]) + .create()?; + + let page = kuchiki::parse_html().one( + env.frontend() + .get("/crate/foo/0.1.0/builds") + .send()? + .text()?, + ); + + let rows: Vec<_> = page + .select("ul > li a.release") + .unwrap() + .map(|row| row.text_contents()) + .collect(); + + assert!(rows[0].contains("rustc 3.0.0")); + assert!(rows[0].contains("docs.rs 3.0.0")); + assert!(rows[1].contains("rustc 2.0.0")); + assert!(rows[1].contains("docs.rs 2.0.0")); + assert!(rows[2].contains("rustc 1.0.0")); + assert!(rows[2].contains("docs.rs 1.0.0")); + + Ok(()) + }); + } + + #[test] + fn build_list_json() { + wrapper(|env| { + env.fake_release() + .name("foo") + .version("0.1.0") + .builds(vec![ + FakeBuild::default() + .rustc_version("rustc 1.0.0") + .docsrs_version("docs.rs 1.0.0"), + FakeBuild::default() + .successful(false) + .rustc_version("rustc 2.0.0") + .docsrs_version("docs.rs 2.0.0"), + FakeBuild::default() + .rustc_version("rustc 3.0.0") + .docsrs_version("docs.rs 3.0.0"), + ]) + .create()?; + + let value: serde_json::Value = serde_json::from_str( + &env.frontend() + .get("/crate/foo/0.1.0/builds.json") + .send()? + .text()?, + )?; + + assert_eq!(value.pointer("/0/build_status"), Some(&true.into())); + assert_eq!( + value.pointer("/0/docsrs_version"), + Some(&"docs.rs 3.0.0".into()) + ); + assert_eq!( + value.pointer("/0/rustc_version"), + Some(&"rustc 3.0.0".into()) + ); + assert!(value.pointer("/0/id").unwrap().is_i64()); + assert!(serde_json::from_value::>( + value.pointer("/0/build_time").unwrap().clone() + ) + .is_ok()); + + assert_eq!(value.pointer("/1/build_status"), Some(&false.into())); + assert_eq!( + value.pointer("/1/docsrs_version"), + Some(&"docs.rs 2.0.0".into()) + ); + assert_eq!( + value.pointer("/1/rustc_version"), + Some(&"rustc 2.0.0".into()) + ); + assert!(value.pointer("/1/id").unwrap().is_i64()); + assert!(serde_json::from_value::>( + value.pointer("/1/build_time").unwrap().clone() + ) + .is_ok()); + + assert_eq!(value.pointer("/2/build_status"), Some(&true.into())); + assert_eq!( + value.pointer("/2/docsrs_version"), + Some(&"docs.rs 1.0.0".into()) + ); + assert_eq!( + value.pointer("/2/rustc_version"), + Some(&"rustc 1.0.0".into()) + ); + assert!(value.pointer("/2/id").unwrap().is_i64()); + assert!(serde_json::from_value::>( + value.pointer("/2/build_time").unwrap().clone() + ) + .is_ok()); + + assert!( + value.pointer("/1/build_time").unwrap().as_str().unwrap() + < value.pointer("/0/build_time").unwrap().as_str().unwrap() + ); + assert!( + value.pointer("/2/build_time").unwrap().as_str().unwrap() + < value.pointer("/1/build_time").unwrap().as_str().unwrap() + ); + + Ok(()) + }); + } + + #[test] + fn limits() { + wrapper(|env| { + env.fake_release().name("foo").version("0.1.0").create()?; + + env.db().conn().query( + "INSERT INTO sandbox_overrides + (crate_name, max_memory_bytes, timeout_seconds, max_targets) + VALUES ($1, $2, $3, $4)", + &[ + &"foo", + &3072i64, + &(Duration::hours(2).num_seconds() as i32), + &1, + ], + )?; + + let page = kuchiki::parse_html().one( + env.frontend() + .get("/crate/foo/0.1.0/builds") + .send()? + .text()?, + ); + + let header = page.select(".about h4").unwrap().next().unwrap(); + assert_eq!(header.text_contents(), "foo's sandbox limits"); + + let values: Vec<_> = page + .select(".about table tr td:last-child") + .unwrap() + .map(|row| row.text_contents()) + .collect(); + let values: Vec<_> = values.iter().map(|v| &**v).collect(); + + dbg!(&values); + assert!(values.contains(&"3 KB")); + assert!(values.contains(&"2 hours")); + assert!(values.contains(&"100 KB")); + assert!(values.contains(&"blocked")); + assert!(values.contains(&"1")); + + Ok(()) + }); + } +} diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs index e49e13304..553f7ecc5 100644 --- a/src/web/crate_details.rs +++ b/src/web/crate_details.rs @@ -361,7 +361,7 @@ mod tests { env.fake_release() .name("foo") .version("0.0.3") - .build_result_successful(false) + .build_result_failed() .create()?; env.fake_release() .name("foo") @@ -371,7 +371,7 @@ mod tests { env.fake_release() .name("foo") .version("0.0.5") - .build_result_successful(false) + .build_result_failed() .yanked(true) .create()?; @@ -392,12 +392,12 @@ mod tests { env.fake_release() .name("foo") .version("0.0.1") - .build_result_successful(false) + .build_result_failed() .create()?; env.fake_release() .name("foo") .version("0.0.2") - .build_result_successful(false) + .build_result_failed() .create()?; env.fake_release() .name("foo") @@ -421,7 +421,7 @@ mod tests { env.fake_release() .name("foo") .version("0.0.2") - .build_result_successful(false) + .build_result_failed() .create()?; env.fake_release() .name("foo") @@ -449,7 +449,7 @@ mod tests { env.fake_release() .name("foo") .version("0.3.0") - .build_result_successful(false) + .build_result_failed() .create()?; env.fake_release().name("foo").version("1.0.0").create()?; env.fake_release().name("foo").version("0.12.0").create()?; @@ -465,7 +465,7 @@ mod tests { env.fake_release() .name("foo") .version("0.0.1") - .build_result_successful(false) + .build_result_failed() .binary(true) .create()?; diff --git a/src/web/error.rs b/src/web/error.rs index 58a3c3cbd..3bb8bb0b2 100644 --- a/src/web/error.rs +++ b/src/web/error.rs @@ -9,6 +9,7 @@ use std::{error::Error, fmt}; #[derive(Debug, Copy, Clone)] pub enum Nope { ResourceNotFound, + BuildNotFound, CrateNotFound, VersionNotFound, NoResults, @@ -19,6 +20,7 @@ impl fmt::Display for Nope { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(match *self { Nope::ResourceNotFound => "Requested resource not found", + Nope::BuildNotFound => "Requested build not found", Nope::CrateNotFound => "Requested crate not found", Nope::VersionNotFound => "Requested crate does not have specified version", Nope::NoResults => "Search yielded no results", @@ -35,6 +37,7 @@ impl From for IronError { let status = match err { Nope::ResourceNotFound + | Nope::BuildNotFound | Nope::CrateNotFound | Nope::VersionNotFound | Nope::NoResults => status::NotFound, @@ -59,6 +62,13 @@ impl Handler for Nope { .into_response(req) } + Nope::BuildNotFound => ErrorPage { + title: "The requested build does not exist", + message: Some("no such build".into()), + status: Status::NotFound, + } + .into_response(req), + Nope::CrateNotFound => { // user tried to navigate to a crate that doesn't exist // TODO: Display the attempted crate and a link to a search for said crate diff --git a/src/web/metrics.rs b/src/web/metrics.rs index 78180333e..b4b6fcf71 100644 --- a/src/web/metrics.rs +++ b/src/web/metrics.rs @@ -146,7 +146,7 @@ mod tests { env.fake_release() .name("rcc") .version("1.0.0") - .build_result_successful(false) + .build_result_failed() .create()?; env.fake_release() .name("hexponent") diff --git a/src/web/mod.rs b/src/web/mod.rs index 6e3d9be23..97a4290c1 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -77,6 +77,7 @@ macro_rules! extension { }}; } +mod build_details; mod builds; mod crate_details; mod error; @@ -591,7 +592,7 @@ impl_webpage! { #[cfg(test)] mod test { use super::*; - use crate::{test::*, web::match_version}; + use crate::{docbuilder::DocCoverage, test::*, web::match_version}; use kuchiki::traits::TendrilSink; use serde_json::json; @@ -643,7 +644,12 @@ mod test { .name("foo") .version("0.0.1") .source_file("test.rs", &[]) - .coverage(6, 10, 2, 1) + .doc_coverage(DocCoverage { + total_items: 10, + documented_items: 6, + total_items_needing_examples: 2, + items_with_examples: 1, + }) .create()?; let web = env.frontend(); diff --git a/src/web/releases.rs b/src/web/releases.rs index ef59cbc38..aff0b3851 100644 --- a/src/web/releases.rs +++ b/src/web/releases.rs @@ -750,7 +750,7 @@ mod tests { env.fake_release() .name("fool") .version("0.0.0") - .build_result_successful(false) + .build_result_failed() .create()?; env.fake_release() .name("freakin") @@ -810,7 +810,7 @@ mod tests { env.fake_release() .name("regex") .version("0.0.0") - .build_result_successful(false) + .build_result_failed() .create()?; let (num_results, results) = get_search_results(&mut db.conn(), "regex", 1, 100)?; @@ -1076,7 +1076,7 @@ mod tests { env.fake_release() .name("crate_that_failed") .version("0.1.0") - .build_result_successful(false) + .build_result_failed() .create()?; let page = kuchiki::parse_html().one(env.frontend().get(path).send()?.text()?); let releases: Vec<_> = page.select("a.release").expect("missing heading").collect(); @@ -1151,7 +1151,7 @@ mod tests { env.fake_release().name("some_random_crate").create()?; env.fake_release() .name("some_random_crate_that_failed") - .build_result_successful(false) + .build_result_failed() .create()?; assert_success("/releases/feed", web) }) diff --git a/src/web/routes.rs b/src/web/routes.rs index dbe888bd1..e5e3116ab 100644 --- a/src/web/routes.rs +++ b/src/web/routes.rs @@ -89,7 +89,7 @@ pub(super) fn build_routes() -> Routes { ); routes.internal_page( "/crate/:name/:version/builds/:id", - super::builds::build_list_handler, + super::build_details::build_details_handler, ); routes.internal_page( "/crate/:name/:version/features", diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index 0326f1d7b..253901c46 100644 --- a/src/web/rustdoc.rs +++ b/src/web/rustdoc.rs @@ -693,7 +693,6 @@ mod test { env.fake_release() .name("buggy") .version("0.1.0") - .build_result_successful(true) .rustdoc_file("settings.html") .rustdoc_file("directory_1/index.html") .rustdoc_file("directory_2.html/index.html") @@ -704,7 +703,7 @@ mod test { env.fake_release() .name("buggy") .version("0.2.0") - .build_result_successful(false) + .build_result_failed() .create()?; let web = env.frontend(); assert_success("/", web)?; @@ -873,7 +872,7 @@ mod test { env.fake_release() .name("dummy") .version("0.2.0") - .build_result_successful(false) + .build_result_failed() .create()?; let web = env.frontend(); @@ -1533,7 +1532,7 @@ mod test { env.fake_release() .name("hexponent") .version("0.2.0") - .build_result_successful(false) + .build_result_failed() .create()?; let web = env.frontend(); diff --git a/src/web/sitemap.rs b/src/web/sitemap.rs index c08b167e0..8bcc93467 100644 --- a/src/web/sitemap.rs +++ b/src/web/sitemap.rs @@ -193,7 +193,7 @@ mod tests { env.fake_release().name("some_random_crate").create()?; env.fake_release() .name("some_random_crate_that_failed") - .build_result_successful(false) + .build_result_failed() .create()?; // these fake crates appear only in the `s` sitemap diff --git a/templates/crate/build_details.html b/templates/crate/build_details.html new file mode 100644 index 000000000..3ab61ff90 --- /dev/null +++ b/templates/crate/build_details.html @@ -0,0 +1,42 @@ +{%- extends "base.html" -%} +{%- import "header/package_navigation.html" as navigation -%} + +{%- block title -%} + {{ macros::doc_title(name=metadata.name, version=metadata.version) }} +{%- endblock title -%} + +{%- block topbar -%} + {%- set latest_version = "" -%} + {%- set latest_path = "" -%} + {%- set target = "" -%} + {%- set inner_path = metadata.target_name ~ "/index.html" -%} + {%- set is_latest_version = true -%} + {%- set is_prerelease = false -%} + {%- include "rustdoc/topbar.html" -%} +{%- endblock topbar -%} + +{%- block header -%} + {{ navigation::package_navigation(metadata=metadata, active_tab="builds") }} +{%- endblock header -%} + +{%- block body -%} +
+
+
+ Build #{{ build_details.id }} {{ build_details.build_time | date(format="%+") }} +
+ + {%- filter dedent -%} +
+                    # rustc version
+                    {{ build_details.rustc_version }}
+                    # docs.rs version
+                    {{ build_details.docsrs_version }}
+
+                    # build log
+                    {{ build_details.output }}
+                
+ {%- endfilter -%} +
+
+{%- endblock body -%} diff --git a/templates/crate/builds.html b/templates/crate/builds.html index e3b011df2..9220c51f9 100644 --- a/templates/crate/builds.html +++ b/templates/crate/builds.html @@ -22,26 +22,6 @@ {%- block body -%}
- {# If there is a build log then show it #} - {# TODO: When viewing a build log, show a back button or a hide button #} - {%- if build_details -%} -
- Build #{{ build_details.id }} {{ build_details.build_time | date(format="%+") }} -
- - {%- filter dedent -%} -
-                        # rustc version
-                        {{ build_details.rustc_version }}
-                        # docs.rs version
-                        {{ build_details.docsrs_version }}
-
-                        # build log
-                        {{ build_details.output }}
-                    
- {%- endfilter -%} - {%- endif -%} -
Builds