Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 20 additions & 196 deletions crates/bin/docs_rs_web/src/handlers/crate_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ use crate::{
page::templates::{RenderBrands, RenderRegular, RenderSolid, filters},
utils::{get_correct_docsrs_style_file, licenses},
};
use anyhow::{Context, Result, anyhow};
use anyhow::{Context, Result};
use askama::Template;
use axum::{
extract::Extension,
response::{IntoResponse, Response as AxumResponse},
};
use chrono::{DateTime, Utc};
use docs_rs_cargo_metadata::{Dependency, ReleaseDependencyList};
use docs_rs_database::crate_details::{Release, latest_release, parse_doc_targets};
use docs_rs_database::crate_details::{Release, parse_doc_targets};
use docs_rs_headers::CanonicalUrl;
use docs_rs_registry_api::OwnerKind;
use docs_rs_storage::{AsyncStorage, PathNotFoundError};
Expand Down Expand Up @@ -338,9 +338,9 @@ impl CrateDetails {
}

/// Returns the latest non-yanked, non-prerelease release of this crate (or latest
/// yanked/prereleased if that is all that exist).
pub fn latest_release(&self) -> Result<&Release> {
latest_release(&self.releases).ok_or_else(|| anyhow!("crate without releases"))
/// prereleased if that is all that exist). Returns `None` if every release is yanked.
pub fn latest_release(&self) -> Option<&Release> {
docs_rs_database::crate_details::latest_release(&self.releases)
}
}

Expand Down Expand Up @@ -660,10 +660,13 @@ pub(crate) async fn get_all_platforms_inner(
.into_response());
}

let latest_release = latest_release(&matched_release.all_releases)
.expect("we couldn't end up here without releases");
let latest_release =
docs_rs_database::crate_details::latest_release(&matched_release.all_releases);

let current_target = if latest_release.build_status.is_success() {
let current_target = if latest_release
.map(|r| r.build_status.is_success())
.unwrap_or(false)
{
params
.doc_target_or_default()
.unwrap_or_default()
Expand Down Expand Up @@ -1174,192 +1177,6 @@ mod tests {
})
}

#[test]
fn test_latest_version() {
async_wrapper(|env| async move {
let db = env.pool()?;

env.fake_release()
.await
.name("foo")
.version("0.0.1")
.create()
.await?;
env.fake_release()
.await
.name("foo")
.version("0.0.3")
.create()
.await?;
env.fake_release()
.await
.name("foo")
.version("0.0.2")
.create()
.await?;

let mut conn = db.get_async().await?;
for version in &["0.0.1", "0.0.2", "0.0.3"] {
let details = crate_details(&mut conn, "foo", *version, None).await;
assert_eq!(
details.latest_release().unwrap().version,
Version::parse("0.0.3")?
);
}

Ok(())
})
}

#[test]
fn test_latest_version_ignores_prerelease() {
async_wrapper(|env| async move {
let db = env.pool()?;

env.fake_release()
.await
.name("foo")
.version("0.0.1")
.create()
.await?;
env.fake_release()
.await
.name("foo")
.version("0.0.3-pre.1")
.create()
.await?;
env.fake_release()
.await
.name("foo")
.version("0.0.2")
.create()
.await?;

let mut conn = db.get_async().await?;
for &version in &["0.0.1", "0.0.2", "0.0.3-pre.1"] {
let details = crate_details(&mut conn, "foo", version, None).await;
assert_eq!(
details.latest_release().unwrap().version,
Version::parse("0.0.2")?
);
}

Ok(())
})
}

#[test]
fn test_latest_version_ignores_yanked() {
async_wrapper(|env| async move {
let db = env.pool()?;

env.fake_release()
.await
.name("foo")
.version("0.0.1")
.create()
.await?;
env.fake_release()
.await
.name("foo")
.version("0.0.3")
.yanked(true)
.create()
.await?;
env.fake_release()
.await
.name("foo")
.version("0.0.2")
.create()
.await?;

let mut conn = db.get_async().await?;
for &version in &["0.0.1", "0.0.2", "0.0.3"] {
let details = crate_details(&mut conn, "foo", version, None).await;
assert_eq!(
details.latest_release().unwrap().version,
Version::parse("0.0.2")?
);
}

Ok(())
})
}

#[test]
fn test_latest_version_only_yanked() {
async_wrapper(|env| async move {
let db = env.pool()?;

env.fake_release()
.await
.name("foo")
.version("0.0.1")
.yanked(true)
.create()
.await?;
env.fake_release()
.await
.name("foo")
.version("0.0.3")
.yanked(true)
.create()
.await?;
env.fake_release()
.await
.name("foo")
.version("0.0.2")
.yanked(true)
.create()
.await?;

let mut conn = db.get_async().await?;
for &version in &["0.0.1", "0.0.2", "0.0.3"] {
let details = crate_details(&mut conn, "foo", version, None).await;
assert_eq!(
details.latest_release().unwrap().version,
Version::parse("0.0.3")?
);
}

Ok(())
})
}

#[test]
fn test_latest_version_in_progress() {
async_wrapper(|env| async move {
let db = env.pool()?;

env.fake_release()
.await
.name("foo")
.version("0.0.1")
.create()
.await?;
env.fake_release()
.await
.name("foo")
.version("0.0.2")
.builds(vec![
FakeBuild::default().build_status(BuildStatus::InProgress),
])
.create()
.await?;

let mut conn = db.get_async().await?;
for &version in &["0.0.1", "0.0.2"] {
let details = crate_details(&mut conn, "foo", version, None).await;
assert_eq!(
details.latest_release().unwrap().version,
Version::parse("0.0.1")?
);
}

Ok(())
})
}

#[test]
fn releases_dropdowns_show_binary_warning() {
async_wrapper(|env| async move {
Expand Down Expand Up @@ -1411,11 +1228,18 @@ mod tests {
.create()
.await?;

let response = env.web_app().await.get("/crate/foo/latest").await?;
let response = env
.web_app()
.await
.assert_success("/crate/foo/0.1.0")
.await?;

let page = kuchikiki::parse_html().one(response.text().await?);
// multiple `a.pure-menu-link` elements share this href (the topbar's
// `crate-name` link and the navigation "Crate" tab); the version dropdown's
// entry is the only one rendered with `rel="nofollow"` by the macro.
let link = page
.select_first("a.pure-menu-link[href='/crate/foo/0.1.0']")
.select_first("a.pure-menu-link[href='/crate/foo/0.1.0'][rel='nofollow']")
.unwrap();

assert_eq!(
Expand Down
9 changes: 5 additions & 4 deletions crates/bin/docs_rs_web/src/handlers/releases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1490,16 +1490,17 @@ mod tests {

let links = get_release_links("/releases/search?query=some_random_crate", &web).await?;

// `some_other_crate` won't be shown since we don't have it yet
assert_eq!(links.len(), 4);
// `some_other_crate` won't be shown since we don't have it yet.
// `yet_another_crate` only has a yanked release, so it has no canonical "latest"
// and renders as "Documentation not available on docs.rs" (no anchor).
assert_eq!(links.len(), 3);
// * `max_version` from the crates.io search result will be ignored since we
// might not have it yet, or the doc-build might be in progress.
// * ranking/order from crates.io result is preserved
// * version used is the highest semver following our own "latest version" logic
assert_eq!(links[0], "/some_random_crate/latest/some_random_crate/");
assert_eq!(links[1], "/and_another_one/latest/and_another_one/");
assert_eq!(links[2], "/yet_another_crate/0.1.0/yet_another_crate/");
assert_eq!(links[3], "/crate/failed_hard/0.1.0");
assert_eq!(links[2], "/crate/failed_hard/0.1.0");
Ok(())
}

Expand Down
52 changes: 49 additions & 3 deletions crates/bin/docs_rs_web/src/handlers/rustdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,10 +730,12 @@ pub(crate) async fn rustdoc_html_server_handler(
);
}

let latest_release = krate.latest_release()?;
let latest_release = krate.latest_release();

// Get the latest version of the crate
let latest_version = latest_release.version.clone();
let latest_version = latest_release
.map(|release| release.version.clone())
.unwrap_or_else(|| krate.version.clone());
let is_latest_version = latest_version == krate.version;
let is_prerelease = !(krate.version.pre.is_empty());

Expand All @@ -744,7 +746,7 @@ pub(crate) async fn rustdoc_html_server_handler(
.rustdoc_url()
.append_raw_query(original_query.as_deref());

let latest_path = if latest_release.build_status.is_success() {
let latest_path = if latest_release.is_some_and(|release| release.build_status.is_success()) {
params
.clone()
.with_req_version(&ReqVersion::Latest)
Expand Down Expand Up @@ -1623,6 +1625,50 @@ mod test {
})
}

#[test_case(true)]
#[test_case(false)]
fn no_latest_stable_button_when_latest_stable_is_yanked(archive_storage: bool) {
async fn has_latest_redirect_button(
path: &str,
web: &axum::Router,
) -> Result<bool, anyhow::Error> {
web.assert_success(path).await?;
let data = web.get(path).await?.text().await?;
Ok(kuchikiki::parse_html()
.one(data)
.select("form > ul > li > a.warn")
.expect("invalid selector")
.next()
.is_some())
}

async_wrapper(|env| async move {
env.fake_release()
.await
.name("dummy")
.version("0.2.0-pre.1")
.archive_storage(archive_storage)
.rustdoc_file("dummy/index.html")
.create()
.await?;
env.fake_release()
.await
.name("dummy")
.version("0.2.0")
.archive_storage(archive_storage)
.rustdoc_file("dummy/index.html")
.yanked(true)
.create()
.await?;

let web = env.web_app().await;
assert!(!has_latest_redirect_button("/dummy/latest/dummy/", &web).await?);
assert!(!has_latest_redirect_button("/dummy/0.2.0-pre.1/dummy/", &web).await?);

Ok(())
})
}

#[test_case(true)]
#[test_case(false)]
fn yanked_release_shows_warning_in_nav(archive_storage: bool) {
Expand Down
Loading
Loading