Initial stab at integration tests
authorCarl Lerche <me@carllerche.com>
Wed, 19 Mar 2014 01:10:48 +0000 (18:10 -0700)
committerCarl Lerche <me@carllerche.com>
Thu, 20 Mar 2014 19:12:47 +0000 (12:12 -0700)
At the same time, we started adding a generic error handling concept to
Cargo.

The idea is that every Result gets converted to a CargoError, which
includes all the information that Cargo needs to print out a friendly
human error message and exit gracefully.

14 files changed:
Makefile
libs/hamcrest-rust
libs/rust-toml
src/bin/cargo-compile.rs
src/bin/cargo-read-manifest.rs
src/bin/cargo-rustc.rs
src/bin/cargo-verify-project.rs
src/cargo.rs [deleted file]
src/cargo/mod.rs [new file with mode: 0644]
src/cargo/util/mod.rs [new file with mode: 0644]
src/cargo/util/process_builder.rs [new file with mode: 0644]
tests/support.rs [new file with mode: 0644]
tests/test_cargo_compile.rs [new file with mode: 0644]
tests/tests.rs [new file with mode: 0644]

index af30c5eb0d89424eed4e3b5dba37c9b71193c967..7a2100fd8f268fd7294e2496792018ae5dffa7f0 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,8 @@ BINS = cargo-compile \
           cargo-rustc \
           cargo-verify-project
 
-SRC = $(wildcard src/*.rs)
+SRC = $(shell find src -name '*.rs')
+
 DEPS = -L libs/hammer.rs/target -L libs/rust-toml/lib
 TOML = libs/rust-toml/lib/$(shell rustc --crate-file-name libs/rust-toml/src/toml/lib.rs)
 HAMMER = libs/hammer.rs/target/$(shell rustc --crate-type=lib --crate-file-name libs/hammer.rs/src/hammer.rs)
@@ -25,14 +26,14 @@ $(HAMMER): $(wildcard libs/hammer.rs/src/*.rs)
 $(TOML): $(wildcard libs/rust-toml/src/toml/*.rs)
        cd libs/rust-toml && make
 
-$(HAMCREST): $(wildcard libs/hamcrest-rust/src/*.rs)
+$(HAMCREST): $(wildcard libs/hamcrest-rust/src/hamcrest/*.rs)
        cd libs/hamcrest-rust && make
 
 # === Cargo
 
 $(LIBCARGO): $(SRC)
        mkdir -p target
-       $(RUSTC) $(RUSTC_FLAGS) --out-dir target src/cargo.rs
+       $(RUSTC) $(RUSTC_FLAGS) --out-dir target src/cargo/mod.rs
        touch $(LIBCARGO)
 
 libcargo: $(LIBCARGO)
@@ -42,8 +43,18 @@ libcargo: $(LIBCARGO)
 $(BIN_TARGETS): target/%: src/bin/%.rs $(HAMMER) $(TOML) $(LIBCARGO)
        $(RUSTC) $(RUSTC_FLAGS) $(DEPS) -Ltarget --out-dir target $<
 
-test:
-       echo "testing"
+# === Tests
+
+TEST_SRC = $(wildcard tests/*.rs)
+TEST_DEPS = $(DEPS) -L libs/hamcrest-rust/target
+
+target/tests: $(BIN_TARGETS) $(HAMCREST) $(TEST_SRC)
+       $(RUSTC) --test --crate-type=lib $(TEST_DEPS) -Ltarget --out-dir target tests/tests.rs
+
+test-integration: target/tests
+       CARGO_BIN_PATH=$(PWD)/target/ $<
+
+test: test-integration
 
 clean:
        rm -rf target
@@ -54,7 +65,8 @@ distclean: clean
        cd libs/rust-toml && make clean
 
 # Setup phony tasks
-.PHONY: all clean distclean test libcargo
+.PHONY: all clean distclean test test-integration libcargo
 
 # Disable unnecessary built-in rules
 .SUFFIXES:
+
index 95f531ad8c726a832f28d171bde2860c77ec7619..39f00624492fd648631041eadf1301a08cd2a482 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 95f531ad8c726a832f28d171bde2860c77ec7619
+Subproject commit 39f00624492fd648631041eadf1301a08cd2a482
index 894fdd9db6c50b9a70d1fc7d4e49c76e86921016..1389ceb42b2ae04dac40c8b2d4af8fe21823ecbc 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 894fdd9db6c50b9a70d1fc7d4e49c76e86921016
+Subproject commit 1389ceb42b2ae04dac40c8b2d4af8fe21823ecbc
index 39e885b23549a3b2b15b2db793fbd22d84100383..34ca4ee196d5862ce7126381e802aadf6589f3e2 100644 (file)
@@ -1,4 +1,5 @@
 #[crate_id="cargo-compile"];
+#[allow(deprecated_owned_vector)];
 
 extern crate serialize;
 extern crate hammer;
index e8e224e115b614a3556fb3d302d357a572010637..5e99cf56f73c2bfddbe8e9075e857cac6bfc8569 100644 (file)
@@ -1,4 +1,5 @@
 #[crate_id="cargo-read-manifest"];
+#[allow(deprecated_owned_vector)];
 
 extern crate cargo;
 extern crate hammer;
@@ -9,7 +10,7 @@ use hammer::{FlagDecoder,FlagConfig,FlagConfiguration};
 use serialize::{Decoder,Decodable};
 use serialize::json::Encoder;
 use toml::from_toml;
-use cargo::{Manifest,LibTarget,ExecTarget,Project};
+use cargo::{Manifest,LibTarget,ExecTarget,Project,CargoResult,CargoError,ToCargoError};
 use std::path::Path;
 
 #[deriving(Decodable,Encodable,Eq,Clone,Ord)]
@@ -41,21 +42,32 @@ impl FlagConfig for ReadManifestFlags {
 }
 
 fn main() {
+    match execute() {
+        Err(e) => {
+            println!("{}", e.message);
+            // TODO: Exit with error code
+        },
+        _ => return
+    }
+}
+
+fn execute() -> CargoResult<()> {
     let mut decoder = FlagDecoder::new::<ReadManifestFlags>(std::os::args().tail());
     let flags: ReadManifestFlags = Decodable::decode(&mut decoder);
 
     if decoder.error.is_some() {
-        fail!("Error: {}", decoder.error.unwrap());
+        return Err(CargoError::new(decoder.error.unwrap(), 1));
     }
 
-    let root = toml::parse_from_file(flags.manifest_path).unwrap();
+    let manifest_path = flags.manifest_path;
+    let root = try!(toml::parse_from_file(manifest_path.clone()).to_cargo_error(format!("Couldn't parse Toml file: {}", manifest_path), 1));
 
     let toml_manifest = from_toml::<SerializedManifest>(root.clone());
 
     let (lib, bin) = normalize(&toml_manifest.lib, &toml_manifest.bin);
 
     let manifest = Manifest{
-        root: Path::new(flags.manifest_path).dirname_str().unwrap().to_owned(),
+        root: try!(Path::new(manifest_path.clone()).dirname_str().to_cargo_error(format!("Could not get dirname from {}", manifest_path), 1)).to_owned(),
         project: toml_manifest.project,
         lib: lib,
         bin: bin
@@ -64,6 +76,8 @@ fn main() {
     let encoded: ~str = Encoder::str_encode(&manifest);
 
     println!("{}", encoded);
+
+    Ok(())
 }
 
 fn normalize(lib: &Option<~[SerializedLibTarget]>, bin: &Option<~[SerializedExecTarget]>) -> (~[LibTarget], ~[ExecTarget]) {
@@ -98,7 +112,7 @@ fn normalize(lib: &Option<~[SerializedLibTarget]>, bin: &Option<~[SerializedExec
             let b = b_ref.clone();
             let mut path = b.path.clone();
             if path.is_none() {
-                path = Some(format!("src/bin/{}.rs", b.name.clone()));
+                path = Some(format!("src/{}.rs", b.name.clone()));
             }
             ExecTarget{ path: path.unwrap(), name: b.name }
         });
index 70c62a86bc1332fcae670280cea4a989de06c46c..4e55d37e9c71c14d43ab5d8177755e5306b4fe9d 100644 (file)
@@ -1,4 +1,5 @@
 #[crate_id="cargo-rustc"];
+#[allow(deprecated_owned_vector)];
 
 extern crate toml;
 extern crate serialize;
@@ -10,7 +11,7 @@ use std::io::process::{Process,ProcessConfig,InheritFd};
 use serialize::json;
 use serialize::Decodable;
 use std::path::Path;
-use cargo::Manifest;
+use cargo::{Manifest,CargoResult,CargoError,ToCargoError};
 
 /**
     cargo-rustc -- ...args
@@ -19,23 +20,40 @@ use cargo::Manifest;
 */
 
 fn main() {
+    match execute() {
+        Err(e) => {
+            write!(&mut std::io::stderr(), "{}", e.message);
+            // TODO: Exit with error code
+        },
+        _ => return
+    }
+}
+
+fn execute() -> CargoResult<()> {
     let mut reader = io::stdin();
-    let input = reader.read_to_str().unwrap();
+    let input = try!(reader.read_to_str().to_cargo_error(~"Cannot read stdin to a string", 1));
 
-    let json = json::from_str(input).unwrap();
+    let json = try!(json::from_str(input).to_cargo_error(format!("Cannot parse json: {}", input), 1));
     let mut decoder = json::Decoder::new(json);
     let manifest: Manifest = Decodable::decode(&mut decoder);
 
-    let Manifest{ root, lib, .. } = manifest;
+    let Manifest{ root, lib, bin, .. } = manifest;
+
+    let (crate_type, out_dir) = if lib.len() > 0 {
+        ( ~"lib", lib[0].path )
+    } else if bin.len() > 0 {
+        ( ~"bin", bin[0].path )
+    } else {
+        return Err(CargoError::new(~"bad manifest, no lib or bin specified", 1));
+    };
 
     let root = Path::new(root);
-    let out_dir = lib[0].path;
     let target = join(&root, ~"target");
 
     let args = [
         join(&root, out_dir),
         ~"--out-dir", target,
-        ~"--crate-type", ~"lib"
+        ~"--crate-type", crate_type
     ];
 
     match io::fs::mkdir_recursive(&root.join("target"), io::UserRWX) {
@@ -51,15 +69,25 @@ fn main() {
     config.program = "rustc";
     config.args = args.as_slice();
 
-    let mut p = Process::configure(config).unwrap();
+    let mut p = try!(Process::configure(config).to_cargo_error(format!("Could not start process: rustc {}", args.as_slice()), 1));
 
     let status = p.wait();
 
     if status != std::io::process::ExitStatus(0) {
         fail!("Failed to execute")
     }
+
+    Ok(())
 }
 
 fn join(path: &Path, part: ~str) -> ~str {
-    path.join(part).as_str().unwrap().to_owned()
+    format!("{}", path.join(part).display())
+}
+
+fn vec_idx<T>(v: ~[T], idx: uint) -> Option<T> {
+    if idx < v.len() {
+        Some(v[idx])
+    } else {
+        None
+    }
 }
index 7ee47ae88daea4d71c8dee001e85e52095ae70da..255a585e02672f26cad04a583ad805e22aa5f7b1 100644 (file)
@@ -1,4 +1,5 @@
 #[crate_id="cargo-verify-project"];
+#[allow(deprecated_owned_vector)];
 
 extern crate toml;
 extern crate getopts;
diff --git a/src/cargo.rs b/src/cargo.rs
deleted file mode 100644 (file)
index 684b8c9..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-#[crate_type="rlib"];
-
-extern crate serialize;
-use serialize::{Decoder};
-
-#[deriving(Decodable,Encodable,Eq,Clone,Ord)]
-pub struct Manifest {
-    project: ~Project,
-    root: ~str,
-    lib: ~[LibTarget],
-    bin: ~[ExecTarget]
-}
-
-#[deriving(Decodable,Encodable,Eq,Clone,Ord)]
-pub struct ExecTarget {
-    name: ~str,
-    path: ~str
-}
-
-#[deriving(Decodable,Encodable,Eq,Clone,Ord)]
-pub struct LibTarget {
-    name: ~str,
-    path: ~str
-}
-
-//pub type LibTarget = Target;
-//pub type ExecTarget = Target;
-
-#[deriving(Decodable,Encodable,Eq,Clone,Ord)]
-pub struct Project {
-    name: ~str,
-    version: ~str,
-    authors: ~[~str]
-}
diff --git a/src/cargo/mod.rs b/src/cargo/mod.rs
new file mode 100644 (file)
index 0000000..8ea9d95
--- /dev/null
@@ -0,0 +1,82 @@
+#[crate_id="cargo"];
+#[crate_type="rlib"];
+
+#[allow(deprecated_owned_vector)];
+
+extern crate serialize;
+use serialize::{Decoder};
+use std::fmt;
+use std::fmt::{Show,Formatter};
+
+pub mod util;
+
+#[deriving(Decodable,Encodable,Eq,Clone,Ord)]
+pub struct Manifest {
+    project: ~Project,
+    root: ~str,
+    lib: ~[LibTarget],
+    bin: ~[ExecTarget]
+}
+
+#[deriving(Decodable,Encodable,Eq,Clone,Ord)]
+pub struct ExecTarget {
+    name: ~str,
+    path: ~str
+}
+
+#[deriving(Decodable,Encodable,Eq,Clone,Ord)]
+pub struct LibTarget {
+    name: ~str,
+    path: ~str
+}
+
+//pub type LibTarget = Target;
+//pub type ExecTarget = Target;
+
+#[deriving(Decodable,Encodable,Eq,Clone,Ord)]
+pub struct Project {
+    name: ~str,
+    version: ~str,
+    authors: ~[~str]
+}
+
+pub type CargoResult<T> = Result<T, CargoError>;
+
+pub struct CargoError {
+    message: ~str,
+    exit_code: uint
+}
+
+impl CargoError {
+    pub fn new(message: ~str, exit_code: uint) -> CargoError {
+        CargoError { message: message, exit_code: exit_code }
+    }
+}
+
+impl Show for CargoError {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        write!(f.buf, "{}", self.message)
+    }
+}
+
+pub trait ToCargoError<T> {
+    fn to_cargo_error(self, message: ~str, exit_code: uint) -> Result<T, CargoError>;
+}
+
+impl<T,U> ToCargoError<T> for Result<T,U> {
+    fn to_cargo_error(self, message: ~str, exit_code: uint) -> Result<T, CargoError> {
+        match self {
+            Err(_) => Err(CargoError{ message: message, exit_code: exit_code }),
+            Ok(val) => Ok(val)
+        }
+    }
+}
+
+impl<T> ToCargoError<T> for Option<T> {
+    fn to_cargo_error(self, message: ~str, exit_code: uint) -> CargoResult<T> {
+        match self {
+            None => Err(CargoError{ message: message, exit_code: exit_code }),
+            Some(val) => Ok(val)
+        }
+    }
+}
diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs
new file mode 100644 (file)
index 0000000..27784a1
--- /dev/null
@@ -0,0 +1,2 @@
+pub use self::process_builder::process;
+pub mod process_builder;
diff --git a/src/cargo/util/process_builder.rs b/src/cargo/util/process_builder.rs
new file mode 100644 (file)
index 0000000..821fa47
--- /dev/null
@@ -0,0 +1,63 @@
+use std;
+use std::os;
+use std::path::Path;
+use std::io::IoResult;
+use std::io::process::{Process,ProcessConfig,ProcessOutput};
+use ToCargoError;
+use CargoResult;
+
+pub struct ProcessBuilder {
+  program: ~str,
+  args: ~[~str],
+  path: ~[~str],
+  cwd: Path
+}
+
+// TODO: Upstream a Windows/Posix branch to Rust proper
+static PATH_SEP : &'static str = ":";
+
+impl ProcessBuilder {
+  pub fn args(mut self, arguments: &[~str]) -> ProcessBuilder {
+    self.args = arguments.to_owned();
+    self
+  }
+
+  pub fn extra_path(mut self, path: &str) -> ProcessBuilder {
+    self.path.push(path.to_owned());
+    self
+  }
+
+  pub fn cwd(mut self, path: Path) -> ProcessBuilder {
+    self.cwd = path;
+    self
+  }
+
+  pub fn exec_with_output(self) -> CargoResult<ProcessOutput> {
+    let mut config = ProcessConfig::new();
+
+    println!("cwd: {}", self.cwd.display());
+
+    config.program = self.program.as_slice();
+    config.args = self.args.as_slice();
+    config.cwd = Some(&self.cwd);
+
+    let os_path = try!(os::getenv("PATH").to_cargo_error(~"Could not find the PATH environment variable", 1));
+    let path = os_path + PATH_SEP + self.path.connect(PATH_SEP);
+
+    let path = [(~"PATH", path)];
+    config.env = Some(path.as_slice());
+
+    println!("{:?}", config);
+
+    Process::configure(config).map(|mut ok| ok.wait_with_output()).to_cargo_error(~"Could not spawn process", 1)
+  }
+}
+
+pub fn process(cmd: &str) -> ProcessBuilder {
+  ProcessBuilder {
+    program: cmd.to_owned(),
+    args: ~[],
+    path: ~[],
+    cwd: os::getcwd()
+  }
+}
diff --git a/tests/support.rs b/tests/support.rs
new file mode 100644 (file)
index 0000000..0c5242d
--- /dev/null
@@ -0,0 +1,123 @@
+// use std::io::fs::{mkdir_recursive,rmdir_recursive};
+use std::io::fs;
+use std::os::tmpdir;
+use std::path::{Path};
+
+static CARGO_INTEGRATION_TEST_DIR : &'static str = "cargo-integration-tests";
+static MKDIR_PERM : u32 = 0o755;
+
+#[deriving(Eq,Clone)]
+struct FileBuilder {
+    path: Path,
+    body: ~str
+}
+
+impl FileBuilder {
+    pub fn new(path: Path, body: &str) -> FileBuilder {
+        FileBuilder { path: path, body: body.to_owned() }
+    }
+
+    fn mk(&self) -> Result<(), ~str> {
+        try!(mkdir_recursive(&self.dirname()));
+
+        let mut file = try!(
+            fs::File::create(&self.path)
+                .with_err_msg(format!("Could not create file; path={}", self.path.display())));
+
+        file.write_str(self.body.as_slice())
+            .with_err_msg(format!("Could not write to file; path={}", self.path.display()))
+    }
+
+    fn dirname(&self) -> Path {
+        Path::new(self.path.dirname())
+    }
+}
+
+#[deriving(Eq,Clone)]
+struct ProjectBuilder {
+    name: ~str,
+    root: Path,
+    files: ~[FileBuilder]
+}
+
+impl ProjectBuilder {
+    pub fn new(name: &str, root: Path) -> ProjectBuilder {
+        ProjectBuilder {
+            name: name.to_owned(),
+            root: root,
+            files: ~[]
+        }
+    }
+
+    pub fn root(&self) -> Path {
+      self.root.clone()
+    }
+
+    pub fn file(mut self, path: &str, body: &str) -> ProjectBuilder {
+        self.files.push(FileBuilder::new(self.root.join(path), body));
+        self
+    }
+
+    // TODO: return something different than a ProjectBuilder
+    pub fn build(self) -> ProjectBuilder {
+        match self.build_with_result() {
+            Err(e) => fail!(e),
+            _ => return self
+        }
+    }
+
+    pub fn build_with_result(&self) -> Result<(), ~str> {
+        // First, clean the directory if it already exists
+        try!(self.rm_root());
+
+        // Create the empty directory
+        try!(mkdir_recursive(&self.root));
+
+        for file in self.files.iter() {
+          try!(file.mk());
+        }
+
+        println!("{}", self.root.display());
+        println!("{:?}", self);
+        Ok(())
+    }
+
+    fn rm_root(&self) -> Result<(), ~str> {
+        if self.root.exists() {
+            rmdir_recursive(&self.root)
+        }
+        else {
+            Ok(())
+        }
+    }
+}
+
+// Generates a project layout
+pub fn project(name: &str) -> ProjectBuilder {
+    ProjectBuilder::new(name, tmpdir().join(CARGO_INTEGRATION_TEST_DIR))
+}
+
+// === Helpers ===
+
+pub fn mkdir_recursive(path: &Path) -> Result<(), ~str> {
+    fs::mkdir_recursive(path, MKDIR_PERM)
+        .with_err_msg(format!("could not create directory; path={}", path.display()))
+}
+
+pub fn rmdir_recursive(path: &Path) -> Result<(), ~str> {
+    fs::rmdir_recursive(path)
+        .with_err_msg(format!("could not rm directory; path={}", path.display()))
+}
+
+trait ErrMsg<T> {
+    fn with_err_msg(self, val: ~str) -> Result<T, ~str>;
+}
+
+impl<T, E> ErrMsg<T> for Result<T, E> {
+    fn with_err_msg(self, val: ~str) -> Result<T, ~str> {
+        match self {
+            Ok(val) => Ok(val),
+            Err(_) => Err(val)
+        }
+    }
+}
diff --git a/tests/test_cargo_compile.rs b/tests/test_cargo_compile.rs
new file mode 100644 (file)
index 0000000..3cdc563
--- /dev/null
@@ -0,0 +1,83 @@
+use std;
+use support::project;
+use hamcrest::{SelfDescribing,Description,Matcher,assert_that};
+use cargo;
+
+#[deriving(Clone,Eq)]
+pub struct ExistingFile;
+
+impl SelfDescribing for ExistingFile {
+  fn describe_to(&self, desc: &mut Description) {
+    desc.append_text("an existing file");
+  }
+}
+
+impl Matcher<Path> for ExistingFile {
+  fn matches(&self, actual: &Path) -> bool {
+    actual.exists()
+  }
+
+  fn describe_mismatch(&self, actual: &Path, desc: &mut Description) {
+    desc.append_text(format!("`{}` was missing", actual.display()));
+  }
+}
+
+pub fn existing_file() -> ExistingFile {
+  ExistingFile
+}
+
+fn setup() {
+
+}
+
+test!(cargo_compile_with_explicit_manifest_path {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [project]
+
+            name = "foo"
+            version = "0.5.0"
+            authors = ["wycats@example.com"]
+
+            [[bin]]
+
+            name = "foo"
+        "#)
+        .file("src/foo.rs", r#"
+            fn main() {
+                println!("i am foo");
+            }"#)
+        .build();
+
+    let output = cargo::util::process("cargo-compile")
+      .args([~"--manifest-path", ~"Cargo.toml"])
+      .extra_path(target_path())
+      .cwd(p.root())
+      .exec_with_output();
+
+    match output {
+      Ok(out) => {
+        println!("out:\n{}\n", std::str::from_utf8(out.output));
+        println!("err:\n{}\n", std::str::from_utf8(out.error));
+      },
+      Err(e) => println!("err: {}", e)
+    }
+
+    assert_that(p.root().join("target/foo/bar"), existing_file());
+    assert!(p.root().join("target/foo").exists(), "the executable exists");
+
+    let o = cargo::util::process("foo")
+      .extra_path(format!("{}", p.root().join("target").display()))
+      .exec_with_output()
+      .unwrap();
+
+    assert_eq!(std::str::from_utf8(o.output).unwrap(), "i am foo\n");
+})
+
+// test!(compiling_project_with_invalid_manifest)
+
+fn target_path() -> ~str {
+  std::os::getenv("CARGO_BIN_PATH").unwrap_or_else(|| {
+    fail!("CARGO_BIN_PATH wasn't set. Cannot continue running test")
+  })
+}
diff --git a/tests/tests.rs b/tests/tests.rs
new file mode 100644 (file)
index 0000000..3c74087
--- /dev/null
@@ -0,0 +1,18 @@
+#[feature(macro_rules)];
+#[allow(deprecated_owned_vector)];
+
+extern crate cargo;
+extern crate hamcrest;
+
+macro_rules! test(
+    ($name:ident $expr:expr) => (
+        #[test]
+        fn $name() {
+            setup();
+            $expr;
+        }
+    )
+)
+
+mod support;
+mod test_cargo_compile;