From ec5f78f93326fafc745b789c156d9adeb328502e Mon Sep 17 00:00:00 2001 From: Pelepeichenko Alexander Date: Mon, 25 Dec 2017 01:32:29 +0200 Subject: [PATCH] add offline mode (-Z offline) with tests --- src/cargo/core/features.rs | 2 + src/cargo/core/resolver/mod.rs | 14 +- src/cargo/ops/cargo_generate_lockfile.rs | 4 + src/cargo/ops/lockfile.rs | 4 + src/cargo/ops/registry.rs | 5 +- src/cargo/sources/git/source.rs | 6 +- src/cargo/sources/git/utils.rs | 5 +- src/cargo/sources/registry/index.rs | 9 +- src/cargo/sources/registry/mod.rs | 2 + src/cargo/sources/registry/remote.rs | 18 ++ src/cargo/util/config.rs | 6 +- tests/build.rs | 213 +++++++++++++++++++++++ tests/git.rs | 138 +++++++++++++++ tests/registry.rs | 20 +++ 14 files changed, 438 insertions(+), 8 deletions(-) diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 4cb5ac5e4..3add03fcc 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -232,6 +232,7 @@ impl Features { pub struct CliUnstable { pub print_im_a_teapot: bool, pub unstable_options: bool, + pub offline: bool, } impl CliUnstable { @@ -262,6 +263,7 @@ impl CliUnstable { match k { "print-im-a-teapot" => self.print_im_a_teapot = parse_bool(v)?, "unstable-options" => self.unstable_options = true, + "offline" => self.offline = true, _ => bail!("unknown `-Z` flag specified: {}", k), } diff --git a/src/cargo/core/resolver/mod.rs b/src/cargo/core/resolver/mod.rs index cfefbf489..e59c73599 100644 --- a/src/cargo/core/resolver/mod.rs +++ b/src/cargo/core/resolver/mod.rs @@ -722,7 +722,7 @@ fn activate_deps_loop<'a>(mut cx: Context<'a>, None => return Err(activation_error(&cx, registry, &parent, &dep, cx.prev_active(&dep), - &candidates)), + &candidates, config)), Some(candidate) => candidate, } } @@ -788,7 +788,8 @@ fn activation_error(cx: &Context, parent: &Summary, dep: &Dependency, prev_active: &[Summary], - candidates: &[Candidate]) -> CargoError { + candidates: &[Candidate], + config: Option<&Config>) -> CargoError { if !candidates.is_empty() { let mut msg = format!("failed to select a version for `{}` \ (required by `{}`):\n\ @@ -843,7 +844,7 @@ fn activation_error(cx: &Context, b.version().cmp(a.version()) }); - let msg = if !candidates.is_empty() { + let mut msg = if !candidates.is_empty() { let versions = { let mut versions = candidates.iter().take(3).map(|cand| { cand.version().to_string() @@ -886,6 +887,13 @@ fn activation_error(cx: &Context, dep.version_req()) }; + if let Some(config) = config { + if config.cli_unstable().offline { + msg.push_str("\nperhaps an error occurred because you are using \ + the offline mode"); + } + } + format_err!("{}", msg) } diff --git a/src/cargo/ops/cargo_generate_lockfile.rs b/src/cargo/ops/cargo_generate_lockfile.rs index 0d6ebefb7..f87473709 100644 --- a/src/cargo/ops/cargo_generate_lockfile.rs +++ b/src/cargo/ops/cargo_generate_lockfile.rs @@ -37,6 +37,10 @@ pub fn update_lockfile(ws: &Workspace, opts: &UpdateOptions) bail!("you can't generate a lockfile for an empty workspace.") } + if opts.config.cli_unstable().offline { + bail!("you can't update in the offline mode"); + } + let previous_resolve = match ops::load_pkg_lockfile(ws)? { Some(resolve) => resolve, None => return generate_lockfile(ws), diff --git a/src/cargo/ops/lockfile.rs b/src/cargo/ops/lockfile.rs index 73961fde0..4f3a0b2a2 100644 --- a/src/cargo/ops/lockfile.rs +++ b/src/cargo/ops/lockfile.rs @@ -76,6 +76,10 @@ pub fn write_pkg_lockfile(ws: &Workspace, resolve: &Resolve) -> CargoResult<()> } if !ws.config().lock_update_allowed() { + if ws.config().cli_unstable().offline { + bail!("can't update in the offline mode"); + } + let flag = if ws.config().network_allowed() {"--locked"} else {"--frozen"}; bail!("the lock file needs to be updated but {} was passed to \ prevent this", flag); diff --git a/src/cargo/ops/registry.rs b/src/cargo/ops/registry.rs index 3d53c5950..922a3b6ab 100644 --- a/src/cargo/ops/registry.rs +++ b/src/cargo/ops/registry.rs @@ -263,10 +263,13 @@ pub fn registry(config: &Config, /// Create a new HTTP handle with appropriate global configuration for cargo. pub fn http_handle(config: &Config) -> CargoResult { - if !config.network_allowed() { + if config.frozen() { bail!("attempting to make an HTTP request, but --frozen was \ specified") } + if !config.network_allowed() { + bail!("can't make HTTP request in the offline mode") + } // The timeout option for libcurl by default times out the entire transfer, // but we probably don't want this. Instead we only set timeouts for the diff --git a/src/cargo/sources/git/source.rs b/src/cargo/sources/git/source.rs index 058c8e911..3862266de 100644 --- a/src/cargo/sources/git/source.rs +++ b/src/cargo/sources/git/source.rs @@ -151,6 +151,10 @@ impl<'cfg> Source for GitSource<'cfg> { let db_path = lock.parent().join("db").join(&self.ident); + if self.config.cli_unstable().offline && !db_path.exists() { + bail!("can't checkout from '{}': you are in the offline mode", self.remote.url()); + } + // Resolve our reference to an actual revision, and check if the // database already has that revision. If it does, we just load a // database pinned at that revision, and if we don't we issue an update @@ -159,7 +163,7 @@ impl<'cfg> Source for GitSource<'cfg> { let should_update = actual_rev.is_err() || self.source_id.precise().is_none(); - let (db, actual_rev) = if should_update { + let (db, actual_rev) = if should_update && !self.config.cli_unstable().offline { self.config.shell().status("Updating", format!("git repository `{}`", self.remote.url()))?; diff --git a/src/cargo/sources/git/utils.rs b/src/cargo/sources/git/utils.rs index 7c3242426..640138525 100644 --- a/src/cargo/sources/git/utils.rs +++ b/src/cargo/sources/git/utils.rs @@ -615,10 +615,13 @@ pub fn fetch(repo: &mut git2::Repository, url: &Url, refspec: &str, config: &Config) -> CargoResult<()> { - if !config.network_allowed() { + if config.frozen() { bail!("attempting to update a git repository, but --frozen \ was specified") } + if !config.network_allowed() { + bail!("can't update a git repository in the offline mode") + } // If we're fetching from github, attempt github's special fast path for // testing if we've already got an up-to-date copy of the repository diff --git a/src/cargo/sources/registry/index.rs b/src/cargo/sources/registry/index.rs index 4f92238c1..f2e993244 100644 --- a/src/cargo/sources/registry/index.rs +++ b/src/cargo/sources/registry/index.rs @@ -110,13 +110,20 @@ impl<'cfg> RegistryIndex<'cfg> { .map(|s| s.trim()) .filter(|l| !l.is_empty()); + let online = !self.config.cli_unstable().offline; // Attempt forwards-compatibility on the index by ignoring // everything that we ourselves don't understand, that should // allow future cargo implementations to break the // interpretation of each line here and older cargo will simply // ignore the new lines. ret.extend(lines.filter_map(|line| { - self.parse_registry_package(line).ok() + self.parse_registry_package(line).ok().and_then(|v|{ + if online || load.is_crate_downloaded(v.0.package_id()) { + Some(v) + } else { + None + } + }) })); Ok(()) diff --git a/src/cargo/sources/registry/mod.rs b/src/cargo/sources/registry/mod.rs index 16269a11d..3ef6e67fd 100644 --- a/src/cargo/sources/registry/mod.rs +++ b/src/cargo/sources/registry/mod.rs @@ -249,6 +249,8 @@ pub trait RegistryData { fn download(&mut self, pkg: &PackageId, checksum: &str) -> CargoResult; + + fn is_crate_downloaded(&self, _pkg: &PackageId) -> bool { true } } mod index; diff --git a/src/cargo/sources/registry/remote.rs b/src/cargo/sources/registry/remote.rs index a2a0f9402..f21e54fa5 100644 --- a/src/cargo/sources/registry/remote.rs +++ b/src/cargo/sources/registry/remote.rs @@ -153,6 +153,10 @@ impl<'cfg> RegistryData for RemoteRegistry<'cfg> { } fn update_index(&mut self) -> CargoResult<()> { + if self.config.cli_unstable().offline { + return Ok(()); + } + // Ensure that we'll actually be able to acquire an HTTP handle later on // once we start trying to download crates. This will weed out any // problems with `.cargo/config` configuration related to HTTP. @@ -258,6 +262,20 @@ impl<'cfg> RegistryData for RemoteRegistry<'cfg> { dst.seek(SeekFrom::Start(0))?; Ok(dst) } + + + fn is_crate_downloaded(&self, pkg: &PackageId) -> bool { + let filename = format!("{}-{}.crate", pkg.name(), pkg.version()); + let path = Path::new(&filename); + + if let Ok(dst) = self.cache_path.open_ro(path, self.config, &filename) { + if let Ok(meta) = dst.file().metadata(){ + return meta.len() > 0; + } + } + false + } + } impl<'cfg> Drop for RemoteRegistry<'cfg> { diff --git a/src/cargo/util/config.rs b/src/cargo/util/config.rs index 77577ab30..daf176f45 100644 --- a/src/cargo/util/config.rs +++ b/src/cargo/util/config.rs @@ -504,7 +504,11 @@ impl Config { } pub fn network_allowed(&self) -> bool { - !self.frozen + !self.frozen() && !self.cli_unstable().offline + } + + pub fn frozen(&self) -> bool { + self.frozen } pub fn lock_update_allowed(&self) -> bool { diff --git a/tests/build.rs b/tests/build.rs index b9b2c6d4c..bf4348b85 100644 --- a/tests/build.rs +++ b/tests/build.rs @@ -15,6 +15,7 @@ use cargotest::support::paths::{CargoPathExt,root}; use cargotest::support::{ProjectBuilder}; use cargotest::support::{project, execs, main_file, basic_bin_manifest}; use cargotest::support::registry::Package; +use cargotest::ChannelChanger; use hamcrest::{assert_that, existing_file, existing_dir, is_not}; use tempdir::TempDir; @@ -829,6 +830,218 @@ Did you mean `a`?")); Did you mean `a`?")); } +#[test] +fn cargo_compile_path_with_offline() { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [dependencies.bar] + path = "bar" + "#) + .file("src/lib.rs", "") + .file("bar/Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + "#) + .file("bar/src/lib.rs", "") + .build(); + + assert_that(p.cargo("build").masquerade_as_nightly_cargo().arg("-Zoffline"), + execs().with_status(0)); +} + +#[test] +fn cargo_compile_with_downloaded_dependency_with_offline() { + Package::new("present_dep", "1.2.3") + .file("Cargo.toml", r#" + [project] + name = "present_dep" + version = "1.2.3" + "#) + .file("src/lib.rs", "") + .publish(); + + { + // make package downloaded + let p = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.1.0" + + [dependencies] + present_dep = "1.2.3" + "#) + .file("src/lib.rs", "") + .build(); + assert_that(p.cargo("build"),execs().with_status(0)); + } + + let p2 = project("bar") + .file("Cargo.toml", r#" + [project] + name = "bar" + version = "0.1.0" + + [dependencies] + present_dep = "1.2.3" + "#) + .file("src/lib.rs", "") + .build(); + + assert_that(p2.cargo("build").masquerade_as_nightly_cargo().arg("-Zoffline"), + execs().with_status(0) + .with_stderr_does_not_contain("Updating registry") + .with_stderr_does_not_contain("Downloading") + .with_stderr(format!("\ +[COMPILING] present_dep v1.2.3 +[COMPILING] bar v0.1.0 ({url}) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", + url = p2.url()))); + +} + +#[test] +fn cargo_compile_offline_not_try_update() { + let p = project("bar") + .file("Cargo.toml", r#" + [project] + name = "bar" + version = "0.1.0" + + [dependencies] + not_cached_dep = "1.2.5" + "#) + .file("src/lib.rs", "") + .build(); + + assert_that(p.cargo("build").masquerade_as_nightly_cargo().arg("-Zoffline"), + execs().with_status(101) + .with_stderr_does_not_contain("Updating registry") + .with_stderr_does_not_contain("Downloading") + .with_stderr("\ +error: no matching package named `not_cached_dep` found (required by `bar`) +location searched: registry `[..]` +version required: ^1.2.5 +perhaps an error occurred because you are using the offline mode")); +} + +#[test] +fn compile_offline_without_maxvers_cached(){ + Package::new("present_dep", "1.2.1").publish(); + Package::new("present_dep", "1.2.2").publish(); + + Package::new("present_dep", "1.2.3") + .file("Cargo.toml", r#" + [project] + name = "present_dep" + version = "1.2.3" + "#) + .file("src/lib.rs", r#"pub fn get_version()->&'static str {"1.2.3"}"#) + .publish(); + + Package::new("present_dep", "1.2.5") + .file("Cargo.toml", r#" + [project] + name = "present_dep" + version = "1.2.5" + "#) + .file("src/lib.rs", r#"pub fn get_version(){"1.2.5"}"#) + .publish(); + + { + // make package cached + let p = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.1.0" + + [dependencies] + present_dep = "=1.2.3" + "#) + .file("src/lib.rs", "") + .build(); + assert_that(p.cargo("build"),execs().with_status(0)); + } + + let p2 = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.1.0" + + [dependencies] + present_dep = "1.2" + "#) + .file("src/main.rs", "\ +extern crate present_dep; +fn main(){ + println!(\"{}\", present_dep::get_version()); +}") + .build(); + + assert_that(p2.cargo("build").masquerade_as_nightly_cargo().arg("-Zoffline"), + execs().with_status(0) + .with_stderr(format!("\ +[COMPILING] present_dep v1.2.3 +[COMPILING] foo v0.1.0 ({url}) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", + url = p2.url()))); + + assert_that(process(&p2.bin("foo")), + execs().with_status(0).with_stdout("1.2.3")); +} + +#[test] +fn compile_offline_while_transitive_dep_not_cached() { + let bar = Package::new("bar", "1.0.0"); + let bar_path = bar.archive_dst(); + bar.publish(); + + let mut content = Vec::new(); + + let mut file = File::open(bar_path.clone()).ok().unwrap(); + let _ok = file.read_to_end(&mut content).ok().unwrap(); + drop(file); + drop(File::create(bar_path.clone()).ok().unwrap() ); + + Package::new("foo", "0.1.0").dep("bar", "1.0.0").publish(); + + let p = project("transitive_load_test") + .file("Cargo.toml", r#" + [project] + name = "transitive_load_test" + version = "0.0.1" + + [dependencies] + foo = "0.1.0" + "#) + .file("src/main.rs", "fn main(){}") + .build(); + + // simulate download foo, but fail to download bar + let _out = p.cargo("build").exec_with_output(); + + drop( File::create(bar_path).ok().unwrap().write_all(&content) ); + + assert_that(p.cargo("build").masquerade_as_nightly_cargo().arg("-Zoffline"), + execs().with_status(101) + .with_stderr_does_not_contain("Updating registry") + .with_stderr_does_not_contain("Downloading") + .with_stderr("\ +error: no matching package named `bar` found (required by `foo`) +location searched: registry `[..]` +version required: = 1.0.0 +perhaps an error occurred because you are using the offline mode")); +} + #[test] fn compile_path_dep_then_change_version() { let p = project("foo") diff --git a/tests/git.rs b/tests/git.rs index ce2836a78..8c9edf75f 100644 --- a/tests/git.rs +++ b/tests/git.rs @@ -15,7 +15,9 @@ use cargo::util::process; use cargotest::sleep_ms; use cargotest::support::paths::{self, CargoPathExt}; use cargotest::support::{git, project, execs, main_file, path2url}; +use cargotest::ChannelChanger; use hamcrest::{assert_that,existing_file}; +use hamcrest::matchers::regex::matches_regex; #[test] fn cargo_compile_simple_git_dep() { @@ -75,6 +77,142 @@ fn cargo_compile_simple_git_dep() { execs().with_stdout("hello world\n")); } +#[test] +fn cargo_compile_forbird_git_httpsrepo_offline() { + + let p = project("need_remote_repo") + .file("Cargo.toml", r#" + + [project] + name = "need_remote_repo" + version = "0.5.0" + authors = ["chabapok@example.com"] + + [dependencies.dep1] + git = 'https://github.com/some_user/dep1.git' + "#) + .file("src/main.rs", "") + .build(); + + + assert_that(p.cargo("build").masquerade_as_nightly_cargo().arg("-Zoffline"), + execs().with_status(101). + with_stderr_does_not_contain("[UPDATING] git repository [..]"). + with_stderr("\ +error: failed to load source for a dependency on `dep1` + +Caused by: + Unable to update https://github.com/some_user/dep1.git + +Caused by: + can't checkout from 'https://github.com/some_user/dep1.git': you are in the offline mode")); +} + + +#[test] +fn cargo_compile_offline_with_cached_git_dep() { + let git_project = git::new("dep1", |project| { + project + .file("Cargo.toml", r#" + [project] + name = "dep1" + version = "0.5.0" + authors = ["chabapok@example.com"] + + [lib] + name = "dep1""#) + .file("src/lib.rs", r#" + pub static COOL_STR:&str = "cached git repo rev1"; + "#) + }).unwrap(); + + let repo = git2::Repository::open(&git_project.root()).unwrap(); + let rev1 = repo.revparse_single("HEAD").unwrap().id(); + + // Commit the changes and make sure we trigger a recompile + File::create(&git_project.root().join("src/lib.rs")).unwrap().write_all(br#" + pub static COOL_STR:&str = "cached git repo rev2"; + "#).unwrap(); + git::add(&repo); + let rev2 = git::commit(&repo); + + { + // cache to regisrty rev1 and rev2 + let prj = project("cache_git_dep") + .file("Cargo.toml", &format!(r#" + [project] + name = "cache_git_dep" + version = "0.5.0" + + [dependencies.dep1] + git = '{}' + rev = "{}" + "#, git_project.url(), rev1.clone())) + .file("src/main.rs", "fn main(){}") + .build(); + assert_that(prj.cargo("build"), execs().with_status(0)); + + File::create(&prj.root().join("Cargo.toml")).unwrap().write_all( + &format!(r#" + [project] + name = "cache_git_dep" + version = "0.5.0" + + [dependencies.dep1] + git = '{}' + rev = "{}" + "#, git_project.url(), rev2.clone()).as_bytes() + ).unwrap(); + assert_that(prj.cargo("build"), execs().with_status(0)); + } + + let project = project("foo") + .file("Cargo.toml", &format!(r#" + [project] + name = "foo" + version = "0.5.0" + + [dependencies.dep1] + git = '{}' + "#, git_project.url())) + .file("src/main.rs", &main_file(r#""hello from {}", dep1::COOL_STR"#, &["dep1"])) + .build(); + + let root = project.root(); + let git_root = git_project.root(); + + assert_that(project.cargo("build").masquerade_as_nightly_cargo().arg("-Zoffline"), + execs().with_stderr(format!("\ +[COMPILING] dep1 v0.5.0 ({}#[..]) +[COMPILING] foo v0.5.0 ({}) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]", + path2url(git_root), + path2url(root) + ))); + + assert_that(&project.bin("foo"), existing_file()); + + assert_that(process(&project.bin("foo")), + execs().with_stdout("hello from cached git repo rev2\n")); + + drop( File::create(&project.root().join("Cargo.toml")).unwrap() + .write_all(&format!(r#" + [project] + name = "foo" + version = "0.5.0" + + [dependencies.dep1] + git = '{}' + rev = "{}" + "#, git_project.url(), rev1).as_bytes()).unwrap() ); + + let _out = project.cargo("build").masquerade_as_nightly_cargo() + .arg("-Zoffline").exec_with_output(); + assert_that(process(&project.bin("foo")), + execs().with_stdout("hello from cached git repo rev1\n")); +} + + #[test] fn cargo_compile_git_dep_branch() { let project = project("foo"); diff --git a/tests/registry.rs b/tests/registry.rs index 862e1bcb2..a5de44f53 100644 --- a/tests/registry.rs +++ b/tests/registry.rs @@ -552,6 +552,26 @@ fn update_lockfile() { ")); } +#[test] +fn update_offline(){ + use cargotest::ChannelChanger; + let p = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.0.1" + authors = [] + + [dependencies] + bar = "*" + "#) + .file("src/main.rs", "fn main() {}") + .build(); + assert_that(p.cargo("update").masquerade_as_nightly_cargo().arg("-Zoffline"), + execs().with_status(101). + with_stderr("error: you can't update in the offline mode[..]")); +} + #[test] fn dev_dependency_not_used() { let p = project("foo")