Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command: criticalup doc #78

Merged
merged 13 commits into from
Jan 27, 2025
348 changes: 326 additions & 22 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions crates/criticalup-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,25 @@ clap = { version = "4.5.21", features = ["std", "derive", "help", "usage"] }
criticaltrust = { path = "../criticaltrust" }
criticalup-core = { path = "../criticalup-core" }
futures.workspace = true
opener = { version = "0.7.2"}
amanjeev marked this conversation as resolved.
Show resolved Hide resolved
serde_json = "1.0.132"
tar = "0.4.43"
thiserror = "2.0.3"
xz2 = "0.1.7"
tempfile.workspace = true
thiserror = "2.0.3"
time = { version = "0.3.36", features = ["std", "serde", "serde-well-known", "macros"] }
tokio.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
time = { version = "0.3.36", features = ["std", "serde", "serde-well-known", "macros"] }
url = "2.5.4"
walkdir.workspace = true
xz2 = "0.1.7"

[dev-dependencies]
insta = { version = "1.41.1", features = ["filters"] }
mock-download-server = { path = "../mock-download-server" }
regex = "1.11.1"
serde = { version = "1.0.215", features = ["derive"] }
tempfile.workspace = true
regex = "1.11.1"

[target.x86_64-pc-windows-msvc.dependencies]
windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_System_Console"] }
Expand Down
55 changes: 55 additions & 0 deletions crates/criticalup-cli/src/commands/doc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: The Ferrocene Developers
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::errors::Error;
use crate::Context;
use criticalup_core::project_manifest::ProjectManifest;
use std::path::PathBuf;
use url::Url;

pub(crate) async fn run(
ctx: &Context,
project: Option<PathBuf>,
path_only: bool,
) -> Result<(), Error> {
// Parse and serialize the project manifest.
let manifest = ProjectManifest::get(project).await?;
let installation_dir = &ctx.config.paths.installation_dir;

for product in manifest.products() {
let doc_package_exists_in_manifest =
product.packages().contains(&"ferrocene-docs".to_string());
let abs_ferrocene_html_doc_path = installation_dir
.join(product.installation_id())
.join("share/doc/")
.join(product.name())
.join("html/index.html");

if !doc_package_exists_in_manifest || !abs_ferrocene_html_doc_path.exists() {
return Err(Error::MissingDocPackage());
}

// Path to the doc root can be clickable so we try to print that.
match Url::from_file_path(abs_ferrocene_html_doc_path.clone()) {
Ok(url) => {
let url = url.to_string();
if path_only {
println!("{}", url);
} else {
// Open in the default browser.
tracing::info!(
"Opening docs in your browser for product '{}'.",
product.name()
);
opener::open_browser(abs_ferrocene_html_doc_path.clone())
.map_err(|err| Error::FailedToOpenDoc { url, kind: err })?
}
}
Err(_) => {
println!("{}", abs_ferrocene_html_doc_path.display());
}
}
}

Ok(())
}
1 change: 1 addition & 0 deletions crates/criticalup-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) mod auth;
pub(crate) mod auth_remove;
pub(crate) mod auth_set;
pub(crate) mod clean;
pub(crate) mod doc;
pub(crate) mod install;
pub(crate) mod remove;
pub(crate) mod run;
Expand Down
13 changes: 13 additions & 0 deletions crates/criticalup-cli/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ pub(crate) enum Error {
tracing_subscriber::util::TryInitError,
),

#[error("Failed to open document in the browser at {}", url)]
FailedToOpenDoc {
url: String,
#[source]
kind: opener::OpenError,
},

#[error(
"Package 'ferrocene-docs' is not installed for this project. Please add the package \
'ferrocene-docs' to the project manifest and run 'criticalup install' again."
)]
MissingDocPackage(),

#[error("Failed to run install command.")]
RevocationSignatureExpired(#[source] criticaltrust::Error),
#[error("Failed to install package '{}'.", .0)]
Expand Down
11 changes: 11 additions & 0 deletions crates/criticalup-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ async fn main_inner(whitelabel: WhitelabelConfig, args: &[OsString]) -> Result<(
project,
out,
} => commands::archive::run(&ctx, project.as_ref(), offline, out.as_ref()).await?,
Commands::Doc { project, path } => commands::doc::run(&ctx, project, path).await?,
}

Ok(())
Expand Down Expand Up @@ -212,6 +213,16 @@ enum Commands {
#[arg()]
out: Option<PathBuf>,
},

/// Open the documentation for the current toolchain
Doc {
/// Path to the manifest `criticalup.toml`
#[arg(long)]
project: Option<PathBuf>,
/// Only print the path to the documentation location
#[arg(long)]
path: bool,
},
}

#[derive(Debug, Subcommand, Clone)]
Expand Down
125 changes: 125 additions & 0 deletions crates/criticalup-cli/tests/cli/doc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: The Ferrocene Developers
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::assert_output;
use crate::utils::{construct_toolchains_product_path, TestEnvironment};
use criticalup_core::project_manifest::ProjectManifest;
use std::env;
use std::fs::File;

#[tokio::test]
async fn help_message() {
let test_env = TestEnvironment::prepare().await;
assert_output!(test_env.cmd().args(["doc", "--help"]));
}

#[tokio::test]
async fn show_path_only() {
let test_env = TestEnvironment::prepare().await;
let mut current_dir =
std::env::current_dir().expect("could not read current directory in the test.");
current_dir.push("tests/resources/criticalup-doc.toml");

let manifest_path = current_dir.to_str().expect("conversion to str failed");

// Generate the manifest object so we can get the installation id hash.
let p = ProjectManifest::load(current_dir.as_path()).expect("could not load project manifest");
let id_hash = p.products()[0].installation_id().0;

// Manually create the toolchain directory which allows us to skip installation.
// TODO: when tests for `install` command are up, use that instead of manual creation.
let product_toolchain_dir = construct_toolchains_product_path(&test_env, id_hash.as_str());
let product_toolchain_doc_dir = product_toolchain_dir.join("share/doc/ferrocene/html");
std::fs::create_dir_all(&product_toolchain_doc_dir)
.expect("could not create product directory");

// Create a file "index.html" in the doc dir to mimic docs.
let _ = File::create(product_toolchain_doc_dir.join("index.html")).unwrap();

assert_output!(test_env
.cmd()
.args(["doc", "--path", "--project", manifest_path]));
}

#[tokio::test]
async fn error_no_file() {
let test_env = TestEnvironment::prepare().await;
let mut current_dir =
std::env::current_dir().expect("could not read current directory in the test.");
current_dir.push("tests/resources/criticalup-doc.toml");

let manifest_path = current_dir.to_str().expect("conversion to str failed");

// Generate the manifest object so we can get the installation id hash.
let p = ProjectManifest::load(current_dir.as_path()).expect("could not load project manifest");
let id_hash = p.products()[0].installation_id().0;

// Manually create the toolchain directory which allows us to skip installation.
// TODO: when tests for `install` command are up, use that instead of manual creation.
let product_toolchain_dir = construct_toolchains_product_path(&test_env, id_hash.as_str());
let product_toolchain_doc_dir = product_toolchain_dir.join("share/doc/ferrocene/html");
std::fs::create_dir_all(&product_toolchain_doc_dir)
.expect("could not create product directory");

assert_output!(test_env
.cmd()
.args(["doc", "--path", "--project", manifest_path]));
}

#[tokio::test]
async fn error_no_package() {
let test_env = TestEnvironment::prepare().await;
let mut current_dir =
std::env::current_dir().expect("could not read current directory in the test.");
current_dir.push("tests/resources/criticalup-doc-no-package.toml");

let manifest_path = current_dir.to_str().expect("conversion to str failed");

// Generate the manifest object so we can get the installation id hash.
let p = ProjectManifest::load(current_dir.as_path()).expect("could not load project manifest");
let id_hash = p.products()[0].installation_id().0;

// Manually create the toolchain directory which allows us to skip installation.
// TODO: when tests for `install` command are up, use that instead of manual creation.
let product_toolchain_dir = construct_toolchains_product_path(&test_env, id_hash.as_str());
let product_toolchain_doc_dir = product_toolchain_dir.join("share/doc/ferrocene/html");
std::fs::create_dir_all(&product_toolchain_doc_dir)
.expect("could not create product directory");

// Create a file "index.html" in the doc dir to mimic docs.
let _ = File::create(product_toolchain_doc_dir.join("index.html")).unwrap();

// Even if the file is available on the disk, it should show error because the manifest
// does not have the 'ferrocene-docs' package.
assert_output!(test_env
.cmd()
.args(["doc", "--path", "--project", manifest_path]));
}

#[tokio::test]
async fn error_opening_browser() {
let test_env = TestEnvironment::prepare().await;
let mut current_dir =
std::env::current_dir().expect("could not read current directory in the test.");
current_dir.push("tests/resources/criticalup-doc.toml");

let manifest_path = current_dir.to_str().expect("conversion to str failed");

// Generate the manifest object so we can get the installation id hash.
let p = ProjectManifest::load(current_dir.as_path()).expect("could not load project manifest");
let id_hash = p.products()[0].installation_id().0;

// Manually create the toolchain directory which allows us to skip installation.
// TODO: when tests for `install` command are up, use that instead of manual creation.
let product_toolchain_dir = construct_toolchains_product_path(&test_env, id_hash.as_str());
let product_toolchain_doc_dir = product_toolchain_dir.join("share/doc/ferrocene/html");
std::fs::create_dir_all(&product_toolchain_doc_dir)
.expect("could not create product directory");

// Create a file "index.html" in the doc dir to mimic docs.
let _ = File::create(product_toolchain_doc_dir.join("index.html")).unwrap();

// Override BROWSER to something that will fail so we can see the errors.
env::set_var("BROWSER", "THEREISNOSUCHBROWSER");
assert_output!(test_env.cmd().args(["doc", "--project", manifest_path]));
}
1 change: 1 addition & 0 deletions crates/criticalup-cli/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod auth_remove;
mod auth_set;
mod binary_proxies;
mod clean;
mod doc;
mod install;
mod remove;
mod root;
Expand Down
18 changes: 18 additions & 0 deletions crates/criticalup-cli/tests/cli/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ macro_rules! assert_output {
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../snapshots");

#[cfg(target_os = "windows")]
settings.add_filter(
r"[a-zA-Z]:\\.*\\toolchains\\(?<ins_id>[_a-zA-Z0-9]+)\\.*html.*",
"/path/to/toolchain/installation/$ins_id/share/doc/ferrocene/html/index.html",
);

#[cfg(target_os = "windows")]
settings.add_filter(
r"file:.*[a-zA-Z]:.*toolchains/(?<ins_id>[_a-zA-Z0-9]+)/share/doc/ferrocene.*",
"file:/path/to/toolchain/installation/$ins_id/share/doc/ferrocene/html/index.html",
);

// using tempfile in tests changes the output tmp dir on every run
// so, this is to normalize the data first
#[cfg(target_os = "linux")]
Expand All @@ -189,6 +201,12 @@ macro_rules! assert_output {
"error: No such file or directory (os error 2)",
);

#[cfg(windows)]
settings.add_filter(
r"caused by: program not found",
"caused by: No such file or directory (os error 2)",
);

#[cfg(windows)]
settings.add_filter(
r"caused by: The system cannot find the path specified\. \(os error 3\)",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: The Ferrocene Developers
# SPDX-License-Identifier: MIT OR Apache-2.0

# Manifest for test
manifest-version = 1

[products.ferrocene]
release = "stable-2024.11.0"
packages = [
"rustc-x86_64-unknown-linux-gnu",
]
12 changes: 12 additions & 0 deletions crates/criticalup-cli/tests/resources/criticalup-doc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: The Ferrocene Developers
# SPDX-License-Identifier: MIT OR Apache-2.0

# Manifest for test
manifest-version = 1

[products.ferrocene]
release = "stable-2024.11.0"
packages = [
"rustc-x86_64-unknown-linux-gnu",
"ferrocene-docs",
]
12 changes: 12 additions & 0 deletions crates/criticalup-cli/tests/snapshots/cli__doc__error_no_file.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: crates/criticalup-cli/tests/cli/doc.rs
expression: repr
---
exit: exit status: 1

empty stdout

stderr
------
error: Package 'ferrocene-docs' is not installed for this project. Please add the package 'ferrocene-docs' to the project manifest and run 'criticalup install' again.
------
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: crates/criticalup-cli/tests/cli/doc.rs
expression: repr
---
exit: exit status: 1

empty stdout

stderr
------
error: Package 'ferrocene-docs' is not installed for this project. Please add the package 'ferrocene-docs' to the project manifest and run 'criticalup install' again.
------
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: crates/criticalup-cli/tests/cli/doc.rs
expression: repr
---
exit: exit status: 1

empty stdout

stderr
------
INFO Opening docs in your browser for product 'ferrocene'.
error: Failed to open document in the browser at file:/path/to/toolchain/installation/96e6f26dbe3633b09bca5969805ee049655ef23f0a95fe9d0139fc9c1cc488fd/share/doc/ferrocene/html/index.html
caused by: error spawning command(s) 'THEREISNOSUCHBROWSER'
caused by: No such file or directory (os error 2)
------
22 changes: 22 additions & 0 deletions crates/criticalup-cli/tests/snapshots/cli__doc__help_message.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
source: crates/criticalup-cli/tests/cli/doc.rs
expression: repr
---
exit: exit status: 0

empty stdout

stderr
------
Open the documentation for the current toolchain

Usage:
criticalup-test doc [OPTIONS]

Options:
--project <PROJECT> Path to the manifest `criticalup.toml`
--path Only print the path to the documentation location
-v, --verbose... Enable debug logs, -vv for trace
--log-level [<LOG_LEVEL>...] Tracing directives
-h, --help Print help
------
Loading
Loading