diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9c5061 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +node_modules +test/proxy~~.dump diff --git a/package-lock.json b/package-lock.json index f57051e..e24ee5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,14 @@ { "name": "n", "version": "3.0.3-0", - "lockfileVersion": 1 + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "bats": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bats/-/bats-1.1.0.tgz", + "integrity": "sha512-1pA29OhDByrUtAXX+nmqZxgRgx2y8PvuZzbLJVjd2dpEDVDvz0MjcBMdmIPNq5lC+tG53G+RbeRsbIlv3vw7tg==", + "dev": true + } + } } diff --git a/package.json b/package.json index 493655b..7cffdbd 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,12 @@ "type": "git", "url": "git://github.com/tj/n.git" }, + "scripts": { + "test": "test/bin/run-all-tests" + }, + "devDependencies": { + "bats": "^1.1.0" + }, "preferGlobal": true, "os": [ "!win32" diff --git a/test/bin/proxy-build b/test/bin/proxy-build new file mode 100755 index 0000000..dce33cc --- /dev/null +++ b/test/bin/proxy-build @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Unoffical bash safe mode +set -euo pipefail +BIN_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +waitproxy() { + while ! nc -z localhost 8080 ; do sleep 1 ; done +} + +if nc -z localhost 8080; then + echo "Error: port 8080 already in use. Is mitmdump already running?" + pgrep -f -l mitmdump + exit 2 +fi + +echo "Launching proxy..." +mitmdump -w proxy~~.dump &> /dev/null & +mitm_process="$!" +echo "Waiting for proxy..." +waitproxy + +echo "Recording downloads..." + +source tests/shared-functions.bash +unset_n_env +setup_tmp_prefix +install_dummy_node + +# Hack curl to avoid certificate issues with proxy +readonly CURL_HOME="$(dirname "${BIN_DIRECTORY}")/config" +export CURL_HOME + +# Go through proxy so it can record traffic, http for taobao redirects +http_proxy="$(hostname):8080" +export http_proxy +https_proxy="$(hostname):8080" +export https_proxy + +# native +tests/install-reference-versions.bash +# linux +docker-compose run ubuntu-curl /mnt/tests/install-reference-versions.bash + +rm -rf "${TMP_PREFIX_DIR}" +echo "Stopping proxy" +kill "${mitm_process}" + diff --git a/test/bin/proxy-run b/test/bin/proxy-run new file mode 100755 index 0000000..1cd303b --- /dev/null +++ b/test/bin/proxy-run @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +BIN_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +echo "" +echo "To make use of proxy server:" +echo " export https_proxy=\"$(hostname):8080\"" +echo " export http_proxy=\"$(hostname):8080\"" +echo " export CURL_HOME=$(dirname "${BIN_DIRECTORY}")/config" +echo "" + +echo "Launching proxy server..." +mitmdump --server-replay-nopop --server-replay proxy~~.dump diff --git a/test/bin/run-all-tests b/test/bin/run-all-tests new file mode 100755 index 0000000..2bf9b07 --- /dev/null +++ b/test/bin/run-all-tests @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +BIN_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +services=( ubuntu-curl ubuntu-wget ) + +cd "$(dirname "${BIN_DIRECTORY}")" || exit 2 +for service in "${services[@]}" ; do + echo "${service}" + docker-compose run --rm "${service}" "bats" "/mnt/tests" + echo "" +done + +uname -s +../node_modules/.bin/bats tests diff --git a/test/config/.curlrc b/test/config/.curlrc new file mode 100644 index 0000000..9036dc9 --- /dev/null +++ b/test/config/.curlrc @@ -0,0 +1,2 @@ +# Allow use of mitm proxy +--insecure diff --git a/test/docker-base.yml b/test/docker-base.yml new file mode 100644 index 0000000..7682802 --- /dev/null +++ b/test/docker-base.yml @@ -0,0 +1,20 @@ +version: '2' +# Define base service to specify the mounts and environment variables +services: + testbed: + volumes: + # make locally installed bats available in container (based on bats/install.sh) + - ../node_modules/bats/bin/bats:/usr/local/bin/bats + - ../node_modules/bats/libexec/bats-core:/usr/local/libexec/bats-core + - ../node_modules/bats/man/bats.1:/usr/local/share/man/man1" + - ../node_modules/bats/man/bats.7:/usr/local/share/man/man7" + # the bats tests + - ./tests:/mnt/tests + # the n script + - ../bin/n:/usr/local/bin/n + # override curl settings to allow insecure connection in case using proxy + - ./config/.curlrc:/root/.curlrc + environment: + # pass through proxy settings to allow caching proxy + - http_proxy + - https_proxy diff --git a/test/docker-compose.yml b/test/docker-compose.yml new file mode 100644 index 0000000..dc5b658 --- /dev/null +++ b/test/docker-compose.yml @@ -0,0 +1,16 @@ +version: '2' +services: + ubuntu-curl: + extends: + file: ./docker-base.yml + service: testbed + build: + context: dockerfiles + dockerfile: Dockerfile-ubuntu-curl + ubuntu-wget: + extends: + file: ./docker-base.yml + service: testbed + build: + context: dockerfiles + dockerfile: Dockerfile-ubuntu-wget diff --git a/test/dockerfiles/Dockerfile-ubuntu-curl b/test/dockerfiles/Dockerfile-ubuntu-curl new file mode 100644 index 0000000..c395f60 --- /dev/null +++ b/test/dockerfiles/Dockerfile-ubuntu-curl @@ -0,0 +1,9 @@ +FROM ubuntu:latest + +# curl + +RUN apt-get update \ +&& apt-get install -y curl \ +&& rm -rf /var/lib/apt/lists/* + +CMD ["/bin/bash"] diff --git a/test/dockerfiles/Dockerfile-ubuntu-wget b/test/dockerfiles/Dockerfile-ubuntu-wget new file mode 100644 index 0000000..0c80b10 --- /dev/null +++ b/test/dockerfiles/Dockerfile-ubuntu-wget @@ -0,0 +1,9 @@ +FROM ubuntu:latest + +# wget + +RUN apt-get update \ +&& apt-get install -y wget \ +&& rm -rf /var/lib/apt/lists/* + +CMD ["/bin/bash"] diff --git a/test/tests.md b/test/tests.md new file mode 100644 index 0000000..3d87574 --- /dev/null +++ b/test/tests.md @@ -0,0 +1,70 @@ +# n-test + +Prototype and develop a set of automated tests for `n`. + +## Setup + +Optional proxy using mitmproxy: + + # using homebrew (Mac) to install mitmproxy + brew install mitmproxy + + +## Running Tests + +Run all the tests across a range of containers and on the host system: + + npm run test + +Run all the tests on a single system: + + cd test + npx bats tests + docker-compose run ubuntu-curl bats /mnt/tests + +Run single test on a single system:: + + cd test + npx bats tests/install-contents.bats + docker-compose run ubuntu-curl bats /mnt/tests/install-contents.bats + +## Proxy + +To speed up running tests multiple times, you can optionally run a caching proxy for the node downloads. The curl settings are modified +to allow an insecure connection through the mitm proxy. + + cd test + bin/proxy-build + bin/proxy-run + # follow the instructions for configuring environment variables for using proxy, then run tests + +`node` versions added to proxy cache (and used in tests): + +* v4.9.1 +* lts +* latest + +## Docker Tips + +Using `docker-compose` in addition to `docker` for convenient mounting of `n` script and the tests into the container. Changes to the tests or to `n` itself are reflected immediately without needing to rebuild the containers. + +`bats` is being mounted directly out of `node_modules` into the container as a manual install based on its own install script. This is a bit of a hack, but avoids needing to install `git` or `npm` for a full remote install of `bats`, and means everything on the same version of `bats`. + +The containers each have: + +* either curl or wget (or both) installed + +Using `docker-compose` to run the container adds: + +* specified `n` script mounted to `/usr/local/bin/n` +* `test/tests` mounted to `/mnt/tests` +* `node_modules/bats` provides `/usr/local/bin/bats` et al +* `.curlrc` with `--insecure` to allow use of proxy + +So for example: + + cd test + docker-compose run ubuntu-curl + # in container + n --version + bats /mnt/tests diff --git a/test/tests/install-contents.bats b/test/tests/install-contents.bats new file mode 100644 index 0000000..a30264c --- /dev/null +++ b/test/tests/install-contents.bats @@ -0,0 +1,43 @@ +#!/usr/bin/env bats + +load shared-functions + + +function setup() { + unset_n_env +} + + +# Test that files get installed to expected locations +# https://github.com/tj/n/issues/246 + +@test "install: contents" { + readonly TARGET_VERSION="4.9.1" + setup_tmp_prefix + + [ ! -d "${N_PREFIX}/n/versions" ] + [ ! -d "${N_PREFIX}/bin" ] + [ ! -d "${N_PREFIX}/include" ] + [ ! -d "${N_PREFIX}/lib" ] + [ ! -d "${N_PREFIX}/shared" ] + + install_dummy_node + n ${TARGET_VERSION} + + # Cached version + [ -d "${N_PREFIX}/n/versions/node/${TARGET_VERSION}" ] + # node and npm + [ -f "${N_PREFIX}/bin/node" ] + [ -f "${N_PREFIX}/bin/npm" ] + # Installed something into each of other key folders + [ -d "${N_PREFIX}/include/node" ] + [ -d "${N_PREFIX}/lib/node_modules" ] + [ -d "${N_PREFIX}/share/doc/node" ] + # Did not install files from top level of tarball + [ ! -f "${N_PREFIX}/README.md" ] + + run node --version + [ "${output}" = "v${TARGET_VERSION}" ] + + rm -rf "${TMP_PREFIX_DIR}" +} diff --git a/test/tests/install-options.bats b/test/tests/install-options.bats new file mode 100644 index 0000000..44d227c --- /dev/null +++ b/test/tests/install-options.bats @@ -0,0 +1,37 @@ +#!/usr/bin/env bats + +load shared-functions + + +function setup() { + unset_n_env + setup_tmp_prefix + install_dummy_node +} + + +function teardown() { + rm -rf "${TMP_PREFIX_DIR}" +} + + +@test "n --download 4.9.1" { + n --download 4.9.1 + [ -d "${N_PREFIX}/n/versions/node/4.9.1" ] + # Remember, we installed a dumy node so do have a bin/node + [ ! -f "${N_PREFIX}/bin/npm" ] + [ ! -d "${N_PREFIX}/include" ] + [ ! -d "${N_PREFIX}/lib" ] + [ ! -d "${N_PREFIX}/shared" ] +} + + +@test "n --quiet 4.9.1" { + # just checking option is allowed, not testing functionality + n --quiet 4.9.1 + run node --version + [ "${output}" = "v4.9.1" ] +} + + +# ToDo: --arch diff --git a/test/tests/install-reference-versions.bash b/test/tests/install-reference-versions.bash new file mode 100755 index 0000000..5fffa6e --- /dev/null +++ b/test/tests/install-reference-versions.bash @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# These are the versions installed and hence cached by proxy-build. + +# Run commands we want to cache downloads for + +# Get index into cache for lookups of expected versions. +curl --location --fail https://nodejs.org/dist/index.tab &> /dev/null + +# Using 4.9.1 as a well known old version (which is no longer getting updated so does not change) +n --download 4 +n --download lts +n --download latest diff --git a/test/tests/install-versions.bats b/test/tests/install-versions.bats new file mode 100644 index 0000000..fdd42da --- /dev/null +++ b/test/tests/install-versions.bats @@ -0,0 +1,76 @@ +#!/usr/bin/env bats + +load shared-functions + + +function setup() { + unset_n_env + setup_tmp_prefix + install_dummy_node +} + + +function teardown() { + rm -rf "${TMP_PREFIX_DIR}" +} + + +# Explicit version +@test "n 4.9.1" { + n 4.9.1 + run node --version + [ "${output}" = "v4.9.1" ] +} + + +# Explicit version, optional leading v +@test "n v4.9.1" { + n v4.9.1 + run node --version + [ "${output}" = "v4.9.1" ] +} + + +# Partial version +@test "n 4" { + n 4 + run node --version + [ "${output}" = "v4.9.1" ] +} + + +# Partial version, optional leading v +@test "n v4" { + n v4 + run node --version + [ "${output}" = "v4.9.1" ] +} + + +# Partial version +@test "n 4.9" { + n 4.9 + run node --version + [ "${output}" = "v4.9.1" ] +} + + +@test "n lts" { + n lts + run node --version + [ "${output}" = "$(display_remote_version lts)" ] +} + + +@test "n stable" { + n stable + run node --version + [ "${output}" = "$(display_remote_version lts)" ] +} + + +@test "n latest" { + n latest + run node --version + [ "${output}" = "$(display_remote_version latest)" ] +} diff --git a/test/tests/lookup.bats b/test/tests/lookup.bats new file mode 100644 index 0000000..39cf7dd --- /dev/null +++ b/test/tests/lookup.bats @@ -0,0 +1,28 @@ +#!/usr/bin/env bats + +load shared-functions + + +function setup() { + unset_n_env +} + + +@test "n --lts" { + run n --lts + [ "${status}" -eq 0 ] + local expected_version + expected_version="$(display_remote_version lts)" + expected_version="${expected_version#v}" + [ "${output}" = "${expected_version}" ] +} + + +@test "n --latest" { + run n --latest + [ "${status}" -eq 0 ] + local expected_version + expected_version="$(display_remote_version latest)" + expected_version="${expected_version#v}" + [ "${output}" = "${expected_version}" ] +} diff --git a/test/tests/shared-functions.bash b/test/tests/shared-functions.bash new file mode 100644 index 0000000..add414b --- /dev/null +++ b/test/tests/shared-functions.bash @@ -0,0 +1,104 @@ +#!/usr/bin/env bash + + +# unset the n environment variables so tests running from known state. +# Globals: +# lots + +function unset_n_env(){ + unset N_PREFIX + + # Undocumented [sic] + unset NODE_MIRROR + + # Documented under "custom source", but PROJECT and HTTP implemented as independent + unset PROJECT_NAME + unset PROJECT_URL + unset PROJECT_VERSION_CHECK + unset HTTP_USER + unset HTTP_PASSWORD +} + + +# Create a dummy version of node so `n install` will always activate (and not be affected by possible system version of node). + +function install_dummy_node() { + local prefix="${N_PREFIX-/usr/local}" + mkdir -p "${prefix}/bin" + echo "echo vDummy" > "${prefix}/bin/node" + chmod a+x "${prefix}/bin/node" +} + + +# Create temporary dir and configure n to use it. +# Globals: +# TMP_PREFIX_DIR +# N_PREFIX +# PATH + +function setup_tmp_prefix() { + TMP_PREFIX_DIR="$(mktemp -d)" + [ -d "${TMP_PREFIX_DIR}" ] || exit 2 + # return a safer variable to `rm -rf` later than N_PREFIX + export TMP_PREFIX_DIR + + export N_PREFIX="${TMP_PREFIX_DIR}" + export PATH="${N_PREFIX}/bin:${PATH}" +} + + +# Display relevant file name (third field of index.tab) for current platform. +# Based on code from nvm rather than n for independent approach. Simplified for just common platforms initially. +# See list on https://github.com/nodejs/nodejs-dist-indexer + +function display_compatible_file_field() { + local os="unexpected" + case "$(uname -a)" in + Linux\ *) os="linux" ;; + Darwin\ *) os="osx" ;; + esac + + local arch="unexpected" + local uname_m + uname_m="$(uname -m)" + case "${uname_m}" in + x86_64 | amd64) arch="x64" ;; + i*86) arch="x86" ;; + aarch64) arch="arm64" ;; + *) arch="${uname_m}" ;; + esac + + echo "${os}-${arch}" +} + + +# display_remote_version +# Limited support for using index.tab to resolve version into a number. +# Return version number, including leading v. + +function display_remote_version() { + # ToDo: support NODE_MIRROR + + local fetch + if command -v curl &> /dev/null; then + fetch="curl --silent --location --fail" + else + # insecure to match current n implementation + fetch="wget -q -O- --no-check-certificate" + fi + + local match='xxx' + if [[ "$1" = "lts" ]]; then + match='[^-]$' + elif [[ "$1" = "latest" ]]; then + match='.' + fi + + # Using awk rather than head so do not close pipe early on curl + # (Add display_compatible_file_field when n does similar check!) + ${fetch} "https://nodejs.org/dist/index.tab" \ + | tail -n +2 \ + | grep -E "${match}" \ + | awk "NR==1" \ + | cut -f -1 +}