add offline mode (-Z offline) with tests
authorPelepeichenko Alexander <chabapok@gmail.com>
Sun, 24 Dec 2017 23:32:29 +0000 (01:32 +0200)
committerPelepeichenko Alexander <chabapok@gmail.com>
Mon, 8 Jan 2018 19:31:39 +0000 (21:31 +0200)
14 files changed:
src/cargo/core/features.rs
src/cargo/core/resolver/mod.rs
src/cargo/ops/cargo_generate_lockfile.rs
src/cargo/ops/lockfile.rs
src/cargo/ops/registry.rs
src/cargo/sources/git/source.rs
src/cargo/sources/git/utils.rs
src/cargo/sources/registry/index.rs
src/cargo/sources/registry/mod.rs
src/cargo/sources/registry/remote.rs
src/cargo/util/config.rs
tests/build.rs
tests/git.rs
tests/registry.rs

index 4cb5ac5e44b09d92fc0dcd27d821638fad20d1b3..3add03fcc3873314990960ac86e655100d7aee1f 100644 (file)
@@ -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),
         }
 
index cfefbf48992c08a7dcaf64e2cee3f2de2b4326b2..e59c735990cd53287df423b0620c9b9afa5c8abe 100644 (file)
@@ -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)
 }
 
index 0d6ebefb7268d29217761da5344c1669986e61f8..f87473709ffaf180882ef668e6b76bf13e9569f0 100644 (file)
@@ -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),
index 73961fde0068ca9498bc5ebe76737b96b2e0e4b2..4f3a0b2a2aa92ec55be041689dd42e1a0219c702 100644 (file)
@@ -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);
index 3d53c59507a6cf337b70d8bc627b1d74e7c69ed7..922a3b6ab4e7fa3ba49c62eba7a21f9d840f0936 100644 (file)
@@ -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<Easy> {
-    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
index 058c8e9113fa4c4065c8997c6b6ef826b73c80b7..3862266de5743e4a8fec552726f38fed1a0ef9ca 100644 (file)
@@ -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()))?;
 
index 7c32424267d947ce265bf904e703409263b06c0b..640138525ee93f2c98a3e5cb506cd4c2a204f5d7 100644 (file)
@@ -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
index 4f92238c17fe745e43cd539ba406a6f4d2707ccc..f2e9932443d047677217bcb843e0aed34b79f317 100644 (file)
@@ -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(())
index 16269a11d0abe89cded7df0396582875be486ca8..3ef6e67fd3c4e86e7c56ec49f7363f7e278ad620 100644 (file)
@@ -249,6 +249,8 @@ pub trait RegistryData {
     fn download(&mut self,
                 pkg: &PackageId,
                 checksum: &str) -> CargoResult<FileLock>;
+
+    fn is_crate_downloaded(&self, _pkg: &PackageId) -> bool { true }
 }
 
 mod index;
index a2a0f9402739ae9150878afc8cb84ce33a8ddac8..f21e54fa58edc53423a25461e95b65f7cc4c34d3 100644 (file)
@@ -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> {
index 77577ab3077f1d2b896507d01cfeaafe0523b22d..daf176f4582937809895c327ffb5570250198830 100644 (file)
@@ -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 {
index b9b2c6d4cc7deebd64e52d105eb9dc2667b19c62..bf4348b8520d92607a1a29395731e1fee1eb1ab9 100644 (file)
@@ -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")
index ce2836a7843e994af51f1dbc0bb7b6ef506143a9..8c9edf75f57e21a366476ff3314f730a423060f8 100644 (file)
@@ -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");
index 862e1bcb28fb7e5e9044737f10b3949d9c2b6d3e..a5de44f53d52eca2dfdc6a17df618dff23b95a28 100644 (file)
@@ -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")