diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..7d96b1b --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,43 @@ +name: Run e2e tests + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + pull_request: + +jobs: + test-extension: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ macos-12, ubuntu-22.04 ] + test_file_path: [ extensions/nns/e2e/tests/nns.bash, extensions/sns/e2e/tests/sns.bash ] + steps: + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + - name: Install brew + uses: Homebrew/actions/setup-homebrew@master + if: contains(matrix.os, 'macos-12') == false + - name: Install sponge and timeout + run: brew install coreutils sponge + - name: Install IC SDK (dfx) + run: DFX_VERSION="0.14.2-beta.1" sh -ci "$(curl -sSL https://internetcomputer.org/install.sh)" + - name: run test + run: timeout 2400 e2e/bats/bin/bats ${{ matrix.test_file_path }} + + aggregate: + name: e2e:required + if: ${{ always() }} + needs: [test-extension] + runs-on: ubuntu-latest + steps: + - name: check e2e test result + if: ${{ needs.test-extension.result != 'success' }} + run: exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e34c340..a303493 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: matrix: # For these target platforms include: - - os: macos-11 + - os: macos-12 dist-args: --artifacts=local --target=aarch64-apple-darwin --target=x86_64-apple-darwin install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.5/cargo-dist-v0.0.5-installer.sh | sh - os: ubuntu-20.04 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..de921da --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "e2e/bats"] + path = e2e/bats + url = https://github.com/bats-core/bats-core.git +[submodule "e2e/bats-support"] + path = e2e/bats-support + url = https://github.com/bats-core/bats-support.git +[submodule "e2e/bats-assert"] + path = e2e/bats-assert + url = https://github.com/bats-core/bats-assert.git diff --git a/e2e/bats b/e2e/bats new file mode 160000 index 0000000..4417a96 --- /dev/null +++ b/e2e/bats @@ -0,0 +1 @@ +Subproject commit 4417a96cfc5e4afc493df94eeef8af4eec760163 diff --git a/e2e/bats-assert b/e2e/bats-assert new file mode 160000 index 0000000..44913ff --- /dev/null +++ b/e2e/bats-assert @@ -0,0 +1 @@ +Subproject commit 44913ffe6020d1561c4c4d1e26cda8e07a1f374f diff --git a/e2e/bats-support b/e2e/bats-support new file mode 160000 index 0000000..3c8fadc --- /dev/null +++ b/e2e/bats-support @@ -0,0 +1 @@ +Subproject commit 3c8fadc5097c9acfc96d836dced2bb598e48b009 diff --git a/e2e/get_ephemeral_port.py b/e2e/get_ephemeral_port.py new file mode 100644 index 0000000..dbbe334 --- /dev/null +++ b/e2e/get_ephemeral_port.py @@ -0,0 +1,5 @@ +import socket + +with socket.socket() as s: + s.bind(('', 0)) + print(s.getsockname()[1], end='') diff --git a/e2e/utils.sh b/e2e/utils.sh new file mode 100644 index 0000000..ab994c9 --- /dev/null +++ b/e2e/utils.sh @@ -0,0 +1,448 @@ +set -e + +GIT_ROOT_DIR=$(git rev-parse --show-toplevel) + +load "$GIT_ROOT_DIR"/e2e/bats-support/load +load "$GIT_ROOT_DIR"/e2e/bats-assert/load + +# Takes a name of the asset folder, and copy those files to the current project. +install_asset() { + ASSET_ROOT="$(dirname "$BATS_TEST_FILENAME")"/../assets/$1 + cp -R "$ASSET_ROOT"/* . + + # shellcheck source=/dev/null + if [ -f ./patch.bash ]; then source ./patch.bash; fi + if [ -f ./Cargo.toml ]; then cargo update; fi +} + +install_shared_asset() { + mkdir -p "$(dirname "$E2E_NETWORKS_JSON")" + + ASSET_ROOT="$(dirname "$BATS_TEST_FILENAME")"/../assets/$1 + cp -R "$ASSET_ROOT"/* "$(dirname "$E2E_NETWORKS_JSON")" +} + +standard_setup() { + # We want to work from a temporary directory, different for every test. + x=$(mktemp -d -t dfx-e2e-XXXXXXXX) + export E2E_TEMP_DIR="$x" + + cache_root="${E2E_CACHE_ROOT:-"$HOME/.e2e-cache-root"}" + + mkdir "$x/working-dir" + mkdir -p "$cache_root" + mkdir "$x/config-root" + mkdir "$x/home-dir" + + cd "$x/working-dir" || exit + + export HOME="$x/home-dir" + export DFX_CACHE_ROOT="$cache_root" + export DFX_CONFIG_ROOT="$x/config-root" + export RUST_BACKTRACE=1 + export MOCK_KEYRING_LOCATION="$HOME/mock_keyring.json" + + if [ "$(uname)" == "Darwin" ]; then + export E2E_SHARED_LOCAL_NETWORK_DATA_DIRECTORY="$HOME/Library/Application Support/org.dfinity.dfx/network/local" + elif [ "$(uname)" == "Linux" ]; then + export E2E_SHARED_LOCAL_NETWORK_DATA_DIRECTORY="$HOME/.local/share/dfx/network/local" + fi + export E2E_NETWORKS_JSON="$DFX_CONFIG_ROOT/.config/dfx/networks.json" + + dfx cache install +} + +standard_teardown() { + rm -rf "$E2E_TEMP_DIR" || rm -rf "$E2E_TEMP_DIR" +} + +dfx_new_frontend() { + local project_name=${1:-e2e_project} + dfx new "${project_name}" --frontend + test -d "${project_name}" + test -f "${project_name}"/dfx.json + cd "${project_name}" + + echo PWD: "$(pwd)" >&2 +} + +dfx_new() { + local project_name=${1:-e2e_project} + dfx new "${project_name}" --no-frontend + test -d "${project_name}" + test -f "${project_name}/dfx.json" + cd "${project_name}" + + echo PWD: "$(pwd)" >&2 +} + +dfx_new_rust() { + local project_name=${1:-e2e_project} + rustup default stable + rustup target add wasm32-unknown-unknown + dfx new "${project_name}" --type=rust --no-frontend + test -d "${project_name}" + test -f "${project_name}/dfx.json" + test -f "${project_name}/Cargo.toml" + test -f "${project_name}/Cargo.lock" + cd "${project_name}" + + echo PWD: "$(pwd)" >&2 +} + +dfx_patchelf() { + # Don't run this function during github actions + [ "$GITHUB_ACTIONS" ] && return 0 + + # Only run this function on Linux + (uname -a | grep Linux) || return 0 + + local CACHE_DIR LD_LINUX_SO BINARY IS_STATIC USE_LIB64 + + echo dfx = "$(which dfx)" + CACHE_DIR="$(dfx cache show)" + + # Both ldd and iconv are providedin glibc.bin package + LD_LINUX_SO=$(ldd "$(which iconv)"|grep ld-linux-x86|cut -d' ' -f3) + for binary in ic-starter icx-proxy replica; do + BINARY="${CACHE_DIR}/${binary}" + test -f "$BINARY" || continue + IS_STATIC=$(ldd "${BINARY}" | grep 'not a dynamic executable') + USE_LIB64=$(ldd "${BINARY}" | grep '/lib64/ld-linux-x86-64.so.2') + chmod +rw "${BINARY}" + test -n "$IS_STATIC" || test -z "$USE_LIB64" || patchelf --set-interpreter "${LD_LINUX_SO}" "${BINARY}" + done +} + +determine_network_directory() { + # not perfect: dfx.json can actually exist in a parent + if [ -f dfx.json ] && [ "$(jq .networks.local dfx.json)" != "null" ]; then + echo "found dfx.json with local network in $(pwd)" + data_dir="$(pwd)/.dfx/network/local" + wallets_json="$(pwd)/.dfx/local/wallets.json" + dfx_json="$(pwd)/dfx.json" + export E2E_NETWORK_DATA_DIRECTORY="$data_dir" + export E2E_NETWORK_WALLETS_JSON="$wallets_json" + export E2E_ROUTE_NETWORKS_JSON="$dfx_json" + else + echo "no dfx.json" + export E2E_NETWORK_DATA_DIRECTORY="$E2E_SHARED_LOCAL_NETWORK_DATA_DIRECTORY" + export E2E_NETWORK_WALLETS_JSON="$E2E_NETWORK_DATA_DIRECTORY/wallets.json" + export E2E_ROUTE_NETWORKS_JSON="$E2E_NETWORKS_JSON" + fi +} + +# Start the replica in the background. +dfx_start() { + local port dfx_config_root webserver_port + dfx_patchelf + + # Start on random port for parallel test execution + FRONTEND_HOST="127.0.0.1:0" + + determine_network_directory + if [ "$USE_IC_REF" ] + then + if [[ $# -eq 0 ]]; then + dfx start --emulator --background --host "$FRONTEND_HOST" 3>&- + else + batslib_decorate "no arguments to dfx start --emulator supported yet" + fail + fi + + test -f "$E2E_NETWORK_DATA_DIRECTORY/ic-ref.port" + port=$(cat "$E2E_NETWORK_DATA_DIRECTORY/ic-ref.port") + else + # Bats creates a FD 3 for test output, but child processes inherit it and Bats will + # wait for it to close. Because `dfx start` leaves child processes running, we need + # to close this pipe, otherwise Bats will wait indefinitely. + if [[ $# -eq 0 ]]; then + dfx start --background --host "$FRONTEND_HOST" --artificial-delay 100 3>&- # Start on random port for parallel test execution + else + dfx start --background --artificial-delay 100 "$@" 3>&- + fi + + dfx_config_root="$E2E_NETWORK_DATA_DIRECTORY/replica-configuration" + printf "Configuration Root for DFX: %s\n" "${dfx_config_root}" + test -f "${dfx_config_root}/replica-1.port" + port=$(cat "${dfx_config_root}/replica-1.port") + fi + + webserver_port=$(cat "$E2E_NETWORK_DATA_DIRECTORY/webserver-port") + + printf "Replica Configured Port: %s\n" "${port}" + printf "Webserver Configured Port: %s\n" "${webserver_port}" + + timeout 5 sh -c \ + "until nc -z localhost ${port}; do echo waiting for replica; sleep 1; done" \ + || (echo "could not connect to replica on port ${port}" && exit 1) +} + +# Tries to start dfx on the default port, repeating until it succeeds or times out. +# +# Motivation: dfx nns install works only on port 8080, as URLs are compiled into the wasms. This means that multiple +# tests MAY compete for the same port. +# - It may be possible in future for the wasms to detect their own URL and recompute signatures accordingly, +# however until such a time, we have this restriction. +# - It may also be that ic-nns-install, if used on a non-standard port, installs only the core canisters not the UI. +# - However until we have implemented good solutions, all tests on ic-nns-install must run on port 8080. +dfx_start_for_nns_install() { + # TODO: When nns-dapp supports dynamic ports, this wait can be removed. + timeout 300 sh -c \ + "until dfx start --clean --background --host 127.0.0.1:8080 --verbose ; do echo waiting for port 8080 to become free; sleep 3; done" \ + || (echo "could not connect to replica on port 8080" && exit 1) + # TODO: figure out how to plug bats' "run" into above statement, + # so that below asserts will work as expected + # assert_success + # assert_output --partial "subnet type: System" + # assert_output --partial "bind address: 127.0.0.1:8080" +} + +wait_until_replica_healthy() { + echo "waiting for replica to become healthy" + dfx ping --wait-healthy + echo "replica became healthy" +} + +# Start the replica in the background. +dfx_replica() { + local replica_port dfx_config_root + dfx_patchelf + determine_network_directory + if [ "$USE_IC_REF" ] + then + # Bats creates a FD 3 for test output, but child processes inherit it and Bats will + # wait for it to close. Because `dfx start` leaves child processes running, we need + # to close this pipe, otherwise Bats will wait indefinitely. + dfx replica --emulator --port 0 "$@" 3>&- & + export DFX_REPLICA_PID=$! + + timeout 60 sh -c \ + "until test -s \"$E2E_NETWORK_DATA_DIRECTORY/ic-ref.port\"; do echo waiting for ic-ref port; sleep 1; done" \ + || (echo "replica did not write to \"$E2E_NETWORK_DATA_DIRECTORY/ic-ref.port\" file" && exit 1) + + test -f "$E2E_NETWORK_DATA_DIRECTORY/ic-ref.port" + replica_port=$(cat "$E2E_NETWORK_DATA_DIRECTORY/ic-ref.port") + + else + # Bats creates a FD 3 for test output, but child processes inherit it and Bats will + # wait for it to close. Because `dfx start` leaves child processes running, we need + # to close this pipe, otherwise Bats will wait indefinitely. + dfx replica --port 0 "$@" 3>&- & + export DFX_REPLICA_PID=$! + + timeout 60 sh -c \ + "until test -s \"$E2E_NETWORK_DATA_DIRECTORY/replica-configuration/replica-1.port\"; do echo waiting for replica port; sleep 1; done" \ + || (echo "replica did not write to port file" && exit 1) + + dfx_config_root="$E2E_NETWORK_DATA_DIRECTORY/replica-configuration" + test -f "${dfx_config_root}/replica-1.port" + replica_port=$(cat "${dfx_config_root}/replica-1.port") + + fi + + printf "Replica Configured Port: %s\n" "${replica_port}" + + timeout 5 sh -c \ + "until nc -z localhost ${replica_port}; do echo waiting for replica; sleep 1; done" \ + || (echo "could not connect to replica on port ${replica_port}" && exit 1) + + # ping the replica directly, because the bootstrap (that launches icx-proxy, which dfx ping usually connects to) + # is not running yet + dfx ping --wait-healthy "http://127.0.0.1:${replica_port}" +} + +dfx_bootstrap() { + # This only works because we use the network by name + # (implicitly: --network local) + # If we passed --network http://127.0.0.1:${replica_port} + # we would get errors like this: + # "Cannot find canister ryjl3-tyaaa-aaaaa-aaaba-cai for network http___127_0_0_1_54084" + dfx bootstrap --port 0 3>&- & + export DFX_BOOTSTRAP_PID=$! + + timeout 5 sh -c \ + "until nc -z localhost \$(cat \"$E2E_NETWORK_DATA_DIRECTORY/webserver-port\"); do echo waiting for webserver; sleep 1; done" \ + || (echo "could not connect to webserver on port $(get_webserver_port)" && exit 1) + + wait_until_replica_healthy + + webserver_port=$(cat "$E2E_NETWORK_DATA_DIRECTORY/webserver-port") + printf "Webserver Configured Port: %s\n", "${webserver_port}" +} + +# Stop the `dfx replica` process that is running in the background. +stop_dfx_replica() { + [ "$DFX_REPLICA_PID" ] && kill -TERM "$DFX_REPLICA_PID" + unset DFX_REPLICA_PID +} + +# Stop the `dfx bootstrap` process that is running in the background +stop_dfx_bootstrap() { + [ "$DFX_BOOTSTRAP_PID" ] && kill -TERM "$DFX_BOOTSTRAP_PID" + unset DFX_BOOTSTRAP_PID +} + +# Stop the replica and verify it is very very stopped. +dfx_stop() { + # to help tell if other icx-proxy processes are from this test: + echo "pwd: $(pwd)" + # A suspicion: "address already is use" errors are due to an extra icx-proxy process. + echo "icx-proxy processes:" + pgrep -l icx-proxy || echo "no ps/grep/icx-proxy output" + + dfx stop + local dfx_root=.dfx/ + rm -rf $dfx_root + + # Verify that processes are killed. + assert_no_dfx_start_or_replica_processes +} + +dfx_set_wallet() { + export WALLET_CANISTER_ID + WALLET_CANISTER_ID=$(dfx identity get-wallet) + assert_command dfx identity set-wallet "${WALLET_CANISTER_ID}" --force --network actuallylocal + assert_match 'Wallet set successfully.' +} + +setup_actuallylocal_project_network() { + webserver_port=$(get_webserver_port) + # [ ! -f "$E2E_ROUTE_NETWORKS_JSON" ] && echo "{}" >"$E2E_ROUTE_NETWORKS_JSON" + jq '.networks.actuallylocal.providers=["http://127.0.0.1:'"$webserver_port"'"]' dfx.json | sponge dfx.json +} + +setup_actuallylocal_shared_network() { + webserver_port=$(get_webserver_port) + [ ! -f "$E2E_NETWORKS_JSON" ] && echo "{}" >"$E2E_NETWORKS_JSON" + jq '.actuallylocal.providers=["http://127.0.0.1:'"$webserver_port"'"]' "$E2E_NETWORKS_JSON" | sponge "$E2E_NETWORKS_JSON" +} + +setup_local_shared_network() { + local replica_port + if [ "$USE_IC_REF" ] + then + replica_port=$(get_ic_ref_port) + else + replica_port=$(get_replica_port) + fi + + [ ! -f "$E2E_NETWORKS_JSON" ] && echo "{}" >"$E2E_NETWORKS_JSON" + + jq ".local.bind=\"127.0.0.1:${replica_port}\"" "$E2E_NETWORKS_JSON" | sponge "$E2E_NETWORKS_JSON" +} + +use_wallet_wasm() { + # shellcheck disable=SC2154 + export DFX_WALLET_WASM="${archive}/wallet/$1/wallet.wasm" +} + +use_asset_wasm() { + # shellcheck disable=SC2154 + export DFX_ASSETS_WASM="${archive}/frontend/$1/assetstorage.wasm.gz" +} + +wallet_sha() { + shasum -a 256 "${archive}/wallet/$1/wallet.wasm" | awk '{ print $1 }' +} + +use_default_wallet_wasm() { + unset DFX_WALLET_WASM +} + +use_default_asset_wasm() { + unset DFX_ASSETS_WASM +} + +get_webserver_port() { + dfx info webserver-port +} +overwrite_webserver_port() { + echo "$1" >"$E2E_NETWORK_DATA_DIRECTORY/webserver-port" +} + +get_replica_pid() { + cat "$E2E_NETWORK_DATA_DIRECTORY/replica-configuration/replica-pid" +} + +get_ic_ref_port() { + cat "$E2E_NETWORK_DATA_DIRECTORY/ic-ref.port" + +} +get_replica_port() { + cat "$E2E_NETWORK_DATA_DIRECTORY/replica-configuration/replica-1.port" +} + +get_btc_adapter_pid() { + cat "$E2E_NETWORK_DATA_DIRECTORY/ic-btc-adapter-pid" +} + +get_canister_http_adapter_pid() { + cat "$E2E_NETWORK_DATA_DIRECTORY/ic-canister-http-adapter-pid" +} + +get_icx_proxy_pid() { + cat "$E2E_NETWORK_DATA_DIRECTORY/icx-proxy-pid" +} + +create_networks_json() { + mkdir -p "$(dirname "$E2E_NETWORKS_JSON")" + [ ! -f "$E2E_NETWORKS_JSON" ] && echo "{}" >"$E2E_NETWORKS_JSON" +} + +define_project_network() { + jq .networks.local.bind=\"127.0.0.1:8000\" dfx.json | sponge dfx.json +} + +use_test_specific_cache_root() { + # Use this when a test depends on the initial state of the cache being empty, + # or if the test corrupts the cache in some way. + # The effect is to ignore the E2E_CACHE_ROOT environment variable, if set. + export DFX_CACHE_ROOT="$E2E_TEMP_DIR/cache-root" + mkdir -p "$DFX_CACHE_ROOT" +} + +start_webserver() { + local port script_dir + script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + port=$(python3 "$script_dir/get_ephemeral_port.py") + export E2E_WEB_SERVER_PORT="$port" + + python3 -m http.server "$E2E_WEB_SERVER_PORT" "$@" & + export E2E_WEB_SERVER_PID=$! + + while ! nc -z localhost "$E2E_WEB_SERVER_PORT"; do + sleep 1 + done +} + +stop_webserver() { + if [ "$E2E_WEB_SERVER_PID" ]; then + kill "$E2E_WEB_SERVER_PID" + fi +} + +# Asserts that the contents of two files are equal. +# Arguments: +# $1 - The name of the file containing the expected value. +# $2 - The name of the file containing the actual value. +assert_files_eq() { + expected="$(cat "$1")" + actual="$(cat "$2")" + + if [[ ! "$actual" == "$expected" ]]; then + diff "$1" "$2" \ + | batslib_decorate "contents of $1 do not match contents of $2" \ + | fail + fi +} + +# Asserts that `dfx start` and `replica` are no longer running +assert_no_dfx_start_or_replica_processes() { + ! ( pgrep "dfx start" ) + if [ -e .dfx/replica-configuration/replica-pid ]; + then + ! ( kill -0 "$(< .dfx/replica-configuration/replica-pid)" 2>/dev/null ) + fi +} + diff --git a/extensions/nns/e2e/assets/project-import/project-directory/canister_ids.json b/extensions/nns/e2e/assets/project-import/project-directory/canister_ids.json new file mode 100644 index 0000000..4b4f931 --- /dev/null +++ b/extensions/nns/e2e/assets/project-import/project-directory/canister_ids.json @@ -0,0 +1,38 @@ +{ + "sibling": { + "mainnet": "rwlgt-iiaaa-aaaaa-aaaaa-cai", + "small01": "rwlgt-iiaaa-aaaaa-aaaaa-cai" + }, + "normal-canister": { + "mainnet": "rrkah-fqaaa-aaaaa-aaaaq-cai", + "small01": "rrkah-fqaaa-aaaaa-aaaaq-cai" + }, + "ledger": { + "mainnet": "ryjl3-tyaaa-aaaaa-aaaba-cai", + "small01": "ryjl3-tyaaa-aaaaa-aaaba-cai" + }, + "root": { + "mainnet": "r7inp-6aaaa-aaaaa-aaabq-cai", + "small01": "r7inp-6aaaa-aaaaa-aaabq-cai" + }, + "cycles-minting": { + "mainnet": "rkp4c-7iaaa-aaaaa-aaaca-cai", + "small01": "rkp4c-7iaaa-aaaaa-aaaca-cai" + }, + "lifeline": { + "mainnet": "rno2w-sqaaa-aaaaa-aaacq-cai", + "small01": "rno2w-sqaaa-aaaaa-aaacq-cai" + }, + "genesis-token": { + "mainnet": "renrk-eyaaa-aaaaa-aaada-cai", + "small01": "renrk-eyaaa-aaaaa-aaada-cai" + }, + "identity": { + "mainnet": "rdmx6-jaaaa-aaaaa-aaadq-cai", + "small01": "rdmx6-jaaaa-aaaaa-aaadq-cai" + }, + "nns-ui": { + "mainnet": "qoctq-giaaa-aaaaa-aaaea-cai", + "small01": "qoctq-giaaa-aaaaa-aaaea-cai" + } +} \ No newline at end of file diff --git a/extensions/nns/e2e/assets/project-import/project-directory/dfx.json b/extensions/nns/e2e/assets/project-import/project-directory/dfx.json new file mode 100644 index 0000000..30fc95e --- /dev/null +++ b/extensions/nns/e2e/assets/project-import/project-directory/dfx.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "canisters": { + "normal-canister": { + "type": "custom", + "candid": "normal-canister-directory/some-subdirectory/the-candid-filename.did", + "wasm": "../target/wasm32-unknown-unknown/release/governance-canister.wasm", + "build": "cargo build --target wasm32-unknown-unknown --release -p ic-nns-governance" + }, + "sibling": { + "type": "custom", + "candid": "../sibling-project/canister/canister/the-sibling-candid-definition.did", + "wasm": "../target/wasm32-unknown-unknown/release/registry-canister.wasm", + "build": "cargo build --target wasm32-unknown-unknown --release -p registry-canister" + } + }, + "networks": { + "mainnet": { + "providers": [ + "https://icp0.io" + ], + "type": "persistent" + }, + "small01": { + "providers": [ + "http://[2a00:fb01:400:42:5000:3dff:feca:9312]:8080" + ], + "type": "persistent" + }, + "local": { + "bind": "127.0.0.1:8080" + } + } +} \ No newline at end of file diff --git a/extensions/nns/e2e/assets/project-import/project-directory/normal-canister-directory/some-subdirectory/the-candid-filename.did b/extensions/nns/e2e/assets/project-import/project-directory/normal-canister-directory/some-subdirectory/the-candid-filename.did new file mode 100644 index 0000000..6dd2813 --- /dev/null +++ b/extensions/nns/e2e/assets/project-import/project-directory/normal-canister-directory/some-subdirectory/the-candid-filename.did @@ -0,0 +1,14 @@ +type AccountIdentifier = record { hash : vec nat8 }; +type Action = variant { + RegisterKnownNeuron : KnownNeuron; + ManageNeuron : ManageNeuron; + ExecuteNnsFunction : ExecuteNnsFunction; + RewardNodeProvider : RewardNodeProvider; + SetSnsTokenSwapOpenTimeWindow : SetSnsTokenSwapOpenTimeWindow; + SetDefaultFollowees : SetDefaultFollowees; + RewardNodeProviders : RewardNodeProviders; + ManageNetworkEconomics : NetworkEconomics; + ApproveGenesisKyc : ApproveGenesisKyc; + AddOrRemoveNodeProvider : AddOrRemoveNodeProvider; + Motion : Motion; +}; diff --git a/extensions/nns/e2e/assets/project-import/sibling-project/canister/canister/the-sibling-candid-definition.did b/extensions/nns/e2e/assets/project-import/sibling-project/canister/canister/the-sibling-candid-definition.did new file mode 100644 index 0000000..6a8f561 --- /dev/null +++ b/extensions/nns/e2e/assets/project-import/sibling-project/canister/canister/the-sibling-candid-definition.did @@ -0,0 +1,14 @@ +type AddFirewallRulesPayload = record { + expected_hash : text; + scope : FirewallRulesScope; + positions : vec int32; + rules : vec FirewallRule; +}; +type AddNodeOperatorPayload = record { + ipv6 : opt text; + node_operator_principal_id : opt principal; + node_allowance : nat64; + rewardable_nodes : vec record { text; nat32 }; + node_provider_principal_id : opt principal; + dc_id : text; +}; diff --git a/extensions/nns/e2e/assets/subnet_type/shared_network_settings/system/networks.json b/extensions/nns/e2e/assets/subnet_type/shared_network_settings/system/networks.json new file mode 100644 index 0000000..2d78ba0 --- /dev/null +++ b/extensions/nns/e2e/assets/subnet_type/shared_network_settings/system/networks.json @@ -0,0 +1,7 @@ +{ + "local": { + "replica": { + "subnet_type": "system" + } + } +} \ No newline at end of file diff --git a/extensions/nns/e2e/tests-dfx/nns.bash b/extensions/nns/e2e/tests/nns.bash similarity index 80% rename from extensions/nns/e2e/tests-dfx/nns.bash rename to extensions/nns/e2e/tests/nns.bash index f5c9465..6515d5d 100755 --- a/extensions/nns/e2e/tests-dfx/nns.bash +++ b/extensions/nns/e2e/tests/nns.bash @@ -1,10 +1,16 @@ #!/usr/bin/env bats -load ../utils/_ +GIT_ROOT_DIR=$(git rev-parse --show-toplevel) + +load "$GIT_ROOT_DIR"/e2e/utils.sh + +assets="$(dirname "$BATS_TEST_FILENAME")"/../assets setup() { standard_setup + dfx extension install nns + dfx_new } @@ -20,32 +26,37 @@ teardown() { dfx cache install # it panics, but still shows help - assert_command_fail "$(dfx cache show)/ic-nns-init" --help - assert_match "thread 'main' panicked at 'Illegal arguments:" - assert_match "ic-nns-init \[OPTIONS\]" - assert_match "-h, --help.*Print help information" - assert_match '--version.*Print version information' + run "$(dfx cache show)/ic-nns-init" --help + assert_failure + assert_output --partial "thread 'main' panicked at 'Illegal arguments:" + assert_output --partial "ic-nns-init [OPTIONS]" + assert_output --regexp "-h, --help.*Print help information" + assert_output --regexp '--version.*Print version information' # --version fails too - assert_command_fail "$(dfx cache show)/ic-nns-init" --version + run "$(dfx cache show)/ic-nns-init" --version + assert_failure } @test "ic-admin binary exists and is executable" { dfx cache install - assert_command "$(dfx cache show)/ic-admin" --help - assert_match "Common command-line options for \`ic-admin\`" + run "$(dfx cache show)/ic-admin" --help + assert_success + assert_output --partial 'Common command-line options for `ic-admin`' } @test "sns binary exists and is executable" { dfx cache install - assert_command_fail "$(dfx cache show)/sns" --help - assert_match "Initialize, deploy and interact with an SNS." + run "$(dfx cache show)/sns" --help + assert_failure + assert_output --partial "Initialize, deploy and interact with an SNS." } @test "dfx nns install command exists" { - assert_command dfx nns install --help + run dfx nns install --help + assert_success } @@ -95,6 +106,7 @@ assert_nns_canister_id_matches() { } @test "dfx nns install runs" { + echo Setting up... install_shared_asset subnet_type/shared_network_settings/system dfx_start_for_nns_install @@ -124,7 +136,7 @@ assert_nns_canister_id_matches() { wasm_matches nns-ledger ledger-canister_notify-method.wasm wasm_matches nns-root root-canister.wasm wasm_matches nns-cycles-minting cycles-minting-canister.wasm - wasm_matches nns-lifeline lifeline.wasm + wasm_matches nns-lifeline lifeline_canister.wasm wasm_matches nns-genesis-token genesis-token-canister.wasm wasm_matches nns-sns-wasm sns-wasm-canister.wasm wasm_matches internet_identity internet_identity_dev.wasm @@ -132,8 +144,9 @@ assert_nns_canister_id_matches() { echo " Accounts should have funds..." account_has_funds() { - assert_command dfx ledger balance "$1" - assert_eq "1000000000.00000000 ICP" + run dfx ledger balance "$1" + assert_success + assert_output "1000000000.00000000 ICP" } SECP256K1_ACCOUNT_ID="2b8fbde99de881f695f279d2a892b1137bfe81a42d7694e064b1be58701e1138" ED25519_ACCOUNT_ID="5b315d2f6702cb3a27d826161797d7b2c2e131cd312aece51d4d5574d1247087" @@ -143,8 +156,9 @@ assert_nns_canister_id_matches() { echo " The secp256k1 account can be controlled from the command line" install_asset nns dfx identity import --force --disable-encryption ident-1 ident-1/identity.pem - assert_command dfx ledger account-id --identity ident-1 - assert_eq "$SECP256K1_ACCOUNT_ID" + run dfx ledger account-id --identity ident-1 + assert_success + assert_output "$SECP256K1_ACCOUNT_ID" echo Stopping dfx... dfx stop @@ -158,18 +172,21 @@ test_project_import() { jq . dfx.json - assert_command jq -r '.canisters."pfx-normal-canister".candid' dfx.json - assert_eq "candid/pfx-normal-canister.did" + run jq -r '.canisters."pfx-normal-canister".candid' dfx.json + assert_success + assert_output "candid/pfx-normal-canister.did" # shellcheck disable=SC2154 assert_files_eq \ "${assets}/project-import/project-directory/normal-canister-directory/some-subdirectory/the-candid-filename.did" \ "candid/pfx-normal-canister.did" - assert_command jq -r '.canisters."pfx-normal-canister".remote.id.ic' dfx.json - assert_eq "rrkah-fqaaa-aaaaa-aaaaq-cai" + run jq -r '.canisters."pfx-normal-canister".remote.id.ic' dfx.json + assert_success + assert_output "rrkah-fqaaa-aaaaa-aaaaq-cai" - assert_command jq -r '.canisters."pfx-sibling".candid' dfx.json - assert_eq "candid/pfx-sibling.did" + run jq -r '.canisters."pfx-sibling".candid' dfx.json + assert_success + assert_output "candid/pfx-sibling.did" assert_files_eq \ "${assets}/project-import/sibling-project/canister/canister/the-sibling-candid-definition.did" \ "candid/pfx-sibling.did" @@ -193,14 +210,16 @@ test_project_import_specific_canister() { jq . dfx.json - assert_command jq -r '.canisters."normal-canister".candid' dfx.json - assert_eq "candid/normal-canister.did" + run jq -r '.canisters."normal-canister".candid' dfx.json + assert_success + assert_output "candid/normal-canister.did" assert_files_eq \ "${assets}/project-import/project-directory/normal-canister-directory/some-subdirectory/the-candid-filename.did" \ "candid/normal-canister.did" - assert_command jq -r '.canisters.sibling.candid' dfx.json - assert_eq "null" + run jq -r '.canisters.sibling.candid' dfx.json + assert_success + assert_output "null" } @test "dfx project import specific canister" { @@ -232,3 +251,4 @@ test_project_import_specific_canister() { dfx beta project import "http://localhost:$E2E_WEB_SERVER_PORT/project-directory/dfx.json" --all } + diff --git a/extensions/nns/src/commands/import.rs b/extensions/nns/src/commands/import.rs index e878257..931ff19 100644 --- a/extensions/nns/src/commands/import.rs +++ b/extensions/nns/src/commands/import.rs @@ -28,7 +28,7 @@ pub struct ImportOpts { pub async fn exec(opts: ImportOpts, dfx_cache_path: &Path) -> anyhow::Result<()> { let config = Config::from_current_dir()?; if config.is_none() { - anyhow::bail!("No config file found. Please run `dfx config create` first."); + anyhow::bail!(crate::errors::DFXJSON_NOT_FOUND); } let mut config = config.unwrap().clone(); let logger = new_logger(); diff --git a/extensions/nns/src/commands/install.rs b/extensions/nns/src/commands/install.rs index a984e01..c7d3e44 100644 --- a/extensions/nns/src/commands/install.rs +++ b/extensions/nns/src/commands/install.rs @@ -53,7 +53,7 @@ pub async fn exec(opts: InstallOpts, dfx_cache_path: &Path) -> anyhow::Result<() let config = Config::from_current_dir()?; if config.is_none() { - anyhow::bail!("No config file found. Please run `dfx config create` first."); + anyhow::bail!(crate::errors::DFXJSON_NOT_FOUND); } let network_descriptor = create_network_descriptor( Some(Arc::new(config.unwrap())), diff --git a/extensions/nns/src/errors.rs b/extensions/nns/src/errors.rs new file mode 100644 index 0000000..4db919d --- /dev/null +++ b/extensions/nns/src/errors.rs @@ -0,0 +1 @@ +pub static DFXJSON_NOT_FOUND: &str = "Cannot find dfx configuration file in the current working directory. Did you forget to create one?"; diff --git a/extensions/nns/src/main.rs b/extensions/nns/src/main.rs index 73d778f..8f6dcdc 100644 --- a/extensions/nns/src/main.rs +++ b/extensions/nns/src/main.rs @@ -7,6 +7,7 @@ use clap::Parser; use tokio::runtime::Runtime; mod commands; +mod errors; mod install_nns; mod nns_types; diff --git a/extensions/sns/e2e/assets/subnet_type/shared_network_settings/system/networks.json b/extensions/sns/e2e/assets/subnet_type/shared_network_settings/system/networks.json new file mode 100644 index 0000000..2d78ba0 --- /dev/null +++ b/extensions/sns/e2e/assets/subnet_type/shared_network_settings/system/networks.json @@ -0,0 +1,7 @@ +{ + "local": { + "replica": { + "subnet_type": "system" + } + } +} \ No newline at end of file diff --git a/extensions/sns/e2e/tests-dfx/sns.bash b/extensions/sns/e2e/tests/sns.bash similarity index 59% rename from extensions/sns/e2e/tests-dfx/sns.bash rename to extensions/sns/e2e/tests/sns.bash index 60b05cb..f004a1c 100755 --- a/extensions/sns/e2e/tests-dfx/sns.bash +++ b/extensions/sns/e2e/tests/sns.bash @@ -1,10 +1,13 @@ #!/usr/bin/env bats -load ../utils/_ +GIT_ROOT_DIR=$(git rev-parse --show-toplevel) + +load "$GIT_ROOT_DIR"/e2e/utils.sh setup() { standard_setup + dfx extension install sns } teardown() { @@ -17,17 +20,19 @@ teardown() { SNS_CONFIG_FILE_NAME="sns.yml" @test "sns config create and validate fail outside of a project" { - assert_command_fail dfx sns config create - assert_match 'Cannot find dfx configuration file in the current working directory' - - assert_command_fail dfx sns config validate - assert_match 'Cannot find dfx configuration file in the current working directory' + run dfx sns config create + assert_failure + assert_output --partial 'Error: Cannot find dfx configuration file in the current working directory. Did you forget to create one?' + run dfx sns config validate + assert_failure + assert_output --partial 'Error: Cannot find dfx configuration file in the current working directory. Did you forget to create one?' } @test "sns config create creates a default configuration" { dfx_new - assert_command dfx sns config create - assert_match "Created SNS configuration at: .*/sns.yml" + run dfx sns config create + assert_success + assert_output --regexp "Created SNS configuration at: .*/sns.yml" : "Check that the file exists..." test -e sns.yml } @@ -35,16 +40,19 @@ SNS_CONFIG_FILE_NAME="sns.yml" @test "sns config validate approves a valid configuration" { dfx_new install_asset sns/valid - assert_command dfx sns config validate - assert_match 'SNS config file is valid' + run dfx sns config validate + assert_success + assert_output --partial 'SNS config file is valid' } @test "sns config validate identifies a missing key" { dfx_new install_asset sns/valid grep -v token_name "${SNS_CONFIG_FILE_NAME}" | sponge "$SNS_CONFIG_FILE_NAME" - assert_command_fail dfx sns config validate - assert_match token.name + run dfx sns config validate + assert_failure + assert_output --partial "Error: token-name must be specified" + } @test "sns deploy exists" { @@ -53,10 +61,12 @@ SNS_CONFIG_FILE_NAME="sns.yml" @test "sns deploy fails without config file" { dfx_new + dfx extension install nns dfx nns import rm -f sns.yml # Is not expected to be present anyway - assert_command_fail dfx sns deploy - assert_match "Error encountered when generating the SnsInitPayload: Couldn't open initial parameters file" + run dfx sns deploy + assert_failure + assert_output --partial "Error encountered when generating the SnsInitPayload: Couldn't open initial parameters file" } @test "sns deploy succeeds" { @@ -64,6 +74,7 @@ SNS_CONFIG_FILE_NAME="sns.yml" install_shared_asset subnet_type/shared_network_settings/system dfx start --clean --background --host 127.0.0.1:8080 sleep 1 + dfx extension install nns dfx nns install dfx nns import dfx sns import diff --git a/extensions/sns/src/commands/config/create.rs b/extensions/sns/src/commands/config/create.rs index ce6d405..e89d82d 100644 --- a/extensions/sns/src/commands/config/create.rs +++ b/extensions/sns/src/commands/config/create.rs @@ -15,7 +15,7 @@ pub fn exec(_opts: CreateOpts, dfx_cache_path: &Path) -> anyhow::Result<()> { let sns_config_path = if let Some(config) = Config::from_current_dir()? { config.get_project_root().join(CONFIG_FILE_NAME) } else { - anyhow::bail!("No config file found. Please run `dfx config create` first."); + anyhow::bail!(crate::errors::DFXJSON_NOT_FOUND); }; create_config(dfx_cache_path, &sns_config_path)?; diff --git a/extensions/sns/src/commands/config/validate.rs b/extensions/sns/src/commands/config/validate.rs index 4b14d16..a31a1fb 100644 --- a/extensions/sns/src/commands/config/validate.rs +++ b/extensions/sns/src/commands/config/validate.rs @@ -15,7 +15,7 @@ pub fn exec(_opts: ValidateOpts, dfx_cache_path: &Path) -> anyhow::Result<()> { let sns_config_path = if let Some(config) = Config::from_current_dir()? { config.get_project_root().join(CONFIG_FILE_NAME) } else { - anyhow::bail!("No config file found. Please run `dfx config create` first."); + anyhow::bail!(crate::errors::DFXJSON_NOT_FOUND); }; validate_config(dfx_cache_path, &sns_config_path).map(|stdout| println!("{}", stdout)) } diff --git a/extensions/sns/src/commands/deploy.rs b/extensions/sns/src/commands/deploy.rs index d2b2a3a..82a15c2 100644 --- a/extensions/sns/src/commands/deploy.rs +++ b/extensions/sns/src/commands/deploy.rs @@ -20,7 +20,7 @@ pub fn exec(_opts: DeployOpts, dfx_cache_path: &Path) -> anyhow::Result<()> { let sns_config_path = if let Some(config) = Config::from_current_dir()? { config.get_project_root().join(CONFIG_FILE_NAME) } else { - anyhow::bail!("No config file found. Please run `dfx config create` first."); + anyhow::bail!(crate::errors::DFXJSON_NOT_FOUND); }; println!("{}", deploy_sns(dfx_cache_path, &sns_config_path)?); diff --git a/extensions/sns/src/commands/import.rs b/extensions/sns/src/commands/import.rs index 2398502..85a2854 100644 --- a/extensions/sns/src/commands/import.rs +++ b/extensions/sns/src/commands/import.rs @@ -26,7 +26,7 @@ pub struct SnsImportOpts { pub fn exec(opts: SnsImportOpts, dfx_cache_path: &Path) -> anyhow::Result<()> { let config = Config::from_current_dir()?; if config.is_none() { - anyhow::bail!("No config file found. Please run `dfx config create` first."); + anyhow::bail!(crate::errors::DFXJSON_NOT_FOUND); } let mut config = config.unwrap(); let logger = new_logger(); diff --git a/extensions/sns/src/errors.rs b/extensions/sns/src/errors.rs new file mode 100644 index 0000000..4db919d --- /dev/null +++ b/extensions/sns/src/errors.rs @@ -0,0 +1 @@ +pub static DFXJSON_NOT_FOUND: &str = "Cannot find dfx configuration file in the current working directory. Did you forget to create one?"; diff --git a/extensions/sns/src/main.rs b/extensions/sns/src/main.rs index 1b44373..d011989 100644 --- a/extensions/sns/src/main.rs +++ b/extensions/sns/src/main.rs @@ -3,6 +3,7 @@ pub mod commands; pub mod create_config; pub mod deploy; +mod errors; pub mod validate_config; /// The default location of an SNS configuration file.