From 24dac452c55fe827840147063a68b6a8b116b8ea Mon Sep 17 00:00:00 2001 From: Arlo Siemsen Date: Wed, 8 Jun 2022 22:04:33 -0500 Subject: [PATCH] Improve testing framework for http registries Improve integration of the http server introduced by the http-registry feature. Now the same HTTP server is used for serving downloads, the index, and the API. This makes it easier to write tests that deal with authentication and http registries. --- crates/cargo-test-support/src/lib.rs | 11 +- crates/cargo-test-support/src/registry.rs | 807 +++++++++++++--------- crates/cargo-util/Cargo.toml | 2 +- crates/cargo-util/src/process_builder.rs | 24 +- tests/testsuite/alt_registry.rs | 18 +- tests/testsuite/credential_process.rs | 74 +- tests/testsuite/install.rs | 5 +- tests/testsuite/login.rs | 31 +- tests/testsuite/logout.rs | 2 +- tests/testsuite/old_cargos.rs | 7 +- tests/testsuite/package.rs | 4 +- tests/testsuite/publish.rs | 85 ++- tests/testsuite/registry.rs | 57 +- tests/testsuite/search.rs | 209 +++--- 14 files changed, 723 insertions(+), 613 deletions(-) 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[[..]")