diff --git a/crates/cargo-test-support/src/lib.rs b/crates/cargo-test-support/src/lib.rs index 3a1d13757c4..aa90ec9554a 100644 --- a/crates/cargo-test-support/src/lib.rs +++ b/crates/cargo-test-support/src/lib.rs @@ -569,6 +569,12 @@ impl Execs { self } + /// Writes the given lines to stdin. + pub fn with_stdin(&mut self, expected: S) -> &mut Self { + self.expect_stdin = Some(expected.to_string()); + self + } + /// Verifies the exit code from the process. /// /// This is not necessary if the expected exit code is `0`. @@ -820,7 +826,10 @@ impl Execs { #[track_caller] pub fn run(&mut self) { self.ran = true; - let p = (&self.process_builder).clone().unwrap(); + let mut p = (&self.process_builder).clone().unwrap(); + if let Some(stdin) = self.expect_stdin.take() { + p.stdin(stdin); + } if let Err(e) = self.match_process(&p) { panic_error(&format!("test failed running {}", p), e); } diff --git a/crates/cargo-test-support/src/registry.rs b/crates/cargo-test-support/src/registry.rs index d3f3e71642a..5a0a4c58280 100644 --- a/crates/cargo-test-support/src/registry.rs +++ b/crates/cargo-test-support/src/registry.rs @@ -1,16 +1,14 @@ use crate::git::repo; use crate::paths; +use cargo_util::paths::append; use cargo_util::{registry::make_dep_path, Sha256}; use flate2::write::GzEncoder; use flate2::Compression; -use std::collections::BTreeMap; -use std::fmt::Write as _; +use std::collections::{BTreeMap, HashMap}; use std::fs::{self, File}; use std::io::{BufRead, BufReader, Write}; -use std::net::{SocketAddr, TcpListener}; +use std::net::{SocketAddr, TcpListener, TcpStream}; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; use std::thread; use tar::{Builder, Header}; use url::Url; @@ -21,228 +19,292 @@ use url::Url; pub fn registry_path() -> PathBuf { generate_path("registry") } -pub fn registry_url() -> Url { - generate_url("registry") -} /// Gets the path for local web API uploads. Cargo will place the contents of a web API /// request here. For example, `api/v1/crates/new` is the result of publishing a crate. pub fn api_path() -> PathBuf { generate_path("api") } -pub fn api_url() -> Url { - generate_url("api") -} /// Gets the path where crates can be downloaded using the web API endpoint. Crates /// should be organized as `{name}/{version}/download` to match the web API /// endpoint. This is rarely used and must be manually set up. -pub fn dl_path() -> PathBuf { +fn dl_path() -> PathBuf { generate_path("dl") } -pub fn dl_url() -> Url { - generate_url("dl") -} /// Gets the alternative-registry version of `registry_path`. -pub fn alt_registry_path() -> PathBuf { +fn alt_registry_path() -> PathBuf { generate_path("alternative-registry") } -pub fn alt_registry_url() -> Url { +/// Gets the alternative-registry version of `registry_url`. +fn alt_registry_url() -> Url { generate_url("alternative-registry") } /// Gets the alternative-registry version of `dl_path`. pub fn alt_dl_path() -> PathBuf { - generate_path("alt_dl") -} -pub fn alt_dl_url() -> String { - generate_alt_dl_url("alt_dl") + generate_path("alternative-dl") } /// Gets the alternative-registry version of `api_path`. pub fn alt_api_path() -> PathBuf { - generate_path("alt_api") -} -pub fn alt_api_url() -> Url { - generate_url("alt_api") + generate_path("alternative-api") } - -pub fn generate_path(name: &str) -> PathBuf { +fn generate_path(name: &str) -> PathBuf { paths::root().join(name) } -pub fn generate_url(name: &str) -> Url { +fn generate_url(name: &str) -> Url { Url::from_file_path(generate_path(name)).ok().unwrap() } -pub fn generate_alt_dl_url(name: &str) -> String { - let base = Url::from_file_path(generate_path(name)).ok().unwrap(); - format!("{}/{{crate}}/{{version}}/{{crate}}-{{version}}.crate", base) -} /// A builder for initializing registries. pub struct RegistryBuilder { - /// If `true`, adds source replacement for crates.io to a registry on the filesystem. - replace_crates_io: bool, - /// If `true`, configures a registry named "alternative". - alternative: bool, - /// If set, sets the API url for the "alternative" registry. - /// This defaults to a directory on the filesystem. - alt_api_url: Option, - /// If `true`, configures `.cargo/credentials` with some tokens. - add_tokens: bool, + /// If set, configures an alternate registry with the given name. + alternative: Option, + /// If set, the authorization token for the registry. + token: Option, + /// If set, serves the index over http. + http_index: bool, + /// If set, serves the API over http. + http_api: bool, + /// If set, config.json includes 'api' + api: bool, + /// Write the token in the configuration. + configure_token: bool, + /// Write the registry in configuration. + configure_registry: bool, + /// API responders. + custom_responders: HashMap<&'static str, Box Response>>, +} + +pub struct TestRegistry { + _server: Option, + index_url: Url, + path: PathBuf, + api_url: Url, + dl_url: Url, + token: Option, +} + +impl TestRegistry { + pub fn index_url(&self) -> &Url { + &self.index_url + } + + pub fn api_url(&self) -> &Url { + &self.api_url + } + + pub fn token(&self) -> &str { + self.token + .as_deref() + .expect("registry was not configured with a token") + } } impl RegistryBuilder { + #[must_use] pub fn new() -> RegistryBuilder { RegistryBuilder { - replace_crates_io: true, - alternative: false, - alt_api_url: None, - add_tokens: true, + alternative: None, + token: Some("api-token".to_string()), + http_api: false, + http_index: false, + api: true, + configure_registry: true, + configure_token: true, + custom_responders: HashMap::new(), } } - /// Sets whether or not to replace crates.io with a registry on the filesystem. - /// Default is `true`. - pub fn replace_crates_io(&mut self, replace: bool) -> &mut Self { - self.replace_crates_io = replace; + /// Adds a custom HTTP response for a specific url + #[must_use] + pub fn add_responder Response>( + mut self, + url: &'static str, + responder: R, + ) -> Self { + self.custom_responders.insert(url, Box::new(responder)); self } - /// Sets whether or not to initialize an alternative registry named "alternative". - /// Default is `false`. - pub fn alternative(&mut self, alt: bool) -> &mut Self { - self.alternative = alt; + /// Sets whether or not to initialize as an alternative registry. + #[must_use] + pub fn alternative_named(mut self, alt: &str) -> Self { + self.alternative = Some(alt.to_string()); self } - /// Sets the API url for the "alternative" registry. - /// Defaults to a path on the filesystem ([`alt_api_path`]). - pub fn alternative_api_url(&mut self, url: &str) -> &mut Self { - self.alternative = true; - self.alt_api_url = Some(url.to_string()); + /// Sets whether or not to initialize as an alternative registry. + #[must_use] + pub fn alternative(self) -> Self { + self.alternative_named("alternative") + } + + /// Prevents placing a token in the configuration + #[must_use] + pub fn no_configure_token(mut self) -> Self { + self.configure_token = false; + self + } + + /// Prevents adding the registry to the configuration. + #[must_use] + pub fn no_configure_registry(mut self) -> Self { + self.configure_registry = false; + self + } + + /// Sets the token value + #[must_use] + pub fn token(mut self, token: &str) -> Self { + self.token = Some(token.to_string()); + self + } + + /// Operate the index over http + #[must_use] + pub fn http_index(mut self) -> Self { + self.http_index = true; self } - /// Sets whether or not to initialize `.cargo/credentials` with some tokens. - /// Defaults to `true`. - pub fn add_tokens(&mut self, add: bool) -> &mut Self { - self.add_tokens = add; + /// Operate the api over http + #[must_use] + pub fn http_api(mut self) -> Self { + self.http_api = true; self } - /// Initializes the registries. - pub fn build(&self) { + /// The registry has no api. + #[must_use] + pub fn no_api(mut self) -> Self { + self.api = false; + self + } + + /// Initializes the registry. + #[must_use] + pub fn build(self) -> TestRegistry { let config_path = paths::home().join(".cargo/config"); - if config_path.exists() { - panic!( - "{} already exists, the registry may only be initialized once, \ - and must be done before the config file is created", - config_path.display() - ); - } t!(fs::create_dir_all(config_path.parent().unwrap())); - let mut config = String::new(); - if self.replace_crates_io { - write!( - &mut config, - " + let prefix = if let Some(alternative) = &self.alternative { + format!("{alternative}-") + } else { + String::new() + }; + let registry_path = generate_path(&format!("{prefix}registry")); + let index_url = generate_url(&format!("{prefix}registry")); + let api_url = generate_url(&format!("{prefix}api")); + let dl_url = generate_url(&format!("{prefix}dl")); + let dl_path = generate_path(&format!("{prefix}dl")); + let api_path = generate_path(&format!("{prefix}api")); + + let (server, index_url, api_url, dl_url) = if !self.http_index && !self.http_api { + // No need to start the HTTP server. + (None, index_url, api_url, dl_url) + } else { + let server = HttpServer::new( + registry_path.clone(), + dl_path, + self.token.clone(), + self.custom_responders, + ); + let index_url = if self.http_index { + server.index_url() + } else { + index_url + }; + let api_url = if self.http_api { + server.api_url() + } else { + api_url + }; + let dl_url = server.dl_url(); + (Some(server), index_url, api_url, dl_url) + }; + + let registry = TestRegistry { + api_url, + index_url, + _server: server, + dl_url, + path: registry_path, + token: self.token, + }; + + if self.configure_registry { + if let Some(alternative) = &self.alternative { + append( + &config_path, + format!( + " + [registries.{alternative}] + index = '{}'", + registry.index_url + ) + .as_bytes(), + ) + .unwrap(); + } else { + append( + &config_path, + format!( + " [source.crates-io] replace-with = 'dummy-registry' [source.dummy-registry] - registry = '{}' - ", - registry_url() - ) - .unwrap(); - } - if self.alternative { - write!( - config, - " - [registries.alternative] - index = '{}' - ", - alt_registry_url() - ) - .unwrap(); + registry = '{}'", + registry.index_url + ) + .as_bytes(), + ) + .unwrap(); + } } - t!(fs::write(&config_path, config)); - if self.add_tokens { + if self.configure_token { + let token = registry.token.as_deref().unwrap(); let credentials = paths::home().join(".cargo/credentials"); - t!(fs::write( - &credentials, - r#" + if let Some(alternative) = &self.alternative { + append( + &credentials, + format!( + r#" + [registries.{alternative}] + token = "{token}" + "# + ) + .as_bytes(), + ) + .unwrap(); + } else { + append( + &credentials, + format!( + r#" [registry] - token = "api-token" - - [registries.alternative] - token = "api-token" + token = "{token}" "# - )); - } - - if self.replace_crates_io { - init_registry(registry_path(), dl_url().into(), api_url(), api_path()); - } - - if self.alternative { - init_registry( - alt_registry_path(), - alt_dl_url(), - self.alt_api_url - .as_ref() - .map_or_else(alt_api_url, |url| Url::parse(url).expect("valid url")), - alt_api_path(), - ); + ) + .as_bytes(), + ) + .unwrap(); + } } - } - - /// Initializes the registries, and sets up an HTTP server for the - /// "alternative" registry. - /// - /// The given callback takes a `Vec` of headers when a request comes in. - /// The first entry should be the HTTP command, such as - /// `PUT /api/v1/crates/new HTTP/1.1`. - /// - /// The callback should return the HTTP code for the response, and the - /// response body. - /// - /// This method returns a `JoinHandle` which you should call - /// `.join().unwrap()` on before exiting the test. - pub fn build_api_server<'a>( - &mut self, - handler: &'static (dyn (Fn(Vec) -> (u32, &'a dyn AsRef<[u8]>)) + Sync), - ) -> thread::JoinHandle<()> { - let server = TcpListener::bind("127.0.0.1:0").unwrap(); - let addr = server.local_addr().unwrap(); - let api_url = format!("http://{}", addr); - - self.replace_crates_io(false) - .alternative_api_url(&api_url) - .build(); - let t = thread::spawn(move || { - let mut conn = BufReader::new(server.accept().unwrap().0); - let headers: Vec<_> = (&mut conn) - .lines() - .map(|s| s.unwrap()) - .take_while(|s| s.len() > 2) - .map(|s| s.trim().to_string()) - .collect(); - let (code, response) = handler(headers); - let response = response.as_ref(); - let stream = conn.get_mut(); - write!( - stream, - "HTTP/1.1 {}\r\n\ - Content-Length: {}\r\n\ - \r\n", - code, - response.len() + let api = if self.api { + format!(r#","api":"{}""#, registry.api_url) + } else { + String::new() + }; + // Initialize a new registry. + repo(®istry.path) + .file( + "config.json", + &format!(r#"{{"dl":"{}"{api}}}"#, registry.dl_url), ) - .unwrap(); - stream.write_all(response).unwrap(); - }); + .build(); + fs::create_dir_all(api_path.join("api/v1/crates")).unwrap(); - t + registry } } @@ -357,195 +419,318 @@ const DEFAULT_MODE: u32 = 0o644; /// Initializes the on-disk registry and sets up the config so that crates.io /// is replaced with the one on disk. -pub fn init() { - let config = paths::home().join(".cargo/config"); - if config.exists() { - return; - } - RegistryBuilder::new().build(); +pub fn init() -> TestRegistry { + RegistryBuilder::new().build() } -/// Variant of `init` that initializes the "alternative" registry. -pub fn alt_init() { - RegistryBuilder::new().alternative(true).build(); +/// Variant of `init` that initializes the "alternative" registry and crates.io +/// replacement. +pub fn alt_init() -> TestRegistry { + init(); + RegistryBuilder::new().alternative().build() } -pub struct RegistryServer { - done: Arc, - server: Option>, +pub struct HttpServerHandle { addr: SocketAddr, } -impl RegistryServer { - pub fn addr(&self) -> SocketAddr { - self.addr +impl HttpServerHandle { + pub fn index_url(&self) -> Url { + Url::parse(&format!("sparse+http://{}/index/", self.addr.to_string())).unwrap() + } + + pub fn api_url(&self) -> Url { + Url::parse(&format!("http://{}/", self.addr.to_string())).unwrap() + } + + pub fn dl_url(&self) -> Url { + Url::parse(&format!("http://{}/dl", self.addr.to_string())).unwrap() } } -impl Drop for RegistryServer { +impl Drop for HttpServerHandle { fn drop(&mut self) { - self.done.store(true, Ordering::SeqCst); - // NOTE: we can't actually await the server since it's blocked in accept() - let _ = self.server.take(); + if let Ok(mut stream) = TcpStream::connect(self.addr) { + // shutdown the server + let _ = stream.write_all(b"stop"); + let _ = stream.flush(); + } } } -#[must_use] -pub fn serve_registry(registry_path: PathBuf) -> RegistryServer { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let addr = listener.local_addr().unwrap(); - let done = Arc::new(AtomicBool::new(false)); - let done2 = done.clone(); +/// Request to the test http server +#[derive(Debug)] +pub struct Request { + pub url: Url, + pub method: String, + pub authorization: Option, + pub if_modified_since: Option, + pub if_none_match: Option, +} - let t = thread::spawn(move || { +/// Response from the test http server +pub struct Response { + pub code: u32, + pub headers: Vec, + pub body: Vec, +} + +struct HttpServer { + listener: TcpListener, + registry_path: PathBuf, + dl_path: PathBuf, + token: Option, + custom_responders: HashMap<&'static str, Box Response>>, +} + +impl HttpServer { + pub fn new( + registry_path: PathBuf, + dl_path: PathBuf, + token: Option, + api_responders: HashMap<&'static str, Box Response>>, + ) -> HttpServerHandle { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + let server = HttpServer { + listener, + registry_path, + dl_path, + token, + custom_responders: api_responders, + }; + thread::spawn(move || server.start()); + HttpServerHandle { addr } + } + + fn start(&self) { let mut line = String::new(); - 'server: while !done2.load(Ordering::SeqCst) { - let (socket, _) = listener.accept().unwrap(); - // Let's implement a very naive static file HTTP server. + 'server: loop { + let (socket, _) = self.listener.accept().unwrap(); let mut buf = BufReader::new(socket); - - // First, the request line: - // GET /path HTTPVERSION line.clear(); if buf.read_line(&mut line).unwrap() == 0 { // Connection terminated. continue; } - - assert!(line.starts_with("GET "), "got non-GET request: {}", line); - let path = PathBuf::from( - line.split_whitespace() - .skip(1) - .next() - .unwrap() - .trim_start_matches('/'), + // Read the "GET path HTTP/1.1" line. + let mut parts = line.split_ascii_whitespace(); + let method = parts.next().unwrap().to_ascii_lowercase(); + if method == "stop" { + // Shutdown the server. + return; + } + let addr = self.listener.local_addr().unwrap(); + let url = format!( + "http://{}/{}", + addr, + parts.next().unwrap().trim_start_matches('/') ); - - let file = registry_path.join(path); - if file.exists() { - // Grab some other headers we may care about. - let mut if_modified_since = None; - let mut if_none_match = None; - loop { + let url = Url::parse(&url).unwrap(); + + // Grab headers we care about. + let mut if_modified_since = None; + let mut if_none_match = None; + let mut authorization = None; + loop { + line.clear(); + if buf.read_line(&mut line).unwrap() == 0 { + continue 'server; + } + if line == "\r\n" { + // End of headers. line.clear(); - if buf.read_line(&mut line).unwrap() == 0 { - continue 'server; - } + break; + } + let (name, value) = line.split_once(':').unwrap(); + let name = name.trim().to_ascii_lowercase(); + let value = value.trim().to_string(); + match name.as_str() { + "if-modified-since" => if_modified_since = Some(value), + "if-none-match" => if_none_match = Some(value), + "authorization" => authorization = Some(value), + _ => {} + } + } + let req = Request { + authorization, + if_modified_since, + if_none_match, + method, + url, + }; + println!("req: {:#?}", req); + let response = self.route(&req); + let buf = buf.get_mut(); + write!(buf, "HTTP/1.1 {}\r\n", response.code).unwrap(); + write!(buf, "Content-Length: {}\r\n", response.body.len()).unwrap(); + for header in response.headers { + write!(buf, "{}\r\n", header).unwrap(); + } + write!(buf, "\r\n").unwrap(); + buf.write_all(&response.body).unwrap(); + buf.flush().unwrap(); + } + } - if line == "\r\n" { - // End of headers. - line.clear(); - break; - } + /// Route the request + fn route(&self, req: &Request) -> Response { + let authorized = |mutatation: bool| { + if mutatation { + self.token == req.authorization + } else { + assert!(req.authorization.is_none(), "unexpected token"); + true + } + }; - let value = line - .splitn(2, ':') - .skip(1) - .next() - .map(|v| v.trim()) - .unwrap(); - - if line.starts_with("If-Modified-Since:") { - if_modified_since = Some(value.to_owned()); - } else if line.starts_with("If-None-Match:") { - if_none_match = Some(value.trim_matches('"').to_owned()); - } + // Check for custom responder + if let Some(responder) = self.custom_responders.get(req.url.path()) { + return responder(&req); + } + let path: Vec<_> = req.url.path()[1..].split('/').collect(); + match (req.method.as_str(), path.as_slice()) { + ("get", ["index", ..]) => { + if !authorized(false) { + self.unauthorized(req) + } else { + self.index(&req) } - - // Now grab info about the file. - let data = fs::read(&file).unwrap(); - let etag = Sha256::new().update(&data).finish_hex(); - let last_modified = format!("{:?}", file.metadata().unwrap().modified().unwrap()); - - // Start to construct our response: - let mut any_match = false; - let mut all_match = true; - if let Some(expected) = if_none_match { - if etag != expected { - all_match = false; - } else { - any_match = true; - } + } + ("get", ["dl", ..]) => { + if !authorized(false) { + self.unauthorized(req) + } else { + self.dl(&req) } - if let Some(expected) = if_modified_since { - // NOTE: Equality comparison is good enough for tests. - if last_modified != expected { - all_match = false; - } else { - any_match = true; - } + } + // The remainder of the operators in the test framework do nothing other than responding 'ok'. + // + // Note: We don't need to support anything real here because the testing framework publishes crates + // by writing directly to the filesystem instead. If the test framework is changed to publish + // via the HTTP API, then this should be made more complete. + + // publish + ("put", ["api", "v1", "crates", "new"]) + // yank + | ("delete", ["api", "v1", "crates", .., "yank"]) + // unyank + | ("put", ["api", "v1", "crates", .., "unyank"]) + // owners + | ("get" | "put" | "delete", ["api", "v1", "crates", .., "owners"]) => { + if !authorized(true) { + self.unauthorized(req) + } else { + self.ok(&req) } + } + _ => self.not_found(&req), + } + } + + /// Unauthorized response + fn unauthorized(&self, _req: &Request) -> Response { + Response { + code: 401, + headers: vec![], + body: b"Unauthorized message from server.".to_vec(), + } + } + + /// Not found response + fn not_found(&self, _req: &Request) -> Response { + Response { + code: 404, + headers: vec![], + body: b"not found".to_vec(), + } + } + + /// Respond OK without doing anything + fn ok(&self, _req: &Request) -> Response { + Response { + code: 200, + headers: vec![], + body: br#"{"ok": true, "msg": "completed!"}"#.to_vec(), + } + } + + /// Serve the download endpoint + fn dl(&self, req: &Request) -> Response { + let file = self + .dl_path + .join(req.url.path().strip_prefix("/dl/").unwrap()); + println!("{}", file.display()); + if !file.exists() { + return self.not_found(req); + } + return Response { + body: fs::read(&file).unwrap(), + code: 200, + headers: vec![], + }; + } - // Write out the main response line. - if any_match && all_match { - buf.get_mut() - .write_all(b"HTTP/1.1 304 Not Modified\r\n") - .unwrap(); + /// Serve the registry index + fn index(&self, req: &Request) -> Response { + let file = self + .registry_path + .join(req.url.path().strip_prefix("/index/").unwrap()); + if !file.exists() { + return self.not_found(req); + } else { + // Now grab info about the file. + let data = fs::read(&file).unwrap(); + let etag = Sha256::new().update(&data).finish_hex(); + let last_modified = format!("{:?}", file.metadata().unwrap().modified().unwrap()); + + // Start to construct our response: + let mut any_match = false; + let mut all_match = true; + if let Some(expected) = &req.if_none_match { + if &etag != expected { + all_match = false; } else { - buf.get_mut().write_all(b"HTTP/1.1 200 OK\r\n").unwrap(); + any_match = true; } - // TODO: Support 451 for crate index deletions. - - // Write out other headers. - buf.get_mut() - .write_all(format!("Content-Length: {}\r\n", data.len()).as_bytes()) - .unwrap(); - buf.get_mut() - .write_all(format!("ETag: \"{}\"\r\n", etag).as_bytes()) - .unwrap(); - buf.get_mut() - .write_all(format!("Last-Modified: {}\r\n", last_modified).as_bytes()) - .unwrap(); - - // And finally, write out the body. - buf.get_mut().write_all(b"\r\n").unwrap(); - buf.get_mut().write_all(&data).unwrap(); - } else { - loop { - line.clear(); - if buf.read_line(&mut line).unwrap() == 0 { - // Connection terminated. - continue 'server; - } - - if line == "\r\n" { - break; - } + } + if let Some(expected) = &req.if_modified_since { + // NOTE: Equality comparison is good enough for tests. + if &last_modified != expected { + all_match = false; + } else { + any_match = true; } + } - buf.get_mut() - .write_all(b"HTTP/1.1 404 Not Found\r\n\r\n") - .unwrap(); - buf.get_mut().write_all(b"\r\n").unwrap(); + if any_match && all_match { + return Response { + body: Vec::new(), + code: 304, + headers: vec![], + }; + } else { + return Response { + body: data, + code: 200, + headers: vec![ + format!("ETag: \"{}\"", etag), + format!("Last-Modified: {}", last_modified), + ], + }; } - buf.get_mut().flush().unwrap(); } - }); - - RegistryServer { - addr, - server: Some(t), - done, } } -/// Creates a new on-disk registry. -pub fn init_registry(registry_path: PathBuf, dl_url: String, api_url: Url, api_path: PathBuf) { - // Initialize a new registry. - repo(®istry_path) - .file( - "config.json", - &format!(r#"{{"dl":"{}","api":"{}"}}"#, dl_url, api_url), - ) - .build(); - fs::create_dir_all(api_path.join("api/v1/crates")).unwrap(); -} - impl Package { /// Creates a new package builder. /// Call `publish()` to finalize and build the package. pub fn new(name: &str, vers: &str) -> Package { - init(); + let config = paths::home().join(".cargo/config"); + if !config.exists() { + init(); + } Package { name: name.to_string(), vers: vers.to_string(), @@ -951,7 +1136,7 @@ impl Package { alt_dl_path() .join(&self.name) .join(&self.vers) - .join(&format!("{}-{}.crate", self.name, self.vers)) + .join("download") } else { dl_path().join(&self.name).join(&self.vers).join("download") } diff --git a/crates/cargo-util/Cargo.toml b/crates/cargo-util/Cargo.toml index 86afbd0eeec..9f969671cd3 100644 --- a/crates/cargo-util/Cargo.toml +++ b/crates/cargo-util/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-util" -version = "0.1.3" +version = "0.1.4" edition = "2021" license = "MIT OR Apache-2.0" homepage = "https://github.com/rust-lang/cargo" diff --git a/crates/cargo-util/src/process_builder.rs b/crates/cargo-util/src/process_builder.rs index 219ab586a88..714cc595eff 100644 --- a/crates/cargo-util/src/process_builder.rs +++ b/crates/cargo-util/src/process_builder.rs @@ -10,7 +10,7 @@ use std::collections::BTreeMap; use std::env; use std::ffi::{OsStr, OsString}; use std::fmt; -use std::io; +use std::io::{self, Write}; use std::iter::once; use std::path::Path; use std::process::{Command, ExitStatus, Output, Stdio}; @@ -39,6 +39,8 @@ pub struct ProcessBuilder { /// `true` to retry with an argfile if hitting "command line too big" error. /// See [`ProcessBuilder::retry_with_argfile`] for more information. retry_with_argfile: bool, + /// Data to write to stdin. + stdin: Vec, } impl fmt::Display for ProcessBuilder { @@ -80,6 +82,7 @@ impl ProcessBuilder { jobserver: None, display_env_vars: false, retry_with_argfile: false, + stdin: Vec::new(), } } @@ -207,6 +210,12 @@ impl ProcessBuilder { self } + /// Sets a value that will be written to stdin of the process on launch. + pub fn stdin>>(&mut self, stdin: T) -> &mut Self { + self.stdin = stdin.into(); + self + } + fn should_retry_with_argfile(&self, err: &io::Error) -> bool { self.retry_with_argfile && imp::command_line_too_big(err) } @@ -278,11 +287,16 @@ impl ProcessBuilder { match piped(&mut cmd).spawn() { Err(ref e) if self.should_retry_with_argfile(e) => {} Err(e) => return Err(e), - Ok(child) => return child.wait_with_output(), + Ok(mut child) => { + child.stdin.take().unwrap().write_all(&self.stdin)?; + return child.wait_with_output(); + } } } let (mut cmd, argfile) = self.build_command_with_argfile()?; - let output = piped(&mut cmd).spawn()?.wait_with_output(); + let mut child = piped(&mut cmd).spawn()?; + child.stdin.take().unwrap().write_all(&self.stdin)?; + let output = child.wait_with_output(); close_tempfile_and_log_error(argfile); output } @@ -527,11 +541,11 @@ fn debug_force_argfile(retry_enabled: bool) -> bool { cfg!(debug_assertions) && env::var("__CARGO_TEST_FORCE_ARGFILE").is_ok() && retry_enabled } -/// Creates new pipes for stderr and stdout. Ignores stdin. +/// Creates new pipes for stderr, stdout and stdin. fn piped(cmd: &mut Command) -> &mut Command { cmd.stdout(Stdio::piped()) .stderr(Stdio::piped()) - .stdin(Stdio::null()) + .stdin(Stdio::piped()) } fn close_tempfile_and_log_error(file: NamedTempFile) { diff --git a/tests/testsuite/alt_registry.rs b/tests/testsuite/alt_registry.rs index 0e047806d1d..9db2d9268cd 100644 --- a/tests/testsuite/alt_registry.rs +++ b/tests/testsuite/alt_registry.rs @@ -2,7 +2,7 @@ use cargo::util::IntoUrl; use cargo_test_support::publish::validate_alt_upload; -use cargo_test_support::registry::{self, Package}; +use cargo_test_support::registry::{self, Package, RegistryBuilder}; use cargo_test_support::{basic_manifest, git, paths, project}; use std::fs; @@ -660,18 +660,8 @@ Caused by: #[cargo_test] fn no_api() { - registry::alt_init(); + let _registry = RegistryBuilder::new().alternative().no_api().build(); Package::new("bar", "0.0.1").alternative(true).publish(); - // Configure without `api`. - let repo = git2::Repository::open(registry::alt_registry_path()).unwrap(); - let cfg_path = registry::alt_registry_path().join("config.json"); - fs::write( - cfg_path, - format!(r#"{{"dl": "{}"}}"#, registry::alt_dl_url()), - ) - .unwrap(); - git::add(&repo); - git::commit(&repo); // First check that a dependency works. let p = project() @@ -1221,8 +1211,6 @@ fn registries_index_relative_url() { ) .unwrap(); - registry::init(); - let p = project() .file( "Cargo.toml", @@ -1270,8 +1258,6 @@ fn registries_index_relative_path_not_allowed() { ) .unwrap(); - registry::init(); - let p = project() .file( "Cargo.toml", diff --git a/tests/testsuite/credential_process.rs b/tests/testsuite/credential_process.rs index 33a36ceaf1b..b6904597d62 100644 --- a/tests/testsuite/credential_process.rs +++ b/tests/testsuite/credential_process.rs @@ -1,8 +1,8 @@ //! Tests for credential-process. +use cargo_test_support::registry::TestRegistry; use cargo_test_support::{basic_manifest, cargo_process, paths, project, registry, Project}; use std::fs; -use std::thread; fn toml_bin(proj: &Project, name: &str) -> String { proj.bin(name).display().to_string().replace('\\', "\\\\") @@ -10,9 +10,13 @@ fn toml_bin(proj: &Project, name: &str) -> String { #[cargo_test] fn gated() { - registry::RegistryBuilder::new() - .alternative(true) - .add_tokens(false) + let _alternative = registry::RegistryBuilder::new() + .alternative() + .no_configure_token() + .build(); + + let _cratesio = registry::RegistryBuilder::new() + .no_configure_token() .build(); let p = project() @@ -61,9 +65,9 @@ fn gated() { #[cargo_test] fn warn_both_token_and_process() { // Specifying both credential-process and a token in config should issue a warning. - registry::RegistryBuilder::new() - .alternative(true) - .add_tokens(false) + let _server = registry::RegistryBuilder::new() + .alternative() + .no_configure_token() .build(); let p = project() .file( @@ -134,19 +138,15 @@ Only one of these values may be set, remove one or the other to proceed. /// * Create a simple `foo` project to run the test against. /// * Configure the credential-process config. /// -/// Returns a thread handle for the API server, the test should join it when -/// finished. Also returns the simple `foo` project to test against. -fn get_token_test() -> (Project, thread::JoinHandle<()>) { +/// Returns returns the simple `foo` project to test against and the API server handle. +fn get_token_test() -> (Project, TestRegistry) { // API server that checks that the token is included correctly. let server = registry::RegistryBuilder::new() - .add_tokens(false) - .build_api_server(&|headers| { - assert!(headers - .iter() - .any(|header| header == "Authorization: sekrit")); - - (200, &r#"{"ok": true, "msg": "completed!"}"#) - }); + .no_configure_token() + .token("sekrit") + .alternative() + .http_api() + .build(); // The credential process to use. let cred_proj = project() @@ -165,7 +165,7 @@ fn get_token_test() -> (Project, thread::JoinHandle<()>) { index = "{}" credential-process = ["{}"] "#, - registry::alt_registry_url(), + server.index_url(), toml_bin(&cred_proj, "test-cred") ), ) @@ -189,7 +189,7 @@ fn get_token_test() -> (Project, thread::JoinHandle<()>) { #[cargo_test] fn publish() { // Checks that credential-process is used for `cargo publish`. - let (p, t) = get_token_test(); + let (p, _t) = get_token_test(); p.cargo("publish --no-verify --registry alternative -Z credential-process") .masquerade_as_nightly_cargo() @@ -201,14 +201,14 @@ fn publish() { ", ) .run(); - - t.join().ok().unwrap(); } #[cargo_test] fn basic_unsupported() { // Non-action commands don't support login/logout. - registry::RegistryBuilder::new().add_tokens(false).build(); + let _server = registry::RegistryBuilder::new() + .no_configure_token() + .build(); cargo_util::paths::append( &paths::home().join(".cargo/config"), br#" @@ -246,7 +246,9 @@ the credential-process configuration value must pass the \ #[cargo_test] fn login() { - registry::init(); + let server = registry::RegistryBuilder::new() + .no_configure_token() + .build(); // The credential process to use. let cred_proj = project() .at("cred_proj") @@ -266,7 +268,7 @@ fn login() { std::fs::write("token-store", buffer).unwrap(); } "# - .replace("__API__", ®istry::api_url().to_string()), + .replace("__API__", server.api_url().as_str()), ) .build(); cred_proj.cargo("build").run(); @@ -301,7 +303,9 @@ fn login() { #[cargo_test] fn logout() { - registry::RegistryBuilder::new().add_tokens(false).build(); + let _server = registry::RegistryBuilder::new() + .no_configure_token() + .build(); // The credential process to use. let cred_proj = project() .at("cred_proj") @@ -354,7 +358,7 @@ token for `crates-io` has been erased! #[cargo_test] fn yank() { - let (p, t) = get_token_test(); + let (p, _t) = get_token_test(); p.cargo("yank --version 0.1.0 --registry alternative -Z credential-process") .masquerade_as_nightly_cargo() @@ -365,13 +369,11 @@ fn yank() { ", ) .run(); - - t.join().ok().unwrap(); } #[cargo_test] fn owner() { - let (p, t) = get_token_test(); + let (p, _t) = get_token_test(); p.cargo("owner --add username --registry alternative -Z credential-process") .masquerade_as_nightly_cargo() @@ -382,14 +384,14 @@ fn owner() { ", ) .run(); - - t.join().ok().unwrap(); } #[cargo_test] fn libexec_path() { // cargo: prefixed names use the sysroot - registry::RegistryBuilder::new().add_tokens(false).build(); + let _server = registry::RegistryBuilder::new() + .no_configure_token() + .build(); cargo_util::paths::append( &paths::home().join(".cargo/config"), br#" @@ -420,9 +422,9 @@ Caused by: #[cargo_test] fn invalid_token_output() { // Error when credential process does not output the expected format for a token. - registry::RegistryBuilder::new() - .alternative(true) - .add_tokens(false) + let _server = registry::RegistryBuilder::new() + .alternative() + .no_configure_token() .build(); let cred_proj = project() .at("cred_proj") diff --git a/tests/testsuite/install.rs b/tests/testsuite/install.rs index eee7c7a563b..dc473c7198f 100644 --- a/tests/testsuite/install.rs +++ b/tests/testsuite/install.rs @@ -5,7 +5,7 @@ use std::io::prelude::*; use cargo_test_support::cross_compile; use cargo_test_support::git; -use cargo_test_support::registry::{self, registry_path, registry_url, Package}; +use cargo_test_support::registry::{self, registry_path, Package}; use cargo_test_support::{ basic_manifest, cargo_process, no_such_file_err_msg, project, project_in, symlink_supported, t, }; @@ -133,10 +133,11 @@ fn simple_with_message_format() { #[cargo_test] fn with_index() { + let registry = registry::init(); pkg("foo", "0.0.1"); cargo_process("install foo --index") - .arg(registry_url().to_string()) + .arg(registry.index_url().as_str()) .with_stderr(&format!( "\ [UPDATING] `{reg}` index diff --git a/tests/testsuite/login.rs b/tests/testsuite/login.rs index 14def0d50e3..716bc3e41b4 100644 --- a/tests/testsuite/login.rs +++ b/tests/testsuite/login.rs @@ -1,10 +1,9 @@ //! Tests for the `cargo login` command. use cargo_test_support::install::cargo_home; -use cargo_test_support::registry; -use cargo_test_support::{cargo_process, paths, t}; -use std::fs::{self, OpenOptions}; -use std::io::prelude::*; +use cargo_test_support::registry::RegistryBuilder; +use cargo_test_support::{cargo_process, t}; +use std::fs::{self}; use std::path::PathBuf; use toml_edit::easy as toml; @@ -62,27 +61,11 @@ fn check_token(expected_token: &str, registry: Option<&str>) -> bool { #[cargo_test] fn registry_credentials() { - registry::alt_init(); - - let config = paths::home().join(".cargo/config"); - let mut f = OpenOptions::new().append(true).open(config).unwrap(); - t!(f.write_all( - format!( - r#" - [registries.alternative2] - index = '{}' - "#, - registry::generate_url("alternative2-registry") - ) - .as_bytes(), - )); + let _alternative = RegistryBuilder::new().alternative().build(); + let _alternative2 = RegistryBuilder::new() + .alternative_named("alternative2") + .build(); - registry::init_registry( - registry::generate_path("alternative2-registry"), - registry::generate_alt_dl_url("alt2_dl"), - registry::generate_url("alt2_api"), - registry::generate_path("alt2_api"), - ); setup_new_credentials(); let reg = "alternative"; diff --git a/tests/testsuite/logout.rs b/tests/testsuite/logout.rs index d491ede13bf..9b40d18da54 100644 --- a/tests/testsuite/logout.rs +++ b/tests/testsuite/logout.rs @@ -45,7 +45,6 @@ fn check_config_token(registry: Option<&str>, should_be_set: bool) { } fn simple_logout_test(reg: Option<&str>, flag: &str) { - registry::init(); let msg = reg.unwrap_or("crates.io"); check_config_token(reg, true); cargo_process(&format!("logout -Z unstable-options {}", flag)) @@ -74,6 +73,7 @@ fn simple_logout_test(reg: Option<&str>, flag: &str) { #[cargo_test] fn default_registry() { + registry::init(); simple_logout_test(None, ""); } diff --git a/tests/testsuite/old_cargos.rs b/tests/testsuite/old_cargos.rs index aa07757785d..99b93cdd2c9 100644 --- a/tests/testsuite/old_cargos.rs +++ b/tests/testsuite/old_cargos.rs @@ -113,6 +113,7 @@ fn default_toolchain_is_stable() -> bool { #[ignore] #[cargo_test] fn new_features() { + let registry = registry::init(); if std::process::Command::new("rustup").output().is_err() { panic!("old_cargos requires rustup to be installed"); } @@ -153,7 +154,7 @@ fn new_features() { let lock_bar_to = |toolchain_version: &Version, bar_version| { let lock = if toolchain_version < &Version::new(1, 12, 0) { - let url = registry::registry_url(); + let url = registry.index_url(); match bar_version { 100 => format!( r#" @@ -314,7 +315,7 @@ fn new_features() { [registry] index = "{}" "#, - registry::registry_url() + registry.index_url() ), ) .unwrap(); @@ -330,7 +331,7 @@ fn new_features() { [source.dummy-registry] registry = '{}' ", - registry::registry_url() + registry.index_url() ), ) .unwrap(); diff --git a/tests/testsuite/package.rs b/tests/testsuite/package.rs index 378b8dd9112..32fe238e410 100644 --- a/tests/testsuite/package.rs +++ b/tests/testsuite/package.rs @@ -1072,7 +1072,7 @@ src/lib.rs #[cargo_test] fn generated_manifest() { - registry::alt_init(); + let registry = registry::alt_init(); Package::new("abc", "1.0.0").publish(); Package::new("def", "1.0.0").alternative(true).publish(); Package::new("ghi", "1.0.0").publish(); @@ -1137,7 +1137,7 @@ registry-index = "{}" version = "1.0" "#, cargo::core::package::MANIFEST_PREAMBLE, - registry::alt_registry_url() + registry.index_url() ); validate_crate_contents( diff --git a/tests/testsuite/publish.rs b/tests/testsuite/publish.rs index 1977aba7007..77de699f6c7 100644 --- a/tests/testsuite/publish.rs +++ b/tests/testsuite/publish.rs @@ -2,7 +2,7 @@ use cargo_test_support::git::{self, repo}; use cargo_test_support::paths; -use cargo_test_support::registry::{self, registry_url, Package}; +use cargo_test_support::registry::{self, Package, Response}; use cargo_test_support::{basic_manifest, no_such_file_err_msg, project, publish}; use std::fs; @@ -187,7 +187,7 @@ See [..] #[cargo_test] fn simple_with_index() { - registry::init(); + let registry = registry::init(); let p = project() .file( @@ -205,7 +205,7 @@ fn simple_with_index() { .build(); p.cargo("publish --no-verify --token sekrit --index") - .arg(registry_url().to_string()) + .arg(registry.index_url().as_str()) .run(); validate_upload_foo(); @@ -287,7 +287,7 @@ the `path` specification will be removed from the dependency declaration. #[cargo_test] fn unpublishable_crate() { - registry::init(); + let registry = registry::init(); let p = project() .file( @@ -306,7 +306,7 @@ fn unpublishable_crate() { .build(); p.cargo("publish --index") - .arg(registry_url().to_string()) + .arg(registry.index_url().as_str()) .with_status(101) .with_stderr( "\ @@ -526,7 +526,7 @@ fn new_crate_rejected() { #[cargo_test] fn dry_run() { - registry::init(); + let registry = registry::init(); let p = project() .file( @@ -544,7 +544,7 @@ fn dry_run() { .build(); p.cargo("publish --dry-run --index") - .arg(registry_url().to_string()) + .arg(registry.index_url().as_str()) .with_stderr( "\ [UPDATING] `[..]` index @@ -1330,7 +1330,7 @@ fn credentials_ambiguous_filename() { fn index_requires_token() { // --index will not load registry.token to avoid possibly leaking // crates.io token to another server. - registry::init(); + let registry = registry::init(); let credentials = paths::home().join(".cargo/credentials"); fs::remove_file(&credentials).unwrap(); @@ -1350,7 +1350,7 @@ fn index_requires_token() { .build(); p.cargo("publish --no-verify --index") - .arg(registry_url().to_string()) + .arg(registry.index_url().as_str()) .with_status(101) .with_stderr( "\ @@ -1440,9 +1440,15 @@ Caused by: #[cargo_test] fn api_error_json() { // Registry returns an API error. - let t = registry::RegistryBuilder::new().build_api_server(&|_headers| { - (403, &r#"{"errors": [{"detail": "you must be logged in"}]}"#) - }); + let _registry = registry::RegistryBuilder::new() + .alternative() + .http_api() + .add_responder("/api/v1/crates/new", |_| Response { + body: br#"{"errors": [{"detail": "you must be logged in"}]}"#.to_vec(), + code: 403, + headers: vec![], + }) + .build(); let p = project() .file( @@ -1476,19 +1482,20 @@ Caused by: ", ) .run(); - - t.join().unwrap(); } #[cargo_test] fn api_error_200() { // Registry returns an API error with a 200 status code. - let t = registry::RegistryBuilder::new().build_api_server(&|_headers| { - ( - 200, - &r#"{"errors": [{"detail": "max upload size is 123"}]}"#, - ) - }); + let _registry = registry::RegistryBuilder::new() + .alternative() + .http_api() + .add_responder("/api/v1/crates/new", |_| Response { + body: br#"{"errors": [{"detail": "max upload size is 123"}]}"#.to_vec(), + code: 200, + headers: vec![], + }) + .build(); let p = project() .file( @@ -1522,14 +1529,20 @@ Caused by: ", ) .run(); - - t.join().unwrap(); } #[cargo_test] fn api_error_code() { // Registry returns an error code without a JSON message. - let t = registry::RegistryBuilder::new().build_api_server(&|_headers| (400, &"go away")); + let _registry = registry::RegistryBuilder::new() + .alternative() + .http_api() + .add_responder("/api/v1/crates/new", |_| Response { + body: br#"go away"#.to_vec(), + code: 400, + headers: vec![], + }) + .build(); let p = project() .file( @@ -1569,15 +1582,18 @@ Caused by: ", ) .run(); - - t.join().unwrap(); } #[cargo_test] fn api_curl_error() { // Registry has a network error. - let t = registry::RegistryBuilder::new().build_api_server(&|_headers| panic!("broke!")); - + let _registry = registry::RegistryBuilder::new() + .alternative() + .http_api() + .add_responder("/api/v1/crates/new", |_| { + panic!("broke"); + }) + .build(); let p = project() .file( "Cargo.toml", @@ -1615,15 +1631,20 @@ Caused by: ", ) .run(); - - let e = t.join().unwrap_err(); - assert_eq!(*e.downcast::<&str>().unwrap(), "broke!"); } #[cargo_test] fn api_other_error() { // Registry returns an invalid response. - let t = registry::RegistryBuilder::new().build_api_server(&|_headers| (200, b"\xff")); + let _registry = registry::RegistryBuilder::new() + .alternative() + .http_api() + .add_responder("/api/v1/crates/new", |_| Response { + body: b"\xff".to_vec(), + code: 200, + headers: vec![], + }) + .build(); let p = project() .file( @@ -1660,8 +1681,6 @@ Caused by: ", ) .run(); - - t.join().unwrap(); } #[cargo_test] diff --git a/tests/testsuite/registry.rs b/tests/testsuite/registry.rs index 7c4e3f6e07c..bf0335cc4cc 100644 --- a/tests/testsuite/registry.rs +++ b/tests/testsuite/registry.rs @@ -1,18 +1,16 @@ //! Tests for normal registry dependencies. use cargo::core::SourceId; +use cargo_test_support::cargo_process; use cargo_test_support::paths::{self, CargoPathExt}; use cargo_test_support::registry::{ - self, registry_path, serve_registry, Dependency, Package, RegistryServer, + self, registry_path, Dependency, Package, RegistryBuilder, TestRegistry, }; use cargo_test_support::{basic_manifest, project, Execs, Project}; -use cargo_test_support::{cargo_process, registry::registry_url}; use cargo_test_support::{git, install::cargo_home, t}; use cargo_util::paths::remove_dir_all; use std::fs::{self, File}; -use std::io::{BufRead, BufReader, Write}; use std::path::Path; -use std::process::Stdio; fn cargo_http(p: &Project, s: &str) -> Execs { let mut e = p.cargo(s); @@ -24,28 +22,8 @@ fn cargo_stable(p: &Project, s: &str) -> Execs { p.cargo(s) } -fn setup_http() -> RegistryServer { - let server = serve_registry(registry_path()); - configure_source_replacement_for_http(&server.addr().to_string()); - server -} - -fn configure_source_replacement_for_http(addr: &str) { - let root = paths::root(); - t!(fs::create_dir(&root.join(".cargo"))); - t!(fs::write( - root.join(".cargo/config"), - format!( - " - [source.crates-io] - replace-with = 'dummy-registry' - - [source.dummy-registry] - registry = 'sparse+http://{}/' - ", - addr - ) - )); +fn setup_http() -> TestRegistry { + RegistryBuilder::new().http_index().build() } #[cargo_test] @@ -1121,26 +1099,10 @@ fn login_with_token_on_stdin() { let credentials = paths::home().join(".cargo/credentials"); fs::remove_file(&credentials).unwrap(); cargo_process("login lmao -v").run(); - let mut cargo = cargo_process("login").build_command(); - cargo - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - let mut child = cargo.spawn().unwrap(); - let out = BufReader::new(child.stdout.as_mut().unwrap()) - .lines() - .next() - .unwrap() - .unwrap(); - assert!(out.starts_with("please paste the API Token found on ")); - assert!(out.ends_with("/me below")); - child - .stdin - .as_ref() - .unwrap() - .write_all(b"some token\n") - .unwrap(); - child.wait().unwrap(); + cargo_process("login") + .with_stdout("please paste the API Token found on [..]/me below") + .with_stdin("some token") + .run(); let credentials = fs::read_to_string(&credentials).unwrap(); assert_eq!(credentials, "[registry]\ntoken = \"some token\"\n"); } @@ -2584,6 +2546,7 @@ Use `[source]` replacement to alter the default index for crates.io. #[cargo_test] fn package_lock_inside_package_is_overwritten() { + let registry = registry::init(); let p = project() .file( "Cargo.toml", @@ -2607,7 +2570,7 @@ fn package_lock_inside_package_is_overwritten() { p.cargo("build").run(); - let id = SourceId::for_registry(®istry_url()).unwrap(); + let id = SourceId::for_registry(registry.index_url()).unwrap(); let hash = cargo::util::hex::short_hash(&id); let ok = cargo_home() .join("registry") diff --git a/tests/testsuite/search.rs b/tests/testsuite/search.rs index 47b9ebd2047..6f4845d01e6 100644 --- a/tests/testsuite/search.rs +++ b/tests/testsuite/search.rs @@ -1,149 +1,99 @@ //! Tests for the `cargo search` command. use cargo_test_support::cargo_process; -use cargo_test_support::git::repo; use cargo_test_support::paths; -use cargo_test_support::registry::{api_path, registry_path, registry_url}; +use cargo_test_support::registry::{RegistryBuilder, Response}; use std::collections::HashSet; -use std::fs; -use std::path::Path; -use url::Url; -fn api() -> Url { - Url::from_file_path(&*api_path()).ok().unwrap() -} - -fn write_crates(dest: &Path) { - let content = r#"{ - "crates": [{ - "created_at": "2014-11-16T20:17:35Z", - "description": "Design by contract style assertions for Rust", - "documentation": null, - "downloads": 2, - "homepage": null, - "id": "hoare", - "keywords": [], - "license": null, - "links": { - "owners": "/api/v1/crates/hoare/owners", - "reverse_dependencies": "/api/v1/crates/hoare/reverse_dependencies", - "version_downloads": "/api/v1/crates/hoare/downloads", - "versions": "/api/v1/crates/hoare/versions" - }, - "max_version": "0.1.1", - "name": "hoare", - "repository": "https://github.com/nick29581/libhoare", - "updated_at": "2014-11-20T21:49:21Z", - "versions": null +const SEARCH_API_RESPONSE: &[u8] = br#" +{ + "crates": [{ + "created_at": "2014-11-16T20:17:35Z", + "description": "Design by contract style assertions for Rust", + "documentation": null, + "downloads": 2, + "homepage": null, + "id": "hoare", + "keywords": [], + "license": null, + "links": { + "owners": "/api/v1/crates/hoare/owners", + "reverse_dependencies": "/api/v1/crates/hoare/reverse_dependencies", + "version_downloads": "/api/v1/crates/hoare/downloads", + "versions": "/api/v1/crates/hoare/versions" }, - { - "id": "postgres", - "name": "postgres", - "updated_at": "2020-05-01T23:17:54.335921+00:00", - "versions": null, - "keywords": null, - "categories": null, - "badges": [ - { - "badge_type": "circle-ci", - "attributes": { - "repository": "sfackler/rust-postgres", - "branch": null - } + "max_version": "0.1.1", + "name": "hoare", + "repository": "https://github.com/nick29581/libhoare", + "updated_at": "2014-11-20T21:49:21Z", + "versions": null + }, + { + "id": "postgres", + "name": "postgres", + "updated_at": "2020-05-01T23:17:54.335921+00:00", + "versions": null, + "keywords": null, + "categories": null, + "badges": [ + { + "badge_type": "circle-ci", + "attributes": { + "repository": "sfackler/rust-postgres", + "branch": null } - ], - "created_at": "2014-11-24T02:34:44.756689+00:00", - "downloads": 535491, - "recent_downloads": 88321, - "max_version": "0.17.3", - "newest_version": "0.17.3", - "description": "A native, synchronous PostgreSQL client", - "homepage": null, - "documentation": null, - "repository": "https://github.com/sfackler/rust-postgres", - "links": { - "version_downloads": "/api/v1/crates/postgres/downloads", - "versions": "/api/v1/crates/postgres/versions", - "owners": "/api/v1/crates/postgres/owners", - "owner_team": "/api/v1/crates/postgres/owner_team", - "owner_user": "/api/v1/crates/postgres/owner_user", - "reverse_dependencies": "/api/v1/crates/postgres/reverse_dependencies" - }, - "exact_match": true - } + } ], - "meta": { - "total": 2 - } - }"#; - - // Older versions of curl don't peel off query parameters when looking for - // filenames, so just make both files. - // - // On windows, though, `?` is an invalid character, but we always build curl - // from source there anyway! - fs::write(&dest, content).unwrap(); - if !cfg!(windows) { - fs::write( - &dest.with_file_name("crates?q=postgres&per_page=10"), - content, - ) - .unwrap(); + "created_at": "2014-11-24T02:34:44.756689+00:00", + "downloads": 535491, + "recent_downloads": 88321, + "max_version": "0.17.3", + "newest_version": "0.17.3", + "description": "A native, synchronous PostgreSQL client", + "homepage": null, + "documentation": null, + "repository": "https://github.com/sfackler/rust-postgres", + "links": { + "version_downloads": "/api/v1/crates/postgres/downloads", + "versions": "/api/v1/crates/postgres/versions", + "owners": "/api/v1/crates/postgres/owners", + "owner_team": "/api/v1/crates/postgres/owner_team", + "owner_user": "/api/v1/crates/postgres/owner_user", + "reverse_dependencies": "/api/v1/crates/postgres/reverse_dependencies" + }, + "exact_match": true } -} + ], + "meta": { + "total": 2 + } +}"#; const SEARCH_RESULTS: &str = "\ hoare = \"0.1.1\" # Design by contract style assertions for Rust postgres = \"0.17.3\" # A native, synchronous PostgreSQL client "; -fn setup() { - let cargo_home = paths::root().join(".cargo"); - fs::create_dir_all(cargo_home).unwrap(); - fs::create_dir_all(&api_path().join("api/v1")).unwrap(); - - // Init a new registry - let _ = repo(®istry_path()) - .file( - "config.json", - &format!(r#"{{"dl":"{0}","api":"{0}"}}"#, api()), - ) - .build(); - - let base = api_path().join("api/v1/crates"); - write_crates(&base); -} - -fn set_cargo_config() { - let config = paths::root().join(".cargo/config"); - - fs::write( - &config, - format!( - r#" - [source.crates-io] - registry = 'https://wut' - replace-with = 'dummy-registry' - - [source.dummy-registry] - registry = '{reg}' - "#, - reg = registry_url(), - ), - ) - .unwrap(); +#[must_use] +fn setup() -> RegistryBuilder { + RegistryBuilder::new() + .http_api() + .add_responder("/api/v1/crates", |_| Response { + code: 200, + headers: vec![], + body: SEARCH_API_RESPONSE.to_vec(), + }) } #[cargo_test] fn not_update() { - setup(); - set_cargo_config(); + let registry = setup().build(); use cargo::core::{Shell, Source, SourceId}; use cargo::sources::RegistrySource; use cargo::util::Config; - let sid = SourceId::for_registry(®istry_url()).unwrap(); + let sid = SourceId::for_registry(registry.index_url()).unwrap(); let cfg = Config::new( Shell::from_write(Box::new(Vec::new())), paths::root(), @@ -163,8 +113,7 @@ fn not_update() { #[cargo_test] fn replace_default() { - setup(); - set_cargo_config(); + let _server = setup().build(); cargo_process("search postgres") .with_stdout_contains(SEARCH_RESULTS) @@ -174,28 +123,27 @@ fn replace_default() { #[cargo_test] fn simple() { - setup(); + let registry = setup().build(); cargo_process("search postgres --index") - .arg(registry_url().to_string()) + .arg(registry.index_url().as_str()) .with_stdout_contains(SEARCH_RESULTS) .run(); } #[cargo_test] fn multiple_query_params() { - setup(); + let registry = setup().build(); cargo_process("search postgres sql --index") - .arg(registry_url().to_string()) + .arg(registry.index_url().as_str()) .with_stdout_contains(SEARCH_RESULTS) .run(); } #[cargo_test] fn ignore_quiet() { - setup(); - set_cargo_config(); + let _server = setup().build(); cargo_process("search -q postgres") .with_stdout_contains(SEARCH_RESULTS) @@ -204,8 +152,7 @@ fn ignore_quiet() { #[cargo_test] fn colored_results() { - setup(); - set_cargo_config(); + let _server = setup().build(); cargo_process("search --color=never postgres") .with_stdout_does_not_contain("[..]\x1b[[..]")