diff --git a/.travis.yml b/.travis.yml index 50426be38..37e469094 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,11 @@ jobs: - stage: test rust: stable script: | - cargo test --all + # the unit tests for all crates (without mocking the network) + # run single-threaded because some tests change environment variables, which is not thread-safe + cargo test --package notion-core --package notion-fail --package node-archive --package notion-fail-derive --package progress-read -- --test-threads=1 && + # the acceptance tests, using network mocks + cargo test --features mock-network - stage: publish # Conditional builds: https://docs.travis-ci.com/user/conditional-builds-stages-jobs/ if: (branch = master) AND (type = push) diff --git a/Cargo.lock b/Cargo.lock index 1e7847fbc..28c22e7a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,11 +139,27 @@ dependencies = [ "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cmdline_words_parser" version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "colored" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "console" version = "0.6.1" @@ -228,6 +244,11 @@ dependencies = [ "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "docopt" version = "0.8.3" @@ -235,7 +256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", "strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -378,6 +399,24 @@ dependencies = [ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "hamcrest2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "http-muncher" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "httparse" version = "1.2.4" @@ -431,6 +470,11 @@ dependencies = [ "unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "indexmap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "indicatif" version = "0.9.0" @@ -456,6 +500,11 @@ name = "itoa" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "itoa" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -505,11 +554,6 @@ dependencies = [ "crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "linked-hash-map" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "log" version = "0.3.9" @@ -616,6 +660,20 @@ dependencies = [ "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "mockito" +version = "0.13.0" +source = "git+https://github.com/lipanski/mockito?rev=48c5a93bcf8cc434875ed8aed22bff9623cb1ff4#48c5a93bcf8cc434875ed8aed22bff9623cb1ff4" +dependencies = [ + "colored 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http-muncher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.20 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "msdos_time" version = "0.1.5" @@ -672,17 +730,24 @@ dependencies = [ name = "notion" version = "0.1.4" dependencies = [ + "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "console 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "docopt 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hamcrest2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "mockito 0.13.0 (git+https://github.com/lipanski/mockito?rev=48c5a93bcf8cc434875ed8aed22bff9623cb1ff4)", "notion-core 0.1.0", "notion-fail 0.1.0", "notion-fail-derive 0.1.0", + "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)", "result 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", + "test-support 0.1.0", ] [[package]] @@ -698,6 +763,7 @@ dependencies = [ "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "indicatif 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazycell 0.6.0 (git+https://github.com/dherman/lazycell?branch=borrow_mut_with)", + "mockito 0.13.0 (git+https://github.com/lipanski/mockito?rev=48c5a93bcf8cc434875ed8aed22bff9623cb1ff4)", "node-archive 0.1.0", "notion-fail 0.1.0", "notion-fail-derive 0.1.0", @@ -705,9 +771,9 @@ dependencies = [ "readext 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -721,7 +787,7 @@ dependencies = [ "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "notion-fail-derive 0.1.0", - "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -735,9 +801,70 @@ dependencies = [ ] [[package]] -name = "num-traits" +name = "num" version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-bigint 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "num-complex 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-bigint" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-complex" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-iter" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-rational" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-bigint 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "num_cpus" @@ -943,6 +1070,23 @@ dependencies = [ "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "readext" version = "0.1.0" @@ -965,6 +1109,18 @@ dependencies = [ "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "regex" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "regex-syntax" version = "0.5.6" @@ -973,6 +1129,14 @@ dependencies = [ "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "regex-syntax" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "relay" version = "0.1.1" @@ -1010,8 +1174,8 @@ dependencies = [ "libflate 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", "serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1033,8 +1197,8 @@ dependencies = [ "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 2.0.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", "native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", "serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-io 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1053,6 +1217,16 @@ name = "rustc-demangle" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ryu" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "safemem" version = "0.2.0" @@ -1107,7 +1281,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde" -version = "1.0.27" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1131,14 +1305,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.9" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1148,7 +1321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1306,6 +1479,19 @@ dependencies = [ "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "test-support" +version = "0.1.0" +dependencies = [ + "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "hamcrest2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "node-archive 0.1.0", + "notion-fail 0.1.0", + "notion-fail-derive 0.1.0", + "serde_json 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "thread_local" version = "0.3.5" @@ -1391,7 +1577,7 @@ name = "toml" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1591,7 +1777,9 @@ dependencies = [ "checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" "checksum chomp 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9f74ad218e66339b11fd23f693fb8f1d621e80ba6ac218297be26073365d163d" "checksum clicolors-control 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1f84dec9bc083ce2503908cd305af98bd363da6f54bf8d4bf0ac14ee749ad5d1" +"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" "checksum cmdline_words_parser 0.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "346b10598869bcdcd53cd166ecfb04ff8a4d5d7bb33e4ef4acbdb2a447ac3c73" +"checksum colored 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc0a60679001b62fb628c4da80e574b9645ab4646056d7c9018885efffe45533" "checksum console 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7649ca90478264b9686aac8d269fcb014f14c2bed7c79a7e51b9f6afd4d783eb" "checksum conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" "checksum core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67" @@ -1602,6 +1790,7 @@ dependencies = [ "checksum debug-builders 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0f5d8e3d14cabcb2a8a59d7147289173c6ada77a0bc526f6b85078f941c0cf12" "checksum debugtrace 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "62e432bd83c5d70317f6ebd8a50ed4afb32907c64d6e2e1e65e339b06dc553f3" "checksum detect-indent 0.1.0 (git+https://github.com/stefanpenner/detect-indent-rs)" = "" +"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" "checksum docopt 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d8acd393692c503b168471874953a2531df0e9ab77d0b6bbc582395743300a4a" "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum either 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a39bffec1e2015c5d8a6773cb0cf48d0d758c842398f624c34969071f5499ea7" @@ -1620,13 +1809,17 @@ dependencies = [ "checksum guid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e691c64d9b226c7597e29aeb46be753beb8c9eeef96d8c78dfd4d306338a38da" "checksum guid-macro-impl 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08d50f7c496073b5a5dec0f6f1c149113a50960ce25dd2a559987a5a71190816" "checksum guid-parser 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "abc7adb441828023999e6cff9eb1ea63156f7ec37ab5bf690005e8fc6c1148ad" +"checksum hamcrest2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fb6b7feed7b56f5219df02c2df6e94bd232915f84351cb7204f5405da0107dff" +"checksum http-muncher 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ad5ef098f884f8d927426a231d14a923be1798ff0d5ad238989f4912565fb283" "checksum httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2f407128745b78abc95c0ffbe4e5d37427fdc0d45470710cfef8c44522a2e37" "checksum hyper 0.11.16 (registry+https://github.com/rust-lang/crates.io-index)" = "6a82c41828dd6f271f4d6ebc3f1db78239a4b2b3d355dfdb5f8bbf55f004463a" "checksum hyper-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c81fa95203e2a6087242c38691a0210f23e9f3f8f944350bd676522132e2985" "checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" +"checksum indexmap 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08173ba1e906efb6538785a8844dd496f5d34f0a2d88038e95195172fc667220" "checksum indicatif 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a29b2fa6f00010c268bface64c18bb0310aaa70d46a195d5382d288c477fb016" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" "checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" +"checksum itoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5adb58558dcd1d786b5f0bd15f3226ee23486e24b7b58304b60f64dc68e62606" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" @@ -1635,7 +1828,6 @@ dependencies = [ "checksum lazycell 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a6f08839bc70ef4a3fe1d566d5350f519c5912ea86be0df1740a7d247c7fc0ef" "checksum libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "1e5d97d6708edaa407429faa671b942dc0f2727222fb6b6539bf1db936e4b121" "checksum libflate 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "1a429b86418868c7ea91ee50e9170683f47fd9d94f5375438ec86ec3adb74e8e" -"checksum linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "70fb39025bc7cdd76305867c4eccf2f2dcf6e9a57f5b21a93e1c2d86cd03ec9e" "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2" "checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376" @@ -1647,10 +1839,17 @@ dependencies = [ "checksum miniz_oxide_c_api 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "92d98fdbd6145645828069b37ea92ca3de225e000d80702da25c20d3584b38a5" "checksum mio 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "75f72a93f046f1517e3cfddc0a096eb756a2ba727d36edc8227dee769a50a9b0" "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +"checksum mockito 0.13.0 (git+https://github.com/lipanski/mockito?rev=48c5a93bcf8cc434875ed8aed22bff9623cb1ff4)" = "" "checksum msdos_time 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "65ba9d75bcea84e07812618fedf284a64776c2f2ea0cad6bca7f69739695a958" "checksum native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f74dbadc8b43df7864539cedb7bc91345e532fdd913cfdc23ad94f4d2d40fbc0" "checksum net2 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)" = "3a80f842784ef6c9a958b68b7516bc7e35883c614004dd94959a4dca1b716c09" -"checksum num-traits 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "9936036cc70fe4a8b2d338ab665900323290efb03983c86cbe235ae800ad8017" +"checksum num 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" +"checksum num-bigint 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" +"checksum num-complex 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" +"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" +"checksum num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "af3fdbbc3291a5464dc57b03860ec37ca6bf915ed6ee385e7c6c052c422b2124" +"checksum num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" +"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" "checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" "checksum ole32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2c49021782e5233cd243168edfa8037574afed4eba4bbaf538b3d8d1789d8c" "checksum openssl 0.9.23 (registry+https://github.com/rust-lang/crates.io-index)" = "169a4b9160baf9b9b1ab975418c673686638995ba921683a7f1e01470dcb8854" @@ -1675,10 +1874,14 @@ dependencies = [ "checksum quote 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ed7d650913520df631972f21e104a4fa2f9c82a14afc65d17b388a2e29731e7c" "checksum rand 0.3.20 (registry+https://github.com/rust-lang/crates.io-index)" = "512870020642bb8c221bf68baa1b2573da814f6ccfe5c9699b1c303047abe9b1" "checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" +"checksum rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e464cd887e869cddcae8792a4ee31d23c7edd516700695608f5b98c67ee0131c" +"checksum rand_core 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "edecf0f94da5551fc9b492093e30b041a891657db7940ee221f9d2f66e82eef2" "checksum readext 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "abdc58f5f18bcf347b55cebb34ed4618b0feff9a9223160f5902adbc1f6a72a6" "checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd" "checksum regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" +"checksum regex 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5bbbea44c5490a1e84357ff28b7d518b4619a159fed5d25f6c1de2d19cc42814" "checksum regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +"checksum regex-syntax 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "747ba3b235651f6e2f67dfa8bcdcd073ddb7c243cb21c442fc12395dfcac212d" "checksum relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a" "checksum remove_dir_all 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b5d2f806b0fcdabd98acd380dc8daef485e22bcb7cddc811d1337967f2528cf5" "checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" @@ -1686,6 +1889,8 @@ dependencies = [ "checksum reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)" = "241faa9a8ca28a03cbbb9815a5d085f271d4c0168a19181f106aa93240c22ddb" "checksum result 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "194d8e591e405d1eecf28819740abed6d719d1a2db87fc0bcdedee9a26d55560" "checksum rustc-demangle 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "aee45432acc62f7b9a108cc054142dac51f979e69e71ddce7d6fc7adf29e817e" +"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" +"checksum ryu 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7153dd96dade874ab973e098cb62fcdbb89a03682e46b144fd09550998d4a4a7" "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" "checksum schannel 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "acece75e0f987c48863a6c792ec8b7d6c4177d4a027f8ccc72f849794f437016" "checksum scoped-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f417c22df063e9450888a7561788e9bd46d3bb3c1466435b4eccb903807f147d" @@ -1693,10 +1898,10 @@ dependencies = [ "checksum security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "5421621e836278a0b139268f36eee0dc7e389b784dc3f79d8f11aabadf41bead" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -"checksum serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "db99f3919e20faa51bb2996057f5031d8685019b5a06139b1ce761da671b8526" +"checksum serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)" = "22d340507cea0b7e6632900a176101fea959c7065d93ba555072da90aaaafc87" "checksum serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "f4ba7591cfe93755e89eeecdbcc668885624829b020050e6aec99c2a03bd3fd0" "checksum serde_derive_internals 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6e03f1c9530c3fb0a0a5c9b826bdd9246a5921ae995d75f512ac917fc4dd55b5" -"checksum serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c9db7266c7d63a4c4b7fe8719656ccdd51acf1bed6124b174f933b009fb10bcb" +"checksum serde_json 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)" = "44dd2cfde475037451fa99b7e5df77aa3cfd1536575fa8e7a538ab36dcde49ae" "checksum serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce0fd303af908732989354c6f02e05e2e6d597152870f2c6990efb0577137480" "checksum shell32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9ee04b46101f57121c9da2b151988283b6beb79b34f5bb29a58ee48cb695122c" "checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537" diff --git a/Cargo.toml b/Cargo.toml index 6751c0e7f..2e08d091b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ repository = "https://github.com/notion-cli/notion" [features] universal-docs = ["notion-core/universal-docs"] +mock-network = ["mockito", "notion-core/mock-network"] [[bin]] name = "notion" @@ -33,12 +34,21 @@ docopt = "0.8" notion-core = { path = "crates/notion-core" } serde = "1.0" serde_derive = "1.0" +serde_json = "1.0.26" console = "0.6.1" failure_derive = "0.1.1" failure = "0.1.1" notion-fail = { path = "crates/notion-fail" } notion-fail-derive = { path = "crates/notion-fail-derive" } +reqwest = "0.8.5" semver = "0.9.0" result = "1.0.0" +rand = "0.5" +cfg-if = "0.1" +mockito = { "git" = "https://github.com/lipanski/mockito", "rev" = "48c5a93bcf8cc434875ed8aed22bff9623cb1ff4", optional = true } +test-support = { path = "crates/test-support" } + +[dev-dependencies] +hamcrest2 = "0.2.3" [workspace] diff --git a/appveyor.yml b/appveyor.yml index 5be48c00e..a5a84fe54 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -114,8 +114,9 @@ install: # the "directory does not contain a project or solution file" error. build: false -# Uses 'cargo test' to run tests and build. Alternatively, the project may call compiled programs -#directly or perform other testing commands. Rust will automatically be placed in the PATH -# environment variable. +# first, this runs the unit tests for all crates (without mocking the network) +# - these are run single-threaded because some tests change environment variables, which is not thread-safe +# next, this runs the acceptance tests, using network mocks test_script: - - cargo test --all --verbose %cargoflags% + - cargo test --package notion-core --package notion-fail --package node-archive --package notion-fail-derive --package progress-read -- --test-threads=1 && + cargo test --features mock-network diff --git a/crates/notion-core/Cargo.toml b/crates/notion-core/Cargo.toml index ad5f97e15..acd697673 100644 --- a/crates/notion-core/Cargo.toml +++ b/crates/notion-core/Cargo.toml @@ -5,6 +5,7 @@ authors = ["David Herman "] [features] universal-docs = ["node-archive/universal-docs"] +mock-network = ["mockito"] [dependencies] toml = "0.4" @@ -30,3 +31,4 @@ tempfile = "3.0.2" os_info = { git = "https://github.com/DarkEld3r/os_info", rev = "912a7941cd797e9fca83f4fe858ed684baa5fc40" } detect-indent = { git = "https://github.com/stefanpenner/detect-indent-rs", branch = "master" } envoy = "0.1.3" +mockito = { git = "https://github.com/lipanski/mockito", rev = "48c5a93bcf8cc434875ed8aed22bff9623cb1ff4", optional = true } \ No newline at end of file diff --git a/crates/notion-core/src/catalog/mod.rs b/crates/notion-core/src/catalog/mod.rs index 309a4413b..997593cb8 100644 --- a/crates/notion-core/src/catalog/mod.rs +++ b/crates/notion-core/src/catalog/mod.rs @@ -30,14 +30,36 @@ use version::VersionSpec; pub(crate) mod serial; +#[cfg(feature = "mock-network")] +use mockito; + // ISSUE (#86): Move public repository URLs to config file -/// URL of the index of available Node versions on the public Node server. -const PUBLIC_NODE_VERSION_INDEX: &'static str = "https://nodejs.org/dist/index.json"; -/// URL of the index of available Yarn versions on the public git repository. -const PUBLIC_YARN_VERSION_INDEX: &'static str = - "https://github.com/notion-cli/yarn-releases/raw/master/index.json"; -/// URL of the latest Yarn version on the public yarnpkg.com -const PUBLIC_YARN_LATEST_VERSION: &'static str = "https://yarnpkg.com/latest-version"; +cfg_if! { + if #[cfg(feature = "mock-network")] { + fn public_node_version_index() -> String { + format!("{}/node-dist/index.json", mockito::SERVER_URL) + } + fn public_yarn_version_index() -> String { + format!("{}/yarn-releases/index.json", mockito::SERVER_URL) + } + fn public_yarn_latest_version() -> String { + format!("{}/yarn-latest", mockito::SERVER_URL) + } + } else { + /// Returns the URL of the index of available Node versions on the public Node server. + fn public_node_version_index() -> String { + "https://nodejs.org/dist/index.json".to_string() + } + /// Return the URL of the index of available Yarn versions on the public git repository. + fn public_yarn_version_index() -> String { + "https://github.com/notion-cli/yarn-releases/raw/master/index.json".to_string() + } + /// URL of the latest Yarn version on the public yarnpkg.com + fn public_yarn_latest_version() -> String { + "https://yarnpkg.com/latest-version".to_string() + } + } +} /// Lazily loaded tool catalog. pub struct LazyCatalog { @@ -308,16 +330,17 @@ impl Resolve for YarnCollection { fn resolve_public(&self, matching: &VersionSpec) -> Fallible { let version = match *matching { VersionSpec::Latest => { - let mut response: reqwest::Response = reqwest::get(PUBLIC_YARN_LATEST_VERSION) - .with_context(RegistryFetchError::from_error)?; + let mut response: reqwest::Response = + reqwest::get(public_yarn_latest_version().as_str()) + .with_context(RegistryFetchError::from_error)?; response.text().unknown()? } VersionSpec::Semver(ref matching) => { let spinner = progress_spinner(&format!( "Fetching public registry: {}", - PUBLIC_YARN_VERSION_INDEX + public_yarn_version_index() )); - let releases: Vec = reqwest::get(PUBLIC_YARN_VERSION_INDEX) + let releases: Vec = reqwest::get(public_yarn_version_index().as_str()) .with_context(RegistryFetchError::from_error)? .json() .unknown()?; @@ -399,10 +422,11 @@ fn resolve_node_versions() -> Result { None => { let spinner = progress_spinner(&format!( "Fetching public registry: {}", - PUBLIC_NODE_VERSION_INDEX + public_node_version_index() )); - let mut response: reqwest::Response = reqwest::get(PUBLIC_NODE_VERSION_INDEX) - .with_context(RegistryFetchError::from_error)?; + let mut response: reqwest::Response = reqwest::get( + public_node_version_index().as_str(), + ).with_context(RegistryFetchError::from_error)?; let response_text: String = response.text().unknown()?; let cached: NamedTempFile = NamedTempFile::new().unknown()?; diff --git a/crates/notion-core/src/distro/node.rs b/crates/notion-core/src/distro/node.rs index c665851a6..abdbd1829 100644 --- a/crates/notion-core/src/distro/node.rs +++ b/crates/notion-core/src/distro/node.rs @@ -15,7 +15,20 @@ use style::{progress_bar, Action}; use notion_fail::{Fallible, ResultExt}; use semver::Version; -const PUBLIC_NODE_SERVER_ROOT: &'static str = "https://nodejs.org/dist/"; +#[cfg(feature = "mock-network")] +use mockito; + +cfg_if! { + if #[cfg(feature = "mock-network")] { + fn public_node_server_root() -> String { + mockito::SERVER_URL.to_string() + } + } else { + fn public_node_server_root() -> String { + "https://nodejs.org/dist".to_string() + } + } +} /// A provisioned Node distribution. pub struct NodeDistro { @@ -42,7 +55,12 @@ impl Distro for NodeDistro { /// Provision a Node distribution from the public Node distributor (`https://nodejs.org`). fn public(version: Version) -> Fallible { let archive_file = path::node_archive_file(&version.to_string()); - let url = format!("{}v{}/{}", PUBLIC_NODE_SERVER_ROOT, version, &archive_file); + let url = format!( + "{}/v{}/{}", + public_node_server_root(), + version, + &archive_file + ); NodeDistro::remote(version, &url) } diff --git a/crates/notion-core/src/distro/yarn.rs b/crates/notion-core/src/distro/yarn.rs index a76f5e609..fa1f89cab 100644 --- a/crates/notion-core/src/distro/yarn.rs +++ b/crates/notion-core/src/distro/yarn.rs @@ -15,8 +15,20 @@ use style::{progress_bar, Action}; use notion_fail::{Fallible, ResultExt}; use semver::Version; -const PUBLIC_YARN_SERVER_ROOT: &'static str = - "https://github.com/notion-cli/yarn-releases/raw/master/dist/"; +#[cfg(feature = "mock-network")] +use mockito; + +cfg_if! { + if #[cfg(feature = "mock-network")] { + fn public_yarn_server_root() -> String { + mockito::SERVER_URL.to_string() + } + } else { + fn public_yarn_server_root() -> String { + "https://github.com/notion-cli/yarn-releases/raw/master/dist".to_string() + } + } +} /// A provisioned Yarn distribution. pub struct YarnDistro { @@ -43,7 +55,7 @@ impl Distro for YarnDistro { /// Provision a distribution from the public Yarn distributor (`https://yarnpkg.com`). fn public(version: Version) -> Fallible { let archive_file = path::yarn_archive_file(&version.to_string()); - let url = format!("{}{}", PUBLIC_YARN_SERVER_ROOT, archive_file); + let url = format!("{}/{}", public_yarn_server_root(), archive_file); YarnDistro::remote(version, &url) } diff --git a/crates/notion-core/src/lib.rs b/crates/notion-core/src/lib.rs index 29d669958..e34dffb01 100644 --- a/crates/notion-core/src/lib.rs +++ b/crates/notion-core/src/lib.rs @@ -8,6 +8,8 @@ extern crate detect_indent; extern crate envoy; extern crate indicatif; extern crate lazycell; +#[cfg(feature = "mock-network")] +extern crate mockito; extern crate node_archive; extern crate readext; extern crate reqwest; diff --git a/crates/notion-core/src/path/windows.rs b/crates/notion-core/src/path/windows.rs index 59e4ade75..8944f9a59 100644 --- a/crates/notion-core/src/path/windows.rs +++ b/crates/notion-core/src/path/windows.rs @@ -1,6 +1,7 @@ //! Provides functions for determining the paths of files and directories //! in a standard Notion layout in Windows operating systems. +use std::env; use std::path::PathBuf; #[cfg(windows)] use std::os::windows; @@ -46,12 +47,19 @@ cfg_if! { // launchscript.exe launchscript_file fn program_data_root() -> Fallible { - #[cfg(windows)] - return Ok(winfolder::Folder::ProgramData.path().join("Notion")); + // if this is sandboxed in CI, use the sandboxed ProgramData directory + if env::var("NOTION_SANDBOX").is_ok() { + let notion_data = env::var("NOTION_DATA_ROOT").unwrap(); + return Ok(PathBuf::from(notion_data).join("Notion")); + } else { + #[cfg(windows)] + return Ok(winfolder::Folder::ProgramData.path().join("Notion")); + + // "universal-docs" is built on a Unix machine, so we can't include Windows-specific libs + #[cfg(feature = "universal-docs")] + unimplemented!() + } - // "universal-docs" is built on a Unix machine, so we can't include Windows-specific libs - #[cfg(feature = "universal-docs")] - unimplemented!() } pub fn cache_dir() -> Fallible { @@ -107,7 +115,7 @@ pub fn yarn_version_bin_dir(version: &str) -> Fallible { } // 3rd-party binaries installed globally for this node version -pub fn node_version_3p_bin_dir(version: &str) -> Fallible { +pub fn node_version_3p_bin_dir(_version: &str) -> Fallible { // ISSUE (#90) Figure out where binaries are globally installed on Windows unimplemented!("global 3rd party executables not yet implemented for Windows") } @@ -164,12 +172,18 @@ pub fn shim_file(toolname: &str) -> Fallible { // catalog.toml user_catalog_file fn local_data_root() -> Fallible { - #[cfg(windows)] - return Ok(winfolder::Folder::LocalAppData.path().join("Notion")); + // if this is sandboxed in CI, use the sandboxed AppData directory + if env::var("NOTION_SANDBOX").is_ok() { + let home_dir = env::home_dir().unwrap(); + return Ok(home_dir.join("AppData").join("Local").join("Notion")); + } else { + #[cfg(windows)] + return Ok(winfolder::Folder::LocalAppData.path().join("Notion")); - // "universal-docs" is built on a Unix machine, so we can't include Windows-specific libs - #[cfg(feature = "universal-docs")] - unimplemented!() + // "universal-docs" is built on a Unix machine, so we can't include Windows-specific libs + #[cfg(feature = "universal-docs")] + unimplemented!() + } } pub fn user_config_file() -> Fallible { diff --git a/crates/notion-core/src/shell/mod.rs b/crates/notion-core/src/shell/mod.rs index 6fc3101c6..e6f234bb0 100644 --- a/crates/notion-core/src/shell/mod.rs +++ b/crates/notion-core/src/shell/mod.rs @@ -5,6 +5,7 @@ use std::str::FromStr; use semver::Version; +use fs::ensure_containing_dir_exists; use notion_fail::{ExitCode, Fallible, NotionError, NotionFail, ResultExt}; use env; @@ -30,6 +31,7 @@ pub trait Shell { fn compile_postscript(&self, postscript: &Postscript) -> String; fn save_postscript(&self, postscript: &Postscript) -> Fallible<()> { + ensure_containing_dir_exists(&self.postscript_path())?; let mut file = File::create(self.postscript_path()).unknown()?; file.write_all(self.compile_postscript(postscript).as_bytes()) .unknown()?; diff --git a/crates/notion-core/src/tool.rs b/crates/notion-core/src/tool.rs index fea09cbad..ddcb2afaa 100644 --- a/crates/notion-core/src/tool.rs +++ b/crates/notion-core/src/tool.rs @@ -125,7 +125,7 @@ pub struct Yarn(Command); #[cfg(windows)] impl Tool for Script { - fn new(session: &mut Session) -> Fallible { + fn new(_session: &mut Session) -> Fallible { throw!(ToolUnimplementedError::new()) } diff --git a/crates/test-support/Cargo.toml b/crates/test-support/Cargo.toml new file mode 100644 index 000000000..5003838f0 --- /dev/null +++ b/crates/test-support/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "test-support" +version = "0.1.0" +authors = ["David Herman "] + +[features] +universal-docs = ["node-archive/universal-docs"] + +[dependencies] +failure = "0.1.1" +failure_derive = "0.1.1" +hamcrest2 = "0.2.3" +node-archive = { path = "../node-archive" } +notion-fail = { path = "../notion-fail" } +notion-fail-derive = { path = "../notion-fail-derive" } +serde_json = { version = "1.0.3" } diff --git a/crates/test-support/src/lib.rs b/crates/test-support/src/lib.rs new file mode 100644 index 000000000..4566441c2 --- /dev/null +++ b/crates/test-support/src/lib.rs @@ -0,0 +1,17 @@ +//! Utilities to use with acceptance tests in Notion. + +#![cfg_attr(feature = "universal-docs", feature(doc_cfg))] + +extern crate hamcrest2; +extern crate serde_json; + +pub mod matchers; +pub mod process; + +extern crate failure; +#[macro_use] +extern crate failure_derive; +#[macro_use] +extern crate notion_fail; +#[macro_use] +extern crate notion_fail_derive; diff --git a/crates/test-support/src/matchers.rs b/crates/test-support/src/matchers.rs new file mode 100644 index 000000000..e33495c58 --- /dev/null +++ b/crates/test-support/src/matchers.rs @@ -0,0 +1,728 @@ +use std::fmt; +use std::process::Output; +use std::str; +use std::usize; + +use process::{ProcessBuilder, ProcessError}; + +use hamcrest2::core::{Matcher, MatchResult}; +use serde_json::{self, Value}; + +#[derive(Clone)] +pub struct Execs { + expect_stdout: Option, + expect_stdin: Option, + expect_stderr: Option, + expect_exit_code: Option, + expect_stdout_contains: Vec, + expect_stderr_contains: Vec, + expect_either_contains: Vec, + expect_stdout_contains_n: Vec<(String, usize)>, + expect_stdout_not_contains: Vec, + expect_stderr_not_contains: Vec, + expect_stderr_unordered: Vec, + expect_neither_contains: Vec, + expect_json: Option>, +} + +impl Execs { + /// Verify that stdout is equal to the given lines. + /// See `lines_match` for supported patterns. + pub fn with_stdout(mut self, expected: S) -> Execs { + self.expect_stdout = Some(expected.to_string()); + self + } + + /// Verify that stderr is equal to the given lines. + /// See `lines_match` for supported patterns. + pub fn with_stderr(mut self, expected: S) -> Execs { + self._with_stderr(&expected); + self + } + + fn _with_stderr(&mut self, expected: &ToString) { + self.expect_stderr = Some(expected.to_string()); + } + + /// Verify the exit code from the process. + pub fn with_status(mut self, expected: i32) -> Execs { + self.expect_exit_code = Some(expected); + self + } + + /// Verify that stdout contains the given contiguous lines somewhere in + /// its output. + /// See `lines_match` for supported patterns. + pub fn with_stdout_contains(mut self, expected: S) -> Execs { + self.expect_stdout_contains.push(expected.to_string()); + self + } + + /// Verify that stderr contains the given contiguous lines somewhere in + /// its output. + /// See `lines_match` for supported patterns. + pub fn with_stderr_contains(mut self, expected: S) -> Execs { + self.expect_stderr_contains.push(expected.to_string()); + self + } + + /// Verify that either stdout or stderr contains the given contiguous + /// lines somewhere in its output. + /// See `lines_match` for supported patterns. + pub fn with_either_contains(mut self, expected: S) -> Execs { + self.expect_either_contains.push(expected.to_string()); + self + } + + /// Verify that stdout contains the given contiguous lines somewhere in + /// its output, and should be repeated `number` times. + /// See `lines_match` for supported patterns. + pub fn with_stdout_contains_n(mut self, expected: S, number: usize) -> Execs { + self.expect_stdout_contains_n + .push((expected.to_string(), number)); + self + } + + /// Verify that stdout does not contain the given contiguous lines. + /// See `lines_match` for supported patterns. + /// See note on `with_stderr_does_not_contain`. + pub fn with_stdout_does_not_contain(mut self, expected: S) -> Execs { + self.expect_stdout_not_contains.push(expected.to_string()); + self + } + + /// Verify that stderr does not contain the given contiguous lines. + /// See `lines_match` for supported patterns. + /// + /// Care should be taken when using this method because there is a + /// limitless number of possible things that *won't* appear. A typo means + /// your test will pass without verifying the correct behavior. If + /// possible, write the test first so that it fails, and then implement + /// your fix/feature to make it pass. + pub fn with_stderr_does_not_contain(mut self, expected: S) -> Execs { + self.expect_stderr_not_contains.push(expected.to_string()); + self + } + + /// Verify that all of the stderr output is equal to the given lines, + /// ignoring the order of the lines. + /// See `lines_match` for supported patterns. + /// This is useful when checking the output of `cargo build -v` since + /// the order of the output is not always deterministic. + /// Recommend use `with_stderr_contains` instead unless you really want to + /// check *every* line of output. + /// + /// Be careful when using patterns such as `[..]`, because you may end up + /// with multiple lines that might match, and this is not smart enough to + /// do anything like longest-match. For example, avoid something like: + /// [RUNNING] `rustc [..] + /// [RUNNING] `rustc --crate-name foo [..] + /// This will randomly fail if the other crate name is `bar`, and the + /// order changes. + pub fn with_stderr_unordered(mut self, expected: S) -> Execs { + self.expect_stderr_unordered.push(expected.to_string()); + self + } + + /// Verify the JSON output matches the given JSON. + /// Typically used when testing cargo commands that emit JSON. + /// Each separate JSON object should be separated by a blank line. + /// Example: + /// assert_that( + /// p.cargo("metadata"), + /// execs().with_json(r#" + /// {"example": "abc"} + /// {"example": "def"} + /// "#) + /// ); + /// Objects should match in the order given. + /// The order of arrays is ignored. + /// Strings support patterns described in `lines_match`. + /// Use `{...}` to match any object. + pub fn with_json(mut self, expected: &str) -> Execs { + self.expect_json = Some( + expected + .split("\n\n") + .map(|obj| obj.parse().unwrap()) + .collect(), + ); + self + } + + fn match_output(&self, actual: &Output) -> MatchResult { + self.match_status(actual) + .and(self.match_stdout(actual)) + .and(self.match_stderr(actual)) + } + + fn match_status(&self, actual: &Output) -> MatchResult { + match self.expect_exit_code { + None => Ok(()), + Some(code) if actual.status.code() == Some(code) => Ok(()), + Some(_) => Err(format!( + "exited with {}\n--- stdout\n{}\n--- stderr\n{}", + actual.status, + String::from_utf8_lossy(&actual.stdout), + String::from_utf8_lossy(&actual.stderr) + )), + } + } + + fn match_stdout(&self, actual: &Output) -> MatchResult { + self.match_std( + self.expect_stdout.as_ref(), + &actual.stdout, + "stdout", + &actual.stderr, + MatchKind::Exact, + )?; + for expect in self.expect_stdout_contains.iter() { + self.match_std( + Some(expect), + &actual.stdout, + "stdout", + &actual.stderr, + MatchKind::Partial, + )?; + } + for expect in self.expect_stderr_contains.iter() { + self.match_std( + Some(expect), + &actual.stderr, + "stderr", + &actual.stdout, + MatchKind::Partial, + )?; + } + for &(ref expect, number) in self.expect_stdout_contains_n.iter() { + self.match_std( + Some(&expect), + &actual.stdout, + "stdout", + &actual.stderr, + MatchKind::PartialN(number), + )?; + } + for expect in self.expect_stdout_not_contains.iter() { + self.match_std( + Some(expect), + &actual.stdout, + "stdout", + &actual.stderr, + MatchKind::NotPresent, + )?; + } + for expect in self.expect_stderr_not_contains.iter() { + self.match_std( + Some(expect), + &actual.stderr, + "stderr", + &actual.stdout, + MatchKind::NotPresent, + )?; + } + for expect in self.expect_stderr_unordered.iter() { + self.match_std( + Some(expect), + &actual.stderr, + "stderr", + &actual.stdout, + MatchKind::Unordered, + )?; + } + for expect in self.expect_neither_contains.iter() { + self.match_std( + Some(expect), + &actual.stdout, + "stdout", + &actual.stdout, + MatchKind::NotPresent, + )?; + + self.match_std( + Some(expect), + &actual.stderr, + "stderr", + &actual.stderr, + MatchKind::NotPresent, + )?; + } + + for expect in self.expect_either_contains.iter() { + let match_std = self.match_std( + Some(expect), + &actual.stdout, + "stdout", + &actual.stdout, + MatchKind::Partial, + ); + let match_err = self.match_std( + Some(expect), + &actual.stderr, + "stderr", + &actual.stderr, + MatchKind::Partial, + ); + + if let (Err(_), Err(_)) = (match_std, match_err) { + Err(format!( + "expected to find:\n\ + {}\n\n\ + did not find in either output.", + expect + ))?; + } + } + + if let Some(ref objects) = self.expect_json { + let stdout = str::from_utf8(&actual.stdout) + .map_err(|_| "stdout was not utf8 encoded".to_owned())?; + let lines = stdout + .lines() + .filter(|line| line.starts_with('{')) + .collect::>(); + if lines.len() != objects.len() { + return Err(format!( + "expected {} json lines, got {}, stdout:\n{}", + objects.len(), + lines.len(), + stdout + )); + } + for (obj, line) in objects.iter().zip(lines) { + self.match_json(obj, line)?; + } + } + Ok(()) + } + + fn match_stderr(&self, actual: &Output) -> MatchResult { + self.match_std( + self.expect_stderr.as_ref(), + &actual.stderr, + "stderr", + &actual.stdout, + MatchKind::Exact, + ) + } + + fn match_std( + &self, + expected: Option<&String>, + actual: &[u8], + description: &str, + extra: &[u8], + kind: MatchKind, + ) -> MatchResult { + let out = match expected { + Some(out) => out, + None => return Ok(()), + }; + let actual = match str::from_utf8(actual) { + Err(..) => return Err(format!("{} was not utf8 encoded", description)), + Ok(actual) => actual, + }; + // Let's not deal with \r\n vs \n on windows... + let actual = actual.replace("\r", ""); + let actual = actual.replace("\t", ""); + + match kind { + MatchKind::Exact => { + let a = actual.lines(); + let e = out.lines(); + + let diffs = self.diff_lines(a, e, false); + if diffs.is_empty() { + Ok(()) + } else { + Err(format!( + "differences:\n\ + {}\n\n\ + other output:\n\ + `{}`", + diffs.join("\n"), + String::from_utf8_lossy(extra) + )) + } + } + MatchKind::Partial => { + let mut a = actual.lines(); + let e = out.lines(); + + let mut diffs = self.diff_lines(a.clone(), e.clone(), true); + while let Some(..) = a.next() { + let a = self.diff_lines(a.clone(), e.clone(), true); + if a.len() < diffs.len() { + diffs = a; + } + } + if diffs.is_empty() { + Ok(()) + } else { + Err(format!( + "expected to find:\n\ + {}\n\n\ + did not find in output:\n\ + {}", + out, actual + )) + } + } + MatchKind::PartialN(number) => { + let mut a = actual.lines(); + let e = out.lines(); + + let mut matches = 0; + + while let Some(..) = { + if self.diff_lines(a.clone(), e.clone(), true).is_empty() { + matches += 1; + } + a.next() + } {} + + if matches == number { + Ok(()) + } else { + Err(format!( + "expected to find {} occurrences:\n\ + {}\n\n\ + did not find in output:\n\ + {}", + number, out, actual + )) + } + } + MatchKind::NotPresent => { + let mut a = actual.lines(); + let e = out.lines(); + + let mut diffs = self.diff_lines(a.clone(), e.clone(), true); + while let Some(..) = a.next() { + let a = self.diff_lines(a.clone(), e.clone(), true); + if a.len() < diffs.len() { + diffs = a; + } + } + if diffs.is_empty() { + Err(format!( + "expected not to find:\n\ + {}\n\n\ + but found in output:\n\ + {}", + out, actual + )) + } else { + Ok(()) + } + } + MatchKind::Unordered => { + let mut a = actual.lines().collect::>(); + let e = out.lines(); + + for e_line in e { + match a.iter().position(|a_line| lines_match(e_line, a_line)) { + Some(index) => a.remove(index), + None => { + return Err(format!( + "Did not find expected line:\n\ + {}\n\ + Remaining available output:\n\ + {}\n", + e_line, + a.join("\n") + )) + } + }; + } + if !a.is_empty() { + Err(format!( + "Output included extra lines:\n\ + {}\n", + a.join("\n") + )) + } else { + Ok(()) + } + } + } + } + + fn match_json(&self, expected: &Value, line: &str) -> MatchResult { + let actual = match line.parse() { + Err(e) => return Err(format!("invalid json, {}:\n`{}`", e, line)), + Ok(actual) => actual, + }; + + match find_mismatch(expected, &actual) { + Some((expected_part, actual_part)) => Err(format!( + "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n", + serde_json::to_string_pretty(expected).unwrap(), + serde_json::to_string_pretty(&actual).unwrap(), + serde_json::to_string_pretty(expected_part).unwrap(), + serde_json::to_string_pretty(actual_part).unwrap(), + )), + None => Ok(()), + } + } + + fn diff_lines<'a>( + &self, + actual: str::Lines<'a>, + expected: str::Lines<'a>, + partial: bool, + ) -> Vec { + let actual = actual.take(if partial { + expected.clone().count() + } else { + usize::MAX + }); + zip_all(actual, expected) + .enumerate() + .filter_map(|(i, (a, e))| match (a, e) { + (Some(a), Some(e)) => { + if lines_match(&e, &a) { + None + } else { + Some(format!("{:3} - |{}|\n + |{}|\n", i, e, a)) + } + } + (Some(a), None) => Some(format!("{:3} -\n + |{}|\n", i, a)), + (None, Some(e)) => Some(format!("{:3} - |{}|\n +\n", i, e)), + (None, None) => panic!("Cannot get here"), + }) + .collect() + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum MatchKind { + Exact, + Partial, + PartialN(usize), + NotPresent, + Unordered, +} + +/// Compare a line with an expected pattern. +/// - Use `[..]` as a wildcard to match 0 or more characters on the same line +/// (similar to `.*` in a regex). +/// - Use `[EXE]` to optionally add `.exe` on Windows (empty string on other +/// platforms). +/// - There is a wide range of macros (such as `[COMPILING]` or `[WARNING]`) +/// to match cargo's "status" output and allows you to ignore the alignment. +/// See `substitute_macros` for a complete list of macros. +pub fn lines_match(expected: &str, actual: &str) -> bool { + // Let's not deal with / vs \ (windows...) + let expected = expected.replace("\\", "/"); + let mut actual: &str = &actual.replace("\\", "/"); + let expected = substitute_macros(&expected); + for (i, part) in expected.split("[..]").enumerate() { + match actual.find(part) { + Some(j) => { + if i == 0 && j != 0 { + return false; + } + actual = &actual[j + part.len()..]; + } + None => return false, + } + } + actual.is_empty() || expected.ends_with("[..]") +} + +#[test] +fn lines_match_works() { + assert!(lines_match("a b", "a b")); + assert!(lines_match("a[..]b", "a b")); + assert!(lines_match("a[..]", "a b")); + assert!(lines_match("[..]", "a b")); + assert!(lines_match("[..]b", "a b")); + + assert!(!lines_match("[..]b", "c")); + assert!(!lines_match("b", "c")); + assert!(!lines_match("b", "cb")); +} + +// Compares JSON object for approximate equality. +// You can use `[..]` wildcard in strings (useful for OS dependent things such +// as paths). You can use a `"{...}"` string literal as a wildcard for +// arbitrary nested JSON (useful for parts of object emitted by other programs +// (e.g. rustc) rather than Cargo itself). Arrays are sorted before comparison. +fn find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a Value, &'a Value)> { + use serde_json::Value::*; + match (expected, actual) { + (&Number(ref l), &Number(ref r)) if l == r => None, + (&Bool(l), &Bool(r)) if l == r => None, + (&String(ref l), &String(ref r)) if lines_match(l, r) => None, + (&Array(ref l), &Array(ref r)) => { + if l.len() != r.len() { + return Some((expected, actual)); + } + + let mut l = l.iter().collect::>(); + let mut r = r.iter().collect::>(); + + l.retain( + |l| match r.iter().position(|r| find_mismatch(l, r).is_none()) { + Some(i) => { + r.remove(i); + false + } + None => true, + }, + ); + + if !l.is_empty() { + assert!(!r.is_empty()); + Some((&l[0], &r[0])) + } else { + assert_eq!(r.len(), 0); + None + } + } + (&Object(ref l), &Object(ref r)) => { + let same_keys = l.len() == r.len() && l.keys().all(|k| r.contains_key(k)); + if !same_keys { + return Some((expected, actual)); + } + + l.values() + .zip(r.values()) + .filter_map(|(l, r)| find_mismatch(l, r)) + .nth(0) + } + (&Null, &Null) => None, + // magic string literal "{...}" acts as wildcard for any sub-JSON + (&String(ref l), _) if l == "{...}" => None, + _ => Some((expected, actual)), + } +} + +struct ZipAll { + first: I1, + second: I2, +} + +impl, I2: Iterator> Iterator for ZipAll { + type Item = (Option, Option); + fn next(&mut self) -> Option<(Option, Option)> { + let first = self.first.next(); + let second = self.second.next(); + + match (first, second) { + (None, None) => None, + (a, b) => Some((a, b)), + } + } +} + +fn zip_all, I2: Iterator>(a: I1, b: I2) -> ZipAll { + ZipAll { + first: a, + second: b, + } +} + +impl fmt::Display for Execs { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "execs") + } +} + +impl fmt::Debug for Execs { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "execs") + } +} + +impl Matcher for Execs { + fn matches(&self, mut process: ProcessBuilder) -> MatchResult { + self.matches(&mut process) + } +} + +impl<'a> Matcher<&'a mut ProcessBuilder> for Execs { + fn matches(&self, process: &'a mut ProcessBuilder) -> MatchResult { + println!("running {}", process); + let res = process.exec_with_output(); + + match res { + Ok(out) => self.match_output(&out), + Err(e) => { + let err = e.downcast_ref::(); + if let Some(&ProcessError { + output: Some(ref out), + .. + }) = err + { + return self.match_output(out); + } + let mut s = format!("could not exec process {}: {}", process, e); + // for cause in e.iter_causes() { + // s.push_str(&format!("\ncaused by: {}", cause)); + // } + Err(s) + } + } + } +} + +impl Matcher for Execs { + fn matches(&self, output: Output) -> MatchResult { + self.match_output(&output) + } +} + +pub fn execs() -> Execs { + Execs { + expect_stdout: None, + expect_stderr: None, + expect_stdin: None, + expect_exit_code: Some(0), + expect_stdout_contains: Vec::new(), + expect_stderr_contains: Vec::new(), + expect_either_contains: Vec::new(), + expect_stdout_contains_n: Vec::new(), + expect_stdout_not_contains: Vec::new(), + expect_stderr_not_contains: Vec::new(), + expect_stderr_unordered: Vec::new(), + expect_neither_contains: Vec::new(), + expect_json: None, + } +} + +fn substitute_macros(input: &str) -> String { + let macros = [ + ("[RUNNING]", " Running"), + ("[COMPILING]", " Compiling"), + ("[CHECKING]", " Checking"), + ("[CREATED]", " Created"), + ("[FINISHED]", " Finished"), + ("[ERROR]", "error:"), + ("[WARNING]", "warning:"), + ("[DOCUMENTING]", " Documenting"), + ("[FRESH]", " Fresh"), + ("[UPDATING]", " Updating"), + ("[ADDING]", " Adding"), + ("[REMOVING]", " Removing"), + ("[DOCTEST]", " Doc-tests"), + ("[PACKAGING]", " Packaging"), + ("[DOWNLOADING]", " Downloading"), + ("[UPLOADING]", " Uploading"), + ("[VERIFYING]", " Verifying"), + ("[ARCHIVING]", " Archiving"), + ("[INSTALLING]", " Installing"), + ("[REPLACING]", " Replacing"), + ("[UNPACKING]", " Unpacking"), + ("[SUMMARY]", " Summary"), + ("[FIXING]", " Fixing"), + ("[EXE]", if cfg!(windows) { ".exe" } else { "" }), + ]; + let mut result = input.to_owned(); + for &(pat, subst) in ¯os { + result = result.replace(pat, subst) + } + result +} + diff --git a/crates/test-support/src/process.rs b/crates/test-support/src/process.rs new file mode 100644 index 000000000..2af821133 --- /dev/null +++ b/crates/test-support/src/process.rs @@ -0,0 +1,247 @@ +use std::collections::HashMap; +use std::env; +use std::ffi::{OsStr, OsString}; +use std::fmt; +use std::path::Path; +use std::process::{Command, ExitStatus, Output}; +use std::str; + +use notion_fail::{ExitCode, Fallible, NotionFail}; + +/// A builder object for an external process, similar to `std::process::Command`. +#[derive(Clone, Debug)] +pub struct ProcessBuilder { + /// The program to execute. + program: OsString, + /// A list of arguments to pass to the program. + args: Vec, + /// Any environment variables that should be set for the program. + env: HashMap>, + /// Which directory to run the program from. + cwd: Option, +} + +impl fmt::Display for ProcessBuilder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "`{}", self.program.to_string_lossy())?; + + for arg in &self.args { + write!(f, " {}", arg.to_string_lossy())?; + } + + write!(f, "`") + } +} + +impl ProcessBuilder { + /// (chainable) Set the executable for the process. + pub fn program>(&mut self, program: T) -> &mut ProcessBuilder { + self.program = program.as_ref().to_os_string(); + self + } + + /// (chainable) Add an arg to the args list. + pub fn arg>(&mut self, arg: T) -> &mut ProcessBuilder { + self.args.push(arg.as_ref().to_os_string()); + self + } + + /// (chainable) Add many args to the args list. + pub fn args>(&mut self, arguments: &[T]) -> &mut ProcessBuilder { + self.args + .extend(arguments.iter().map(|t| t.as_ref().to_os_string())); + self + } + + /// (chainable) Replace args with new args list + pub fn args_replace>(&mut self, arguments: &[T]) -> &mut ProcessBuilder { + self.args = arguments + .iter() + .map(|t| t.as_ref().to_os_string()) + .collect(); + self + } + + /// (chainable) Set the current working directory of the process + pub fn cwd>(&mut self, path: T) -> &mut ProcessBuilder { + self.cwd = Some(path.as_ref().to_os_string()); + self + } + + /// (chainable) Set an environment variable for the process. + pub fn env>(&mut self, key: &str, val: T) -> &mut ProcessBuilder { + self.env + .insert(key.to_string(), Some(val.as_ref().to_os_string())); + self + } + + /// (chainable) Unset an environment variable for the process. + pub fn env_remove(&mut self, key: &str) -> &mut ProcessBuilder { + self.env.insert(key.to_string(), None); + self + } + + /// Get the executable name. + pub fn get_program(&self) -> &OsString { + &self.program + } + + /// Get the program arguments + pub fn get_args(&self) -> &[OsString] { + &self.args + } + + /// Get the current working directory for the process + pub fn get_cwd(&self) -> Option<&Path> { + self.cwd.as_ref().map(Path::new) + } + + /// Get an environment variable as the process will see it (will inherit from environment + /// unless explicitally unset). + pub fn get_env(&self, var: &str) -> Option { + self.env + .get(var) + .cloned() + .or_else(|| Some(env::var_os(var))) + .and_then(|s| s) + } + + /// Get all environment variables explicitly set or unset for the process (not inherited + /// vars). + pub fn get_envs(&self) -> &HashMap> { + &self.env + } + + /// Run the process, waiting for completion, and mapping non-success exit codes to an error. + pub fn exec(&self) -> Fallible<()> { + let mut command = self.build_command(); + + let exit = match command.status() { + Ok(e) => e, + Err(_) => { + throw!(process_error( + &format!("could not execute process {}", self), + None, + None + )); + } + }; + + if exit.success() { + Ok(()) + } else { + Err(process_error( + &format!("process didn't exit successfully: {}", self), + Some(exit), + None, + ).into()) + } + } + + /// Execute the process, returning the stdio output, or an error if non-zero exit status. + pub fn exec_with_output(&self) -> Fallible { + let mut command = self.build_command(); + + let output = match command.output() { + Ok(o) => o, + Err(_) => { + throw!(process_error( + &format!("could not execute process {}", self), + None, + None + )); + } + }; + + if output.status.success() { + Ok(output) + } else { + Err(process_error( + &format!("process didn't exit successfully: {}", self), + Some(output.status), + Some(&output), + ).into()) + } + } + + /// Converts ProcessBuilder into a `std::process::Command` + pub fn build_command(&self) -> Command { + let mut command = Command::new(&self.program); + if let Some(cwd) = self.get_cwd() { + command.current_dir(cwd); + } + for arg in &self.args { + command.arg(arg); + } + for (k, v) in &self.env { + match *v { + Some(ref v) => { + command.env(k, v); + } + None => { + command.env_remove(k); + } + } + } + command + } +} + +/// A helper function to create a `ProcessBuilder`. +pub fn process>(cmd: T) -> ProcessBuilder { + ProcessBuilder { + program: cmd.as_ref().to_os_string(), + args: Vec::new(), + cwd: None, + env: HashMap::new(), + } +} + +#[derive(Debug, Fail, NotionFail)] +#[fail(display = "{}", desc)] +#[notion_fail(code = "ExecutionFailure")] +pub struct ProcessError { + pub desc: String, + pub exit: Option, + pub output: Option, +} + +pub fn process_error( + msg: &str, + status: Option, + output: Option<&Output>, +) -> ProcessError { + let exit = match status { + Some(s) => status_to_string(s), + None => "never executed".to_string(), + }; + let mut desc = format!("{} ({})", &msg, exit); + + if let Some(out) = output { + match str::from_utf8(&out.stdout) { + Ok(s) if !s.trim().is_empty() => { + desc.push_str("\n--- stdout\n"); + desc.push_str(s); + } + Ok(..) | Err(..) => {} + } + match str::from_utf8(&out.stderr) { + Ok(s) if !s.trim().is_empty() => { + desc.push_str("\n--- stderr\n"); + desc.push_str(s); + } + Ok(..) | Err(..) => {} + } + } + + return ProcessError { + desc, + exit: status, + output: output.cloned(), + }; + + fn status_to_string(status: ExitStatus) -> String { + status.to_string() + } +} + diff --git a/package.json b/dev/package.json similarity index 100% rename from package.json rename to dev/package.json diff --git a/src/command/install.rs b/src/command/install.rs index 0c349b284..1f5ace516 100644 --- a/src/command/install.rs +++ b/src/command/install.rs @@ -20,6 +20,8 @@ pub(crate) enum Install { Yarn(VersionSpec), Other { package: String, + // not used + #[allow(dead_code)] version: VersionSpec, }, } diff --git a/src/command/use_.rs b/src/command/use_.rs index 9ab7841d5..fee47f0d1 100644 --- a/src/command/use_.rs +++ b/src/command/use_.rs @@ -34,7 +34,12 @@ pub(crate) enum Use { Help, Node(VersionSpec), Yarn(VersionSpec), - Other { name: String, version: VersionSpec }, + Other { + name: String, + // not currently used + #[allow(dead_code)] + version: VersionSpec, + }, } impl Command for Use { @@ -78,10 +83,7 @@ Options: Use::Help => Help::Command(CommandName::Use).run(session)?, Use::Node(spec) => session.pin_node_version(&spec)?, Use::Yarn(spec) => session.pin_yarn_version(&spec)?, - Use::Other { - name: _name, - version: _, - } => throw!(NoCustomUseError::new(_name)), + Use::Other { name, .. } => throw!(NoCustomUseError::new(name)), }; session.add_event_end(ActivityKind::Use, ExitCode::Success); Ok(()) diff --git a/tests/acceptance/main.rs b/tests/acceptance/main.rs new file mode 100644 index 000000000..d6bb05824 --- /dev/null +++ b/tests/acceptance/main.rs @@ -0,0 +1,21 @@ +#[macro_use] +extern crate cfg_if; +extern crate failure; +#[macro_use] +extern crate hamcrest2; +#[cfg(feature = "mock-network")] +extern crate mockito; +extern crate notion_core; +extern crate notion_fail; +extern crate rand; +extern crate reqwest; +extern crate serde_json; +extern crate test_support; + +mod support; + +// test files + +mod notion_current; +mod notion_deactivate; +mod notion_use; diff --git a/tests/acceptance/notion_current.rs b/tests/acceptance/notion_current.rs new file mode 100644 index 000000000..fa9e66934 --- /dev/null +++ b/tests/acceptance/notion_current.rs @@ -0,0 +1,162 @@ +use hamcrest2::core::Matcher; +use test_support::matchers::execs; +use support::sandbox::sandbox; + +use notion_fail::ExitCode; + +const BASIC_PACKAGE_JSON: &'static str = r#"{ + "name": "test-package" +}"#; + +fn package_json_with_pinned_node(version: &str) -> String { + format!( + r#"{{ + "name": "test-package", + "toolchain": {{ + "node": "{}" + }} +}}"#, + version + ) +} + +#[test] +fn pinned_project() { + let s = sandbox() + .package_json(&package_json_with_pinned_node("1.7.19")) + .build(); + + assert_that!( + s.notion("current"), + execs() + .with_status(0) + .with_stdout_contains("project: v1.7.19 (active)") + ); +} + +#[test] +fn pinned_project_with_user_node_env() { + let s = sandbox() + .package_json(&package_json_with_pinned_node("1.7.19")) + .env("NOTION_NODE_VERSION", "2.18.5") + .build(); + + assert_that!( + s.notion("current"), + execs() + .with_status(0) + .with_stdout_contains("project: v1.7.19 (active)") + .with_stdout_contains("user: v2.18.5") + ); +} + +#[test] +fn pinned_project_with_user_node_default() { + let s = sandbox() + .package_json(&package_json_with_pinned_node("1.7.19")) + .catalog( + r#"[node] +default = '9.12.11' +versions = [ '9.12.11' ] +"#, + ) + .build(); + + assert_that!( + s.notion("current"), + execs() + .with_status(0) + .with_stdout_contains("project: v1.7.19 (active)") + .with_stdout_contains("user: v9.12.11") + ); +} + +#[test] +fn unpinned_project() { + let s = sandbox().package_json(BASIC_PACKAGE_JSON).build(); + + assert_that!( + s.notion("current"), + execs() + .with_status(ExitCode::NoVersionMatch as i32) + .with_stderr("error: no versions found") + ); +} + +#[test] +fn unpinned_project_with_user_node_env() { + let s = sandbox() + .package_json(BASIC_PACKAGE_JSON) + .env("NOTION_NODE_VERSION", "2.18.5") + .build(); + + assert_that!( + s.notion("current"), + execs() + .with_status(0) + .with_stdout_contains("user: v2.18.5 (active)") + ); +} + +#[test] +fn unpinned_project_with_user_node_default() { + let s = sandbox() + .package_json(BASIC_PACKAGE_JSON) + .catalog( + r#"[node] +default = '9.12.11' +versions = [ '9.12.11' ] +"#, + ) + .build(); + + assert_that!( + s.notion("current"), + execs() + .with_status(0) + .with_stdout_contains("user: v9.12.11 (active)") + ); +} + +#[test] +fn no_project() { + let s = sandbox().build(); + + assert_that!( + s.notion("current"), + execs() + .with_status(ExitCode::NoVersionMatch as i32) + .with_stderr("error: no versions found") + ); +} + +#[test] +fn no_project_with_user_node_env() { + let s = sandbox().env("NOTION_NODE_VERSION", "2.18.5").build(); + + assert_that!( + s.notion("current"), + execs() + .with_status(0) + .with_stdout_contains("user: v2.18.5 (active)") + ); +} + +#[test] +fn no_project_with_user_node_default() { + let s = sandbox() + .catalog( + r#"[node] +default = '9.12.11' +versions = [ '9.12.11' ] +"#, + ) + .build(); + + assert_that!( + s.notion("current"), + execs() + .with_status(0) + .with_stdout_contains("user: v9.12.11 (active)") + ); +} diff --git a/tests/acceptance/notion_deactivate.rs b/tests/acceptance/notion_deactivate.rs new file mode 100644 index 000000000..44f2c4563 --- /dev/null +++ b/tests/acceptance/notion_deactivate.rs @@ -0,0 +1,20 @@ +use hamcrest2::core::Matcher; +use test_support::matchers::execs; +use support::sandbox::sandbox; + +#[test] +#[cfg(unix)] +fn deactivate_bash() { + let s = sandbox() + .notion_shell("bash") + .path_dir("/usr/bin") + .path_dir("/usr/local/bin") + .build(); + + assert_that!(s.notion("deactivate"), execs().with_status(0)); + + assert_eq!( + s.read_postscript(), + "export PATH='/usr/bin:/usr/local/bin'\n", + ) +} diff --git a/tests/acceptance/notion_use.rs b/tests/acceptance/notion_use.rs new file mode 100644 index 000000000..1562cdbea --- /dev/null +++ b/tests/acceptance/notion_use.rs @@ -0,0 +1,143 @@ +use hamcrest2::core::Matcher; +use test_support::matchers::execs; +use support::sandbox::sandbox; + +use notion_fail::ExitCode; + +const BASIC_PACKAGE_JSON: &'static str = r#"{ + "name": "test-package" +}"#; + +fn package_json_with_pinned_node(version: &str) -> String { + format!( + r#"{{ + "name": "test-package", + "toolchain": {{ + "node": "{}" + }} +}}"#, + version + ) +} + +fn package_json_with_pinned_node_yarn(node_version: &str, yarn_version: &str) -> String { + format!( + r#"{{ + "name": "test-package", + "toolchain": {{ + "node": "{}", + "yarn": "{}" + }} +}}"#, + node_version, yarn_version + ) +} + +const NODE_VERSION_INFO: &'static str = r#"[ +{"version":"v10.18.11","files":["linux-x64","osx-x64-tar","win-x64-zip","win-x86-zip"]}, +{"version":"v10.13.12","files":["linux-x64","osx-x64-tar","win-x64-zip","win-x86-zip"]}, +{"version":"v9.13.2","files":["linux-x64","osx-x64-tar","win-x64-zip","win-x86-zip"]}, +{"version":"v8.8.923","files":["linux-x64","osx-x64-tar","win-x64-zip","win-x86-zip"]} +]"#; + +#[test] +fn use_node() { + let s = sandbox() + .package_json(BASIC_PACKAGE_JSON) + .node_available_versions(NODE_VERSION_INFO) + .node_archive_mocks() + .build(); + + assert_that!( + s.notion("use node 10"), + execs() + .with_status(0) + .with_stdout_contains("Pinned node to version 10.18.11 in package.json") + ); + + assert_eq!( + s.read_package_json(), + package_json_with_pinned_node("10.18.11"), + ) +} + +#[test] +fn use_node_latest() { + let s = sandbox() + .package_json(BASIC_PACKAGE_JSON) + .node_available_versions(NODE_VERSION_INFO) + .node_archive_mocks() + .build(); + + assert_that!( + s.notion("use node latest"), + execs() + .with_status(0) + .with_stdout_contains("Pinned node to version 10.18.11 in package.json") + ); + + assert_eq!( + s.read_package_json(), + package_json_with_pinned_node("10.18.11"), + ) +} + +#[test] +fn use_yarn_no_node() { + let s = sandbox() + .package_json(BASIC_PACKAGE_JSON) + .yarn_available_versions(r#"[ "1.0.0", "1.0.1", "1.2.0", "1.4.0", "1.9.2", "1.9.4" ]"#) + .yarn_archive_mocks() + .build(); + + assert_that!( + s.notion("use yarn 1.4"), + execs() + .with_status(ExitCode::ConfigurationError as i32) + .with_stderr_contains("error: There is no pinned node version for this project") + ); + + assert_eq!(s.read_package_json(), BASIC_PACKAGE_JSON,) +} + +#[test] +fn use_yarn() { + let s = sandbox() + .package_json(&package_json_with_pinned_node("1.2.3")) + .yarn_available_versions(r#"[ "1.0.0", "1.0.1", "1.2.0", "1.4.0", "1.9.2", "1.9.4" ]"#) + .yarn_archive_mocks() + .build(); + + assert_that!( + s.notion("use yarn 1.4"), + execs() + .with_status(0) + .with_stdout_contains("Pinned yarn to version 1.4.0 in package.json") + ); + + assert_eq!( + s.read_package_json(), + package_json_with_pinned_node_yarn("1.2.3", "1.4.0"), + ) +} + +#[test] +fn use_yarn_latest() { + let s = sandbox() + .package_json(&package_json_with_pinned_node("1.2.3")) + .yarn_latest("1.2.0") + .yarn_archive_mocks() + .build(); + + assert_that!( + s.notion("use yarn latest"), + execs() + .with_status(0) + .with_stdout_contains("Pinned yarn to version 1.2.0 in package.json") + ); + + assert_eq!( + s.read_package_json(), + package_json_with_pinned_node_yarn("1.2.3", "1.2.0"), + ) +} diff --git a/tests/acceptance/support/mod.rs b/tests/acceptance/support/mod.rs new file mode 100644 index 000000000..9dcec4f56 --- /dev/null +++ b/tests/acceptance/support/mod.rs @@ -0,0 +1,11 @@ +macro_rules! ok_or_panic { + { $e:expr } => { + match $e { + Ok(x) => x, + Err(err) => panic!("{} failed with {}", stringify!($e), err), + } + }; +} + +pub mod paths; +pub mod sandbox; diff --git a/tests/acceptance/support/paths.rs b/tests/acceptance/support/paths.rs new file mode 100644 index 000000000..8007242b3 --- /dev/null +++ b/tests/acceptance/support/paths.rs @@ -0,0 +1,115 @@ +use std::cell::Cell; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicUsize, Ordering, ATOMIC_USIZE_INIT}; +use std::sync::{Once, ONCE_INIT}; + +static ACCEPTANCE_TEST_DIR: &'static str = "acceptance_test"; +static NEXT_ID: AtomicUsize = ATOMIC_USIZE_INIT; + +thread_local!(static TASK_ID: usize = NEXT_ID.fetch_add(1, Ordering::SeqCst)); + + +// creates the root directory for the acceptancce tests (once), and +// initializes the root and home directories for the current task +fn init() { + static GLOBAL_INIT: Once = ONCE_INIT; + thread_local!(static LOCAL_INIT: Cell = Cell::new(false)); + GLOBAL_INIT.call_once(|| { + global_root().mkdir_p(); + }); + LOCAL_INIT.with(|i| { + if i.get() { + return; + } + i.set(true); + root().rm_rf(); + home().mkdir_p(); + }) +} + +// the root directory for the acceptance tests, in `target/acceptance_test` +fn global_root() -> PathBuf { + let mut path = ok_or_panic!{ env::current_exe() }; + path.pop(); // chop off exe name + path.pop(); // chop off 'debug' + + // If `cargo test` is run manually then our path looks like + // `target/debug/foo`, in which case our `path` is already pointing at + // `target`. If, however, `cargo test --target $target` is used then the + // output is `target/$target/debug/foo`, so our path is pointing at + // `target/$target`. Here we conditionally pop the `$target` name. + if path.file_name().and_then(|s| s.to_str()) != Some("target") { + path.pop(); + } + + path.join(ACCEPTANCE_TEST_DIR) +} + +pub fn root() -> PathBuf { + init(); + global_root().join(&TASK_ID.with(|my_id| format!("t{}", my_id))) +} + +pub fn home() -> PathBuf { + root().join("home") +} + +enum Remove { File, Dir } +impl Remove { + fn to_str(&self) -> &'static str { + match *self { + Remove::File => "remove file", + Remove::Dir => "remove dir" + } + } + + fn at(&self, path: &Path) -> () { + if cfg!(windows) { + let mut p = ok_or_panic!(path.metadata()).permissions(); + p.set_readonly(false); + ok_or_panic!{ fs::set_permissions(path, p) }; + } + match *self { + Remove::File => fs::remove_file(path), + Remove::Dir => fs::remove_dir(path) + }.unwrap_or_else(|e| { + panic!("failed to {} {}: {}", self.to_str(), path.display(), e); + }) + } +} + +pub trait PathExt { + fn rm_rf(&self); + fn mkdir_p(&self); +} + +impl PathExt for Path { + /* Technically there is a potential race condition, but we don't + * care all that much for our tests + */ + fn rm_rf(&self) { + if !self.exists() { + return; + } + + for file in ok_or_panic!{ fs::read_dir(self) } { + let file = ok_or_panic!{ file }; + if file.file_type().map(|m| m.is_dir()).unwrap_or(false) { + file.path().rm_rf(); + } else { + // On windows we can't remove a readonly file, and git will + // often clone files as readonly. As a result, we have some + // special logic to remove readonly files on windows. + Remove::File.at(&file.path()); + } + } + Remove::Dir.at(self); + } + + fn mkdir_p(&self) { + fs::create_dir_all(self) + .unwrap_or_else(|e| panic!("failed to mkdir_p {}: {}", self.display(), e)) + } +} diff --git a/tests/acceptance/support/sandbox.rs b/tests/acceptance/support/sandbox.rs new file mode 100644 index 000000000..0de843d92 --- /dev/null +++ b/tests/acceptance/support/sandbox.rs @@ -0,0 +1,505 @@ +use std::env; +use std::ffi::{OsStr, OsString}; +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::iter; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime}; + +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use reqwest::header::HttpDate; + +use support::paths::{self, PathExt}; +use test_support; +use test_support::process::ProcessBuilder; + +#[cfg(feature = "mock-network")] +use mockito::{self, mock, Matcher}; + +// version cache for node and yarn +#[derive(PartialEq, Clone)] +struct CacheBuilder { + path: PathBuf, + expiry_path: PathBuf, + contents: String, + expired: bool, +} + +impl CacheBuilder { + pub fn new(path: PathBuf, expiry_path: PathBuf, contents: &str, expired: bool) -> CacheBuilder { + CacheBuilder { + path, + expiry_path, + contents: contents.to_string(), + expired, + } + } + + fn build(&self) { + self.dirname().mkdir_p(); + + // write cache file + let mut cache_file = File::create(&self.path).unwrap_or_else(|e| { + panic!("could not create cache file {}: {}", self.path.display(), e) + }); + ok_or_panic!{ cache_file.write_all(self.contents.as_bytes()) }; + + // write expiry file + let one_day = Duration::from_secs(24 * 60 * 60); + let expiry_date = HttpDate::from(if self.expired { + SystemTime::now() - one_day + } else { + SystemTime::now() + one_day + }); + let mut expiry_file = File::create(&self.expiry_path).unwrap_or_else(|e| { + panic!( + "could not create cache expiry file {}: {}", + self.expiry_path.display(), + e + ) + }); + ok_or_panic!{ expiry_file.write_all(expiry_date.to_string().as_bytes()) }; + } + + fn dirname(&self) -> &Path { + self.path.parent().unwrap() + } +} + +// environment variables +pub struct EnvVar { + name: String, + value: String, +} + +impl EnvVar { + pub fn new(name: &str, value: &str) -> Self { + EnvVar { + name: name.to_string(), + value: value.to_string(), + } + } +} + +// catalog.toml +#[derive(PartialEq, Clone)] +pub struct FileBuilder { + path: PathBuf, + contents: String, +} + +impl FileBuilder { + pub fn new(path: PathBuf, contents: &str) -> FileBuilder { + FileBuilder { + path, + contents: contents.to_string(), + } + } + + pub fn build(&self) { + self.dirname().mkdir_p(); + + let mut file = File::create(&self.path) + .unwrap_or_else(|e| panic!("could not create file {}: {}", self.path.display(), e)); + + ok_or_panic!{ file.write_all(self.contents.as_bytes()) }; + } + + fn dirname(&self) -> &Path { + self.path.parent().unwrap() + } +} + +// because the http request methods from reqwest show up as in mockito +cfg_if! { + if #[cfg(all(windows, target_arch = "x86_64"))] { + fn method_name(_method: &str) -> &str { + "" + } + } else { + fn method_name(method: &str) -> &str { + method + } + } +} + +#[must_use] +pub struct SandboxBuilder { + root: Sandbox, + files: Vec, + caches: Vec, + path_dirs: Vec, +} + +impl SandboxBuilder { + /// Root of the project, ex: `/path/to/cargo/target/integration_test/t0/foo` + pub fn root(&self) -> PathBuf { + self.root.root() + } + + pub fn new(root: PathBuf) -> SandboxBuilder { + SandboxBuilder { + root: Sandbox { + root, + mocks: vec![], + env_vars: vec![], + path: OsString::new(), + }, + files: vec![], + caches: vec![], + path_dirs: vec![notion_bin_dir()], + } + } + + /// Set the Node cache for the sandbox (chainable) + pub fn node_cache(mut self, cache: &str, expired: bool) -> Self { + self.caches.push(CacheBuilder::new( + node_index_file(), + node_index_expiry_file(), + cache, + expired, + )); + self + } + + /// Set the package.json for the sandbox (chainable) + pub fn package_json(mut self, contents: &str) -> Self { + let package_file = package_json_file(self.root()); + self.files.push(FileBuilder::new(package_file, contents)); + self + } + + /// Set the catalog.toml for the sandbox (chainable) + pub fn catalog(mut self, contents: &str) -> Self { + self.files + .push(FileBuilder::new(user_catalog_file(), contents)); + self + } + + /// Set the shell for the sandbox (chainable) + pub fn notion_shell(mut self, shell_name: &str) -> Self { + self.root + .env_vars + .push(EnvVar::new("NOTION_SHELL", shell_name)); + self + } + + /// Set an environment variable for the sandbox (chainable) + pub fn env(mut self, name: &str, value: &str) -> Self { + self.root.env_vars.push(EnvVar::new(name, value)); + self + } + + /// Add a directory to the PATH (chainable) + pub fn path_dir(mut self, dir: &str) -> Self { + self.path_dirs.push(PathBuf::from(dir)); + self + } + + /// Setup mock to return the available node versions (chainable) + pub fn node_available_versions(mut self, body: &str) -> Self { + let mock = mock(method_name("GET"), "/node-dist/index.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(body) + .create(); + self.root.mocks.push(mock); + + self + } + + /// Setup mocks to return info about the node archive file (chainable) + pub fn node_archive_mocks(mut self) -> Self { + // ISSUE(#145): this should actually use a real http server instead of these mocks + + // generate a "file" that is 200 bytes long + let mut rng = thread_rng(); + let archive_file_mock: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(200) + .collect(); + + // mock the HEAD request, which gets the file size + let head_mock = mock( + method_name("HEAD"), + Matcher::Regex(r"^/v\d+.\d+.\d+/node-v\d+.\d+.\d+".to_string()), + ).with_header("Accept-Ranges", "bytes") + .with_body(&archive_file_mock) + .create(); + self.root.mocks.push(head_mock); + + // mock the "Range: bytes" request, which gets the ISIZE value (last 4 bytes) + // this will be interpreted as a packed integer value + // (doesn't really matter - used for progress bar) + let range_mock = mock( + method_name("GET"), + Matcher::Regex(r"^/v\d+.\d+.\d+/node-v\d+.\d+.\d+".to_string()), + ).match_header("Range", Matcher::Any) + .with_body("1234") + .create(); + self.root.mocks.push(range_mock); + + // mock the file download + let file_mock = mock( + method_name("GET"), + Matcher::Regex(r"^/v\d+.\d+.\d+/node-v\d+.\d+.\d+".to_string()), + ).match_header("Range", Matcher::Missing) + .with_body(&archive_file_mock) + .create(); + self.root.mocks.push(file_mock); + + self + } + + /// Setup mock to return the available yarn versions (chainable) + pub fn yarn_available_versions(mut self, body: &str) -> Self { + let mock = mock(method_name("GET"), "/yarn-releases/index.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(body) + .create(); + self.root.mocks.push(mock); + self + } + + /// Setup mock to return the latest version of yarn (chainable) + pub fn yarn_latest(mut self, version: &str) -> Self { + let mock = mock(method_name("GET"), "/yarn-latest") + .with_status(200) + .with_body(version) + .create(); + self.root.mocks.push(mock); + self + } + + /// Setup mocks to return info about the yarn archive file (chainable) + pub fn yarn_archive_mocks(mut self) -> Self { + // ISSUE(#145): this should actually use a real http server instead of these mocks + + // generate a "file" that is 200 bytes long + let mut rng = thread_rng(); + let archive_file_mock: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(200) + .collect(); + + // mock the HEAD request, which gets the file size + let head_mock = mock(method_name("HEAD"), Matcher::Regex(r"^/yarn-v\d+.\d+.\d+".to_string())) + .with_header("Accept-Ranges", "bytes") + .with_body(&archive_file_mock) + .create(); + self.root.mocks.push(head_mock); + + // mock the "Range: bytes" request, which gets the ISIZE value (last 4 bytes) + // this will be interpreted as a packed integer value + // (doesn't really matter - used for progress bar) + let range_mock = mock(method_name("GET"), Matcher::Regex(r"^/yarn-v\d+.\d+.\d+".to_string())) + .match_header("Range", Matcher::Any) + .with_body("1234") + .create(); + self.root.mocks.push(range_mock); + + // mock the file download + let file_mock = mock(method_name("GET"), Matcher::Regex(r"^/yarn-v\d+.\d+.\d+".to_string())) + .match_header("Range", Matcher::Missing) + .with_body(&archive_file_mock) + .create(); + self.root.mocks.push(file_mock); + + self + } + + /// Create the project + pub fn build(mut self) -> Sandbox { + // First, clean the directory if it already exists + self.rm_root(); + + // Create the empty directory + self.root.root().mkdir_p(); + + // make sure these directories exist + ok_or_panic!{ fs::create_dir_all(node_cache_dir()) }; + ok_or_panic!{ fs::create_dir_all(yarn_cache_dir()) }; + ok_or_panic!{ fs::create_dir_all(notion_tmp_dir()) }; + + // write node and yarn caches + for cache in self.caches.iter() { + cache.build(); + } + + // write files + for file_builder in self.files { + file_builder.build(); + } + + // join dirs for the path (notion bin path is already first) + self.root.path = env::join_paths(self.path_dirs.iter()).unwrap(); + + let SandboxBuilder { root, .. } = self; + root + } + + fn rm_root(&self) { + self.root.root().rm_rf() + } +} + +// files and dirs in the sandbox + +fn home_dir() -> PathBuf { + paths::home() +} +fn notion_home() -> PathBuf { + home_dir().join(".notion") +} +fn notion_tmp_dir() -> PathBuf { + notion_home().join("tmp") +} +fn notion_bin_dir() -> PathBuf { + notion_home().join("bin") +} +fn notion_postscript() -> PathBuf { + notion_tmp_dir().join("notion_tmp_1234.sh") +} +#[cfg(unix)] +fn cache_dir() -> PathBuf { + notion_home().join("cache") +} +#[cfg(windows)] +fn cache_dir() -> PathBuf { + home_dir().join("Notion").join("cache") +} +fn node_cache_dir() -> PathBuf { + cache_dir().join("node") +} +fn yarn_cache_dir() -> PathBuf { + cache_dir().join("yarn") +} +fn node_index_file() -> PathBuf { + node_cache_dir().join("index.json") +} +fn node_index_expiry_file() -> PathBuf { + node_cache_dir().join("index.json.expires") +} +fn package_json_file(mut root: PathBuf) -> PathBuf { + root.push("package.json"); + root +} +#[cfg(unix)] +fn user_catalog_file() -> PathBuf { + notion_home().join("catalog.toml") +} +#[cfg(windows)] +fn local_data_root() -> PathBuf { + home_dir().join("AppData").join("Local").join("Notion") +} +#[cfg(windows)] +fn user_catalog_file() -> PathBuf { + local_data_root().join("catalog.toml") +} + +pub struct Sandbox { + root: PathBuf, + mocks: Vec, + env_vars: Vec, + path: OsString, +} + +impl Sandbox { + /// Root of the project, ex: `/path/to/cargo/target/integration_test/t0/foo` + pub fn root(&self) -> PathBuf { + self.root.clone() + } + + /// Create a `ProcessBuilder` to run a program in the project. + /// Example: + /// assert_that( + /// p.process(&p.bin("foo")), + /// execs().with_stdout("bar\n"), + /// ); + pub fn process>(&self, program: T) -> ProcessBuilder { + let mut p = test_support::process::process(program); + p.cwd(self.root()) + // sandbox the Notion environment + .env("NOTION_SANDBOX", "true") // used to indicate that Notion is running sandboxed, for directory logic in Windows + .env("HOME", home_dir()) + .env("USERPROFILE", home_dir()) // windows + .env("NOTION_HOME", notion_home()) + .env("NOTION_DATA_ROOT", notion_home()) // windows + .env("PATH", &self.path) + .env("NOTION_POSTSCRIPT", notion_postscript()) + .env_remove("NOTION_DEV") + .env_remove("NOTION_NODE_VERSION") + .env_remove("NOTION_SHELL") + .env_remove("MSYSTEM"); // assume cmd.exe everywhere on windows + + // overrides for env vars + for env_var in &self.env_vars { + p.env(&env_var.name, &env_var.value); + } + + p + } + + /// Create a `ProcessBuilder` to run notion. + /// Arguments can be separated by spaces. + /// Example: + /// assert_that(p.notion("use node 9.5"), execs()); + pub fn notion(&self, cmd: &str) -> ProcessBuilder { + let mut p = self.process(¬ion_exe()); + split_and_add_args(&mut p, cmd); + p + } + + pub fn read_package_json(&self) -> String { + let package_file = package_json_file(self.root()); + read_file_to_string(package_file) + } + + pub fn read_postscript(&self) -> String { + let postscript_file = notion_postscript(); + read_file_to_string(postscript_file) + } +} + +// Generates a sandboxed environment +pub fn sandbox() -> SandboxBuilder { + SandboxBuilder::new(paths::root().join("sandbox")) +} + +// Path to compiled executables +pub fn cargo_dir() -> PathBuf { + env::var_os("CARGO_BIN_PATH") + .map(PathBuf::from) + .or_else(|| { + env::current_exe().ok().map(|mut path| { + path.pop(); + if path.ends_with("deps") { + path.pop(); + } + path + }) + }) + .unwrap_or_else(|| panic!("CARGO_BIN_PATH wasn't set. Cannot continue running test")) +} + +fn notion_exe() -> PathBuf { + cargo_dir().join(format!("notion{}", env::consts::EXE_SUFFIX)) +} + +fn split_and_add_args(p: &mut ProcessBuilder, s: &str) { + for arg in s.split_whitespace() { + if arg.contains('"') || arg.contains('\'') { + panic!("shell-style argument parsing is not supported") + } + p.arg(arg); + } +} + +fn read_file_to_string(file_path: PathBuf) -> String { + let mut contents = String::new(); + let mut file = ok_or_panic!{ File::open(file_path) }; + ok_or_panic!{ file.read_to_string(&mut contents) }; + contents +}