From 88b5e9dbad0df34c692d153ff27e7a939553d16f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 9 Jul 2020 15:11:42 -0300 Subject: [PATCH 01/88] Add configuration support ApiTestConfig works like :common Config, but specific to this subproject. BisqAppConfig is an enumeration specifying Bisq desktop and daemon options for running seednode, arbnode, bob & alices nodes in regtest / full-dao mode. --- .../bisq/apitest/config/ApiTestConfig.java | 416 ++++++++++++++++++ .../bisq/apitest/config/BisqAppConfig.java | 122 +++++ .../apitest/config/CompositeOptionSet.java | 61 +++ 3 files changed, 599 insertions(+) create mode 100644 apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java create mode 100644 apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java create mode 100644 apitest/src/main/java/bisq/apitest/config/CompositeOptionSet.java diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java new file mode 100644 index 00000000000..9d2bfea4dea --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -0,0 +1,416 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.config; + +import bisq.common.storage.FileUtil; + +import joptsimple.AbstractOptionSpec; +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.HelpFormatter; +import joptsimple.OptionException; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermissions; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.UncheckedIOException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import lombok.extern.slf4j.Slf4j; + +import static java.lang.String.format; +import static java.lang.System.getProperty; +import static java.lang.System.getenv; + +@Slf4j +public class ApiTestConfig { + + // Option name constants + static final String HELP = "help"; + static final String BASH_PATH = "bashPath"; + static final String BERKELEYDB_LIB_PATH = "berkeleyDbLibPath"; + static final String BITCOIN_PATH = "bitcoinPath"; + static final String BITCOIN_RPC_PORT = "bitcoinRpcPort"; + static final String BITCOIN_RPC_USER = "bitcoinRpcUser"; + static final String BITCOIN_RPC_PASSWORD = "bitcoinRpcPassword"; + static final String BITCOIN_REGTEST_HOST = "bitcoinRegtestHost"; + static final String CONFIG_FILE = "configFile"; + static final String ROOT_APP_DATA_DIR = "rootAppDataDir"; + static final String API_PASSWORD = "apiPassword"; + static final String RUN_SUBPROJECT_JARS = "runSubprojectJars"; + static final String RUN_ARB_NODE_AS_DESKTOP = "runArbNodeAsDesktop"; + static final String RUN_ALICE_NODE_AS_DESKTOP = "runAliceNodeAsDesktop"; + static final String RUN_BOB_NODE_AS_DESKTOP = "runBobNodeAsDesktop"; + static final String SKIP_TESTS = "skipTests"; + static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests"; + static final String NUM_SETUP_TASKS = "numSetupTasks"; + + // Default values for certain options + static final String DEFAULT_CONFIG_FILE_NAME = "apitest.properties"; + + // Static fields that provide access to Config properties in locations where injecting + // a Config instance is not feasible. + public static String BASH_PATH_VALUE; + + public final File defaultConfigFile; + + // Options supported only at the command line, not within a config file. + public final boolean helpRequested; + public final File configFile; + + // Options supported at the command line and a config file. + public final File rootAppDataDir; + public final String bashPath; + public final String berkeleyDbLibPath; + public final String bitcoinPath; + public final String bitcoinRegtestHost; + public final int bitcoinRpcPort; + public final String bitcoinRpcUser; + public final String bitcoinRpcPassword; + // Daemon instances can use same gRPC password, but each needs a different apiPort. + public final String apiPassword; + public final boolean runSubprojectJars; + public final boolean runArbNodeAsDesktop; + public final boolean runAliceNodeAsDesktop; + public final boolean runBobNodeAsDesktop; + public final boolean skipTests; + public final boolean shutdownAfterTests; + public final int numSetupTasks; + + // Immutable system configurations. + public final String bitcoinDatadir; + public final String userDir; + + // The parser that will be used to parse both cmd line and config file options + private final OptionParser parser = new OptionParser(); + + public ApiTestConfig(String... args) { + this.defaultConfigFile = absoluteConfigFile( + Paths.get("apitest", "build", "resources", "main").toFile().getAbsolutePath(), + DEFAULT_CONFIG_FILE_NAME); + this.bitcoinDatadir = Paths.get("apitest", "build", "resources", "main", "Bitcoin-regtest") + .toFile().getAbsolutePath(); + this.userDir = getProperty("user.dir"); + + AbstractOptionSpec helpOpt = + parser.accepts(HELP, "Print this help text") + .forHelp(); + + ArgumentAcceptingOptionSpec configFileOpt = + parser.accepts(CONFIG_FILE, format("Specify configuration file. " + + "Relative paths will be prefixed by %s location.", userDir)) + .withRequiredArg() + .ofType(String.class) + .defaultsTo(DEFAULT_CONFIG_FILE_NAME); + + ArgumentAcceptingOptionSpec appDataDirOpt = + parser.accepts(ROOT_APP_DATA_DIR, "Application data directory") + .withRequiredArg() + .ofType(File.class) + .defaultsTo(Paths.get("apitest", "build", "resources", "main") + .toFile().getAbsoluteFile()); + + ArgumentAcceptingOptionSpec bashPathOpt = + parser.accepts(BASH_PATH, "Bash path") + .withRequiredArg() + .ofType(String.class) + .defaultsTo( + (getenv("SHELL") == null || !getenv("SHELL").contains("bash")) + ? "/bin/bash" + : getenv("SHELL")); + + ArgumentAcceptingOptionSpec berkeleyDbLibPathOpt = + parser.accepts(BERKELEYDB_LIB_PATH, "Berkeley DB lib path") + .withRequiredArg() + .ofType(String.class).defaultsTo("/usr/local/BerkeleyDB.4.8/lib"); + + ArgumentAcceptingOptionSpec bitcoinPathOpt = + parser.accepts(BITCOIN_PATH, "Bitcoin path") + .withRequiredArg() + .ofType(String.class).defaultsTo("/usr/local/bin"); + + ArgumentAcceptingOptionSpec bitcoinRegtestHostOpt = + parser.accepts(BITCOIN_REGTEST_HOST, "Bitcoin Core regtest host") + .withRequiredArg() + .ofType(String.class).defaultsTo("localhost"); + + ArgumentAcceptingOptionSpec bitcoinRpcPortOpt = + parser.accepts(BITCOIN_RPC_PORT, "Bitcoin Core rpc port") + .withRequiredArg() + .ofType(Integer.class).defaultsTo(18443); + + ArgumentAcceptingOptionSpec bitcoinRpcUserOpt = + parser.accepts(BITCOIN_RPC_USER, "Bitcoin rpc user") + .withRequiredArg() + .ofType(String.class).defaultsTo("apitest"); + + ArgumentAcceptingOptionSpec bitcoinRpcPasswordOpt = + parser.accepts(BITCOIN_RPC_PASSWORD, "Bitcoin rpc password") + .withRequiredArg() + .ofType(String.class).defaultsTo("apitest"); + + ArgumentAcceptingOptionSpec apiPasswordOpt = + parser.accepts(API_PASSWORD, "gRPC API password") + .withRequiredArg() + .defaultsTo("xyz"); + + ArgumentAcceptingOptionSpec runSubprojectJarsOpt = + parser.accepts(RUN_SUBPROJECT_JARS, + "Run subproject build jars instead of full build jars") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec runArbNodeAsDesktopOpt = + parser.accepts(RUN_ARB_NODE_AS_DESKTOP, + "Run Arbitration node as desktop") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); // TODO how do I register arbitrator? + + ArgumentAcceptingOptionSpec runAliceNodeAsDesktopOpt = + parser.accepts(RUN_ALICE_NODE_AS_DESKTOP, + "Run Alice node as desktop") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec runBobNodeAsDesktopOpt = + parser.accepts(RUN_BOB_NODE_AS_DESKTOP, + "Run Bob node as desktop") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec skipTestsOpt = + parser.accepts(SKIP_TESTS, + "Start apps, but skip tests") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); + + ArgumentAcceptingOptionSpec shutdownAfterTestsOpt = + parser.accepts(SHUTDOWN_AFTER_TESTS, + "Terminate all processes after tests") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(true); + + ArgumentAcceptingOptionSpec numSetupTasksOpt = + parser.accepts(NUM_SETUP_TASKS, + "Number of test setup tasks") + .withRequiredArg() + .ofType(Integer.class) + .defaultsTo(4); + + try { + CompositeOptionSet options = new CompositeOptionSet(); + + // Parse command line options + OptionSet cliOpts = parser.parse(args); + options.addOptionSet(cliOpts); + + // Parse config file specified at the command line only if it was specified as + // an absolute path. Otherwise, the config file will be processed later below. + File configFile = null; + OptionSpec[] disallowedOpts = new OptionSpec[]{helpOpt, configFileOpt}; + final boolean cliHasConfigFileOpt = cliOpts.has(configFileOpt); + boolean configFileHasBeenProcessed = false; + if (cliHasConfigFileOpt) { + configFile = new File(cliOpts.valueOf(configFileOpt)); + if (configFile.isAbsolute()) { + Optional configFileOpts = parseOptionsFrom(configFile, disallowedOpts); + if (configFileOpts.isPresent()) { + options.addOptionSet(configFileOpts.get()); + configFileHasBeenProcessed = true; + } + } + } + + // If the config file has not yet been processed, either because a relative + // path was provided at the command line, or because no value was provided at + // the command line, attempt to process the file now, falling back to the + // default config file location if none was specified at the command line. + if (!configFileHasBeenProcessed) { + configFile = cliHasConfigFileOpt && !configFile.isAbsolute() ? + absoluteConfigFile(userDir, configFile.getPath()) : + defaultConfigFile; + Optional configFileOpts = parseOptionsFrom(configFile, disallowedOpts); + configFileOpts.ifPresent(options::addOptionSet); + } + + + // Assign all remaining properties, with command line options taking + // precedence over those provided in the config file (if any) + this.helpRequested = options.has(helpOpt); + this.configFile = configFile; + this.rootAppDataDir = options.valueOf(appDataDirOpt); + bashPath = options.valueOf(bashPathOpt); + this.berkeleyDbLibPath = options.valueOf(berkeleyDbLibPathOpt); + this.bitcoinPath = options.valueOf(bitcoinPathOpt); + this.bitcoinRegtestHost = options.valueOf(bitcoinRegtestHostOpt); + this.bitcoinRpcPort = options.valueOf(bitcoinRpcPortOpt); + this.bitcoinRpcUser = options.valueOf(bitcoinRpcUserOpt); + this.bitcoinRpcPassword = options.valueOf(bitcoinRpcPasswordOpt); + this.apiPassword = options.valueOf(apiPasswordOpt); + this.runSubprojectJars = options.valueOf(runSubprojectJarsOpt); + this.runArbNodeAsDesktop = options.valueOf(runArbNodeAsDesktopOpt); + this.runAliceNodeAsDesktop = options.valueOf(runAliceNodeAsDesktopOpt); + this.runBobNodeAsDesktop = options.valueOf(runBobNodeAsDesktopOpt); + this.skipTests = options.valueOf(skipTestsOpt); + this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt); + this.numSetupTasks = options.valueOf(numSetupTasksOpt); + + // Assign values to special-case static fields. + BASH_PATH_VALUE = bashPath; + + // Write and save bitcoin.conf to disk, with the correct path to + // the blocknotify script. + installBitcoinConf(); + installBitcoinBlocknotify(); + + } catch (OptionException ex) { + throw new RuntimeException(format("Problem parsing option '%s': %s", + ex.options().get(0), + ex.getCause() != null ? + ex.getCause().getMessage() : + ex.getMessage())); + } + } + + public void printHelp(OutputStream sink, HelpFormatter formatter) { + try { + parser.formatHelpWith(formatter); + parser.printHelpOn(sink); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private void installBitcoinConf() { + // We write out and install a bitcoin.conf file for regtest/dao mode because + // the path to the blocknotify script is not known until runtime. + String bitcoinConf = "\n" + + "regtest=1\n" + + "[regtest]\n" + + "peerbloomfilters=1\n" + + "rpcport=18443\n" + + "server=1\n" + + "txindex=1\n" + + "debug=net\n" + + "deprecatedrpc=generate\n" + + "rpcuser=apitest\n" + + "rpcpassword=apitest\n" + + "blocknotify=" + bashPath + " " + bitcoinDatadir + "/blocknotify %\n"; + String chmod644Perms = "rw-r--r--"; + saveToFile(bitcoinConf, bitcoinDatadir, "bitcoin.conf", chmod644Perms); + log.info("Installed {} with perms {}.", bitcoinDatadir + "/bitcoin.conf", chmod644Perms); + } + + private void installBitcoinBlocknotify() { + // gradle is not working for this + try { + Path srcPath = Paths.get("apitest", "src", "main", "resources", "blocknotify"); + Path destPath = Paths.get(bitcoinDatadir, "blocknotify"); + Files.copy(srcPath, destPath, StandardCopyOption.REPLACE_EXISTING); + String chmod700Perms = "rwx------"; + Files.setPosixFilePermissions(destPath, PosixFilePermissions.fromString(chmod700Perms)); + log.info("Installed {} with perms {}.", destPath.toString(), chmod700Perms); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private Optional parseOptionsFrom(File configFile, OptionSpec[] disallowedOpts) { + if (!configFile.exists()) { + if (!configFile.equals(absoluteConfigFile(userDir, DEFAULT_CONFIG_FILE_NAME))) + throw new RuntimeException(format("The specified config file '%s' does not exist.", configFile)); + } + + Properties properties = getProperties(configFile); + List optionLines = new ArrayList<>(); + properties.forEach((k, v) -> { + optionLines.add("--" + k + "=" + v); // dashes expected by jopt parser below + }); + + OptionSet configFileOpts = parser.parse(optionLines.toArray(new String[0])); + for (OptionSpec disallowedOpt : disallowedOpts) + if (configFileOpts.has(disallowedOpt)) + throw new RuntimeException( + format("The '%s' option is disallowed in config files", + disallowedOpt.options().get(0))); + + return Optional.of(configFileOpts); + } + + private Properties getProperties(File configFile) { + try { + Properties properties = new Properties(); + properties.load(new FileInputStream(configFile.getAbsolutePath())); + return properties; + } catch (IOException ex) { + throw new RuntimeException( + format("Could not load properties from config file %s", + configFile.getAbsolutePath()), ex); + } + } + + private void saveToFile(String content, + String parentDir, + @SuppressWarnings("SameParameterValue") String relativeConfigFilePath, + String posixFilePermissions) { + File tempFile = null; + File file; + try { + file = absoluteConfigFile(parentDir, relativeConfigFilePath); + tempFile = File.createTempFile("temp", relativeConfigFilePath, file.getParentFile()); + tempFile.deleteOnExit(); + try (PrintWriter out = new PrintWriter(tempFile)) { + out.println(content); + } + FileUtil.renameFile(tempFile, file); + Files.setPosixFilePermissions(Paths.get(file.toURI()), PosixFilePermissions.fromString(posixFilePermissions)); + } catch (IOException ex) { + throw new RuntimeException(format("Error saving %s/%s to disk", parentDir, relativeConfigFilePath), ex); + } finally { + if (tempFile != null && tempFile.exists()) { + log.warn("Temp file still exists after failed save; deleting {} now.", tempFile.getAbsolutePath()); + if (!tempFile.delete()) + log.error("Cannot delete temp file."); + } + } + } + + private static File absoluteConfigFile(String parentDir, String relativeConfigFilePath) { + return new File(parentDir, relativeConfigFilePath); + } +} diff --git a/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java b/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java new file mode 100644 index 00000000000..30046d7262d --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java @@ -0,0 +1,122 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.config; + +import bisq.seednode.SeedNodeMain; + +import bisq.desktop.app.BisqAppMain; + + + +import bisq.daemon.app.BisqDaemonMain; + +/** + Some non user configurable Bisq seednode, arb node, bob and alice daemon option values. + @see dev-setup.md + @see dao-setup.md + */ +@SuppressWarnings("unused") +public enum BisqAppConfig { + + seednode("bisq-BTC_REGTEST_Seed_2002", + "bisq-seednode", + "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + SeedNodeMain.class.getName(), + 2002, + 5120, + -1), + arbdaemon("bisq-BTC_REGTEST_Arb_dao", + "bisq-daemon", + "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + BisqDaemonMain.class.getName(), + 4444, + 5121, + 9997), + arbdesktop("bisq-BTC_REGTEST_Arb_dao", + "bisq-desktop", + "\"-XX:MaxRAM=3g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + BisqAppMain.class.getName(), + 4444, + 5121, + -1), + alicedaemon("bisq-BTC_REGTEST_Alice_dao", + "bisq-daemon", + "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + BisqDaemonMain.class.getName(), + 7777, + 5122, + 9998), + alicedesktop("bisq-BTC_REGTEST_Alice_dao", + "bisq-desktop", + "\"-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + BisqAppMain.class.getName(), + 7777, + 5122, + -1), + bobdaemon("bisq-BTC_REGTEST_Bob_dao", + "bisq-daemon", + "\"-XX:MaxRAM=2g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + BisqDaemonMain.class.getName(), + 8888, + 5123, + 9999), + bobdesktop("bisq-BTC_REGTEST_Bob_dao", + "bisq-desktop", + "\"-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", + BisqAppMain.class.getName(), + 8888, + 5123, -1); + + public final String appName; + public final String startupScript; + public final String javaOpts; + public final String mainClassName; + public final int nodePort; + public final int rpcBlockNotificationPort; + // Daemons can use a global gRPC password, but each needs a unique apiPort. + public final int apiPort; + + BisqAppConfig(String appName, + String startupScript, + String javaOpts, + String mainClassName, + int nodePort, + int rpcBlockNotificationPort, + int apiPort) { + this.appName = appName; + this.startupScript = startupScript; + this.javaOpts = javaOpts; + this.mainClassName = mainClassName; + this.nodePort = nodePort; + this.rpcBlockNotificationPort = rpcBlockNotificationPort; + this.apiPort = apiPort; + } + + @Override + public String toString() { + return "BisqAppConfig{" + "\n" + + " appName='" + appName + '\'' + "\n" + + ", startupScript='" + startupScript + '\'' + "\n" + + ", javaOpts='" + javaOpts + '\'' + "\n" + + ", mainClassName='" + mainClassName + '\'' + "\n" + + ", nodePort=" + nodePort + "\n" + + ", rpcBlockNotificationPort=" + rpcBlockNotificationPort + "\n" + + ", apiPort=" + apiPort + "\n" + + '}'; + } +} diff --git a/apitest/src/main/java/bisq/apitest/config/CompositeOptionSet.java b/apitest/src/main/java/bisq/apitest/config/CompositeOptionSet.java new file mode 100644 index 00000000000..341b1e01017 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/config/CompositeOptionSet.java @@ -0,0 +1,61 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.config; + +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import java.util.ArrayList; +import java.util.List; + +/** + * Composes multiple JOptSimple {@link OptionSet} instances such that calls to + * {@link #valueOf(OptionSpec)} and co will search all instances in the order they were + * added and return any value explicitly set, otherwise returning the default value for + * the given option or null if no default has been set. The API found here loosely + * emulates the {@link OptionSet} API without going through the unnecessary work of + * actually extending it. In practice, this class is used to compose options provided at + * the command line with those provided via config file, such that those provided at the + * command line take precedence over those provided in the config file. + */ +class CompositeOptionSet { + + private final List optionSets = new ArrayList<>(); + + public void addOptionSet(OptionSet optionSet) { + optionSets.add(optionSet); + } + + public boolean has(OptionSpec option) { + for (OptionSet optionSet : optionSets) + if (optionSet.has(option)) + return true; + + return false; + } + + public V valueOf(OptionSpec option) { + for (OptionSet optionSet : optionSets) + if (optionSet.has(option)) + return optionSet.valueOf(option); + + // None of the provided option sets specified the given option so fall back to + // the default value (if any) provided by the first specified OptionSet + return optionSets.get(0).valueOf(option); + } +} From e0c27eedcf7671365faf31ef4dcabd2e11aead2d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 9 Jul 2020 15:27:01 -0300 Subject: [PATCH 02/88] Add main resource files * apitest.properties - config file for customizing ApiTestConfig options * logback.xml - logging config file, will override logback files found in classpath * bitcoin.conf - bitcoin-core regtest config file, overwritten during startup with correct path to blocknofity * blocknotify - bitcoin-core blocknotify config file --- apitest/src/main/resources/apitest.properties | 0 apitest/src/main/resources/bitcoin.conf | 16 ++++++++++++++ apitest/src/main/resources/blocknotify | 20 ++++++++++++++++++ apitest/src/main/resources/logback.xml | 21 +++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 apitest/src/main/resources/apitest.properties create mode 100644 apitest/src/main/resources/bitcoin.conf create mode 100755 apitest/src/main/resources/blocknotify create mode 100644 apitest/src/main/resources/logback.xml diff --git a/apitest/src/main/resources/apitest.properties b/apitest/src/main/resources/apitest.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apitest/src/main/resources/bitcoin.conf b/apitest/src/main/resources/bitcoin.conf new file mode 100644 index 00000000000..7c92bbf0b34 --- /dev/null +++ b/apitest/src/main/resources/bitcoin.conf @@ -0,0 +1,16 @@ +# This file is overwritten by ApiTestConfig.java at startup, to set the correct blocknotify script path. +regtest=1 + +[regtest] +peerbloomfilters=1 +rpcport=18443 + +server=1 +txindex=1 + +# TODO migrate to rpcauth +rpcuser=apitest +rpcpassword=apitest + +# The blocknotify path will be defined and at runtime. +# blocknotify=bash ~/.bitcoin/blocknotify % diff --git a/apitest/src/main/resources/blocknotify b/apitest/src/main/resources/blocknotify new file mode 100755 index 00000000000..210d23f17c6 --- /dev/null +++ b/apitest/src/main/resources/blocknotify @@ -0,0 +1,20 @@ +#!/bin/bash + +# Regtest ports start with 512* + +# To avoid pesky bitcoind io errors, do not specify ports Bisq is not listening to. + +# SeedNode listens on port 5120 +echo $1 | nc -w 1 127.0.0.1 5120 + +# Arb Node listens on port 5121 +echo $1 | nc -w 1 127.0.0.1 5121 + +# Alice Node listens on port 5122 +echo $1 | nc -w 1 127.0.0.1 5122 + +# Bob Node listens on port 5123 +echo $1 | nc -w 1 127.0.0.1 5123 + +# Some other node listens on port 5124, etc. +# echo $1 | nc -w 1 127.0.0.1 5124 diff --git a/apitest/src/main/resources/logback.xml b/apitest/src/main/resources/logback.xml new file mode 100644 index 00000000000..9c5d0974bff --- /dev/null +++ b/apitest/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + + + + + + + + + + From 77b6878ec6b9dbf4aa2fa113dd1aa21f12a348f6 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 9 Jul 2020 15:30:22 -0300 Subject: [PATCH 03/88] Add script to get Bisq app pid Finding the pid of background linux/java processes has been difficult to do from java, so a bash script is provided to simplify the task. --- apitest/scripts/get-bisq-pid.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 apitest/scripts/get-bisq-pid.sh diff --git a/apitest/scripts/get-bisq-pid.sh b/apitest/scripts/get-bisq-pid.sh new file mode 100755 index 00000000000..0897cccb227 --- /dev/null +++ b/apitest/scripts/get-bisq-pid.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Find the pid of the java process by grepping for the mainClassName and appName, +# then print the 2nd column of the output to stdout. +# +# Doing this from Java is problematic, probably due to limitation of the +# apitest.linux.BashCommand implementation. + + +MAIN_CLASS_NAME=$1 +APP_NAME=$2 + +# TODO args validation + +ps aux | grep java | grep ${MAIN_CLASS_NAME} | grep ${APP_NAME} | awk '{print $2}' From c0c75e247140c1a7b4e77629b50e11c1eb8fbb50 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 9 Jul 2020 15:35:45 -0300 Subject: [PATCH 04/88] Support starting bitcoin & bisq apps on Linux The apitest.linux package is for running random bash commands, running 'bitcoind -regtest', running random 'bitcoin-cli -regtest' commands, and spinning up Bisq apps such as seednode, arbnode, and bob & alice nodes. All but random bash and bitcoin-cli commands are run in the background. The bitcoin-cli response processing is crude; a more sophiticated bitcoin-core rpc interface is not in the scope of this PR. --- .../apitest/linux/AbstractLinuxProcess.java | 91 +++++++ .../java/bisq/apitest/linux/BashCommand.java | 149 ++++++++++ .../main/java/bisq/apitest/linux/BisqApp.java | 257 ++++++++++++++++++ .../java/bisq/apitest/linux/BitcoinCli.java | 166 +++++++++++ .../bisq/apitest/linux/BitcoinDaemon.java | 91 +++++++ .../java/bisq/apitest/linux/LinuxProcess.java | 30 ++ .../apitest/linux/SystemCommandExecutor.java | 119 ++++++++ .../apitest/linux/ThreadedStreamHandler.java | 128 +++++++++ 8 files changed, 1031 insertions(+) create mode 100644 apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java create mode 100644 apitest/src/main/java/bisq/apitest/linux/BashCommand.java create mode 100644 apitest/src/main/java/bisq/apitest/linux/BisqApp.java create mode 100644 apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java create mode 100644 apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java create mode 100644 apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java create mode 100644 apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java create mode 100644 apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java new file mode 100644 index 00000000000..19391898b52 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -0,0 +1,91 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.linux; + +import java.io.File; +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.linux.BashCommand.isAlive; +import static java.lang.String.format; + + + +import bisq.apitest.config.ApiTestConfig; + +@Slf4j +abstract class AbstractLinuxProcess implements LinuxProcess { + + protected final String name; + + protected long pid; + + protected final ApiTestConfig config; + + public AbstractLinuxProcess(String name, ApiTestConfig config) { + this.name = name; + this.config = config; + } + + @Override + public String getName() { + return this.name; + } + + public void verifyBitcoinConfig() { + verifyBitcoinConfig(false); + } + + public void verifyBitcoinConfig(boolean verbose) { + if (verbose) + log.info(format("Checking bitcoind env...%n" + + "\t%-20s%s%n\t%-20s%s%n\t%-20s%s%n\t%-20s%s%n\t%-20s%s", + "berkeleyDbLibPath", config.berkeleyDbLibPath, + "bitcoinPath", config.bitcoinPath, + "bitcoinDatadir", config.bitcoinDatadir, + "bitcoin.conf", config.bitcoinDatadir + "/bitcoin.conf", + "blocknotify", config.bitcoinDatadir + "/blocknotify")); + + File berkeleyDbLibPath = new File(config.berkeleyDbLibPath); + if (!berkeleyDbLibPath.exists() || !berkeleyDbLibPath.canExecute()) + throw new IllegalStateException(berkeleyDbLibPath + "cannot be found or executed"); + + File bitcoindExecutable = new File(config.bitcoinPath); + if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute()) + throw new IllegalStateException(bitcoindExecutable + "cannot be found or executed"); + + File bitcoindDatadir = new File(config.bitcoinDatadir); + if (!bitcoindDatadir.exists() || !bitcoindDatadir.canWrite()) + throw new IllegalStateException(bitcoindDatadir + "cannot be found or written to"); + + File bitcoinConf = new File(bitcoindDatadir, "bitcoin.conf"); + if (!bitcoinConf.exists() || !bitcoinConf.canRead()) + throw new IllegalStateException(bitcoindDatadir + "cannot be found or read"); + + File blocknotify = new File(bitcoindDatadir, "blocknotify"); + if (!blocknotify.exists() || !blocknotify.canExecute()) + throw new IllegalStateException(bitcoindDatadir + "cannot be found or executed"); + } + + public void verifyBitcoindRunning() throws IOException, InterruptedException { + long bitcoindPid = BashCommand.getPid("bitcoind"); + if (bitcoindPid < 0 || !isAlive(bitcoindPid)) + throw new IllegalStateException("Bitcoind not running"); + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/BashCommand.java b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java new file mode 100644 index 00000000000..2eaa0b9f8e4 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java @@ -0,0 +1,149 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.linux; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import static java.lang.management.ManagementFactory.getRuntimeMXBean; + + + +import bisq.apitest.config.ApiTestConfig; + +@Slf4j +public class BashCommand { + + private int exitStatus = -1; + private String output; + private String error; + + private final String command; + private final int numResponseLines; + + public BashCommand(String command) { + this(command, 0); + } + + public BashCommand(String command, int numResponseLines) { + this.command = command; + this.numResponseLines = numResponseLines; // only want the top N lines of output + } + + public BashCommand run() throws IOException, InterruptedException { + SystemCommandExecutor commandExecutor = new SystemCommandExecutor(tokenizeSystemCommand()); + exitStatus = commandExecutor.execCommand(); + + // Get the error status and stderr from system command. + StringBuilder stderr = commandExecutor.getStandardErrorFromCommand(); + if (stderr.length() > 0) + error = stderr.toString(); + + if (exitStatus != 0) + return this; + + // Format and cache the stdout from system command. + StringBuilder stdout = commandExecutor.getStandardOutputFromCommand(); + String[] rawLines = stdout.toString().split("\n"); + StringBuilder truncatedLines = new StringBuilder(); + int limit = numResponseLines > 0 ? Math.min(numResponseLines, rawLines.length) : rawLines.length; + for (int i = 0; i < limit; i++) { + String line = rawLines[i].length() >= 220 ? rawLines[i].substring(0, 220) + " ..." : rawLines[i]; + truncatedLines.append(line).append((i < limit - 1) ? "\n" : ""); + } + output = truncatedLines.toString(); + return this; + } + + public String getCommand() { + return this.command; + } + + public int getExitStatus() { + return this.exitStatus; + } + + // TODO return Optional + public String getOutput() { + return this.output; + } + + // TODO return Optional + public String getError() { + return this.error; + } + + @NotNull + private List tokenizeSystemCommand() { + return new ArrayList<>() {{ + add(ApiTestConfig.BASH_PATH_VALUE); + add("-c"); + add(command); + }}; + } + + @SuppressWarnings("unused") + // Convenience method for getting system load info. + public static String printSystemLoadString(Exception tracingException) throws IOException, InterruptedException { + StackTraceElement[] stackTraceElement = tracingException.getStackTrace(); + StringBuilder stackTraceBuilder = new StringBuilder(tracingException.getMessage()).append("\n"); + int traceLimit = Math.min(stackTraceElement.length, 4); + for (int i = 0; i < traceLimit; i++) { + stackTraceBuilder.append(stackTraceElement[i]).append("\n"); + } + stackTraceBuilder.append("..."); + log.info(stackTraceBuilder.toString()); + BashCommand cmd = new BashCommand("ps -aux --sort -rss --headers", 2).run(); + return cmd.getOutput() + "\n" + + "System load: Memory (MB): " + getUsedMemoryInMB() + " / No. of threads: " + Thread.activeCount() + + " JVM uptime (ms): " + getRuntimeMXBean().getUptime(); + } + + public static long getUsedMemoryInMB() { + Runtime runtime = Runtime.getRuntime(); + long free = runtime.freeMemory() / 1024 / 1024; + long total = runtime.totalMemory() / 1024 / 1024; + return total - free; + } + + public static long getPid(String processName) throws IOException, InterruptedException { + String psCmd = "ps aux | pgrep " + processName + " | grep -v grep"; + String psCmdOutput = new BashCommand(psCmd).run().getOutput(); + if (psCmdOutput == null || psCmdOutput.isEmpty()) + return -1; + + return Long.parseLong(psCmdOutput); + } + + @SuppressWarnings("unused") + public static BashCommand grep(String processName) throws IOException, InterruptedException { + String c = "ps -aux | grep " + processName + " | grep -v grep"; + return new BashCommand(c).run(); + } + + public static boolean isAlive(long pid) throws IOException, InterruptedException { + String isAliveScript = "if ps -p " + pid + " > /dev/null; then echo true; else echo false; fi"; + return new BashCommand(isAliveScript).run().getOutput().equals("true"); + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java new file mode 100644 index 00000000000..2ed3d3540f0 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java @@ -0,0 +1,257 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.linux; + +import java.nio.file.Paths; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.linux.BashCommand.isAlive; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.config.BisqAppConfig; +import bisq.daemon.app.BisqDaemonMain; + +/** + * Runs a regtest/dao Bisq application instance in the background. + */ +@Slf4j +public class BisqApp extends AbstractLinuxProcess implements LinuxProcess { + + private final BisqAppConfig bisqAppConfig; + private final String baseCurrencyNetwork; + private final String genesisTxId; + private final int genesisBlockHeight; + private final String seedNodes; + private final boolean daoActivated; + private final boolean fullDaoNode; + private final boolean useLocalhostForP2P; + public final boolean useDevPrivilegeKeys; + private final String findBisqPidScript; + private final List startupExceptions; + + public BisqApp(BisqAppConfig bisqAppConfig, ApiTestConfig config) { + super(bisqAppConfig.appName, config); + this.bisqAppConfig = bisqAppConfig; + this.baseCurrencyNetwork = "BTC_REGTEST"; + this.genesisTxId = "30af0050040befd8af25068cc697e418e09c2d8ebd8d411d2240591b9ec203cf"; + this.genesisBlockHeight = 111; + this.seedNodes = "localhost:2002"; + this.daoActivated = true; + this.fullDaoNode = true; + this.useLocalhostForP2P = true; + this.useDevPrivilegeKeys = true; + this.findBisqPidScript = config.userDir + "/apitest/scripts/get-bisq-pid.sh"; + this.startupExceptions = new ArrayList<>(); + } + + @Override + public void start() throws InterruptedException, IOException { + try { + if (config.runSubprojectJars) + runJar(); // run subproject/build/lib/*.jar (not full build) + else + runStartupScript(); // run bisq-* script for end to end test (default) + } catch (Throwable t) { + startupExceptions.add(t); + } + } + + @Override + public long getPid() { + return this.pid; + } + + @Override + public void shutdown() throws IOException, InterruptedException { + try { + log.info("Shutting down {} ...", bisqAppConfig.appName); + if (!isAlive(pid)) + throw new IllegalStateException(format("%s already shut down", bisqAppConfig.appName)); + + String killCmd = "kill -15 " + pid; + if (new BashCommand(killCmd).run().getExitStatus() != 0) + throw new IllegalStateException(format("Could not shut down %s", bisqAppConfig.appName)); + + MILLISECONDS.sleep(4000); // allow it time to shutdown + log.info("{} stopped", bisqAppConfig.appName); + } catch (Exception e) { + throw new IllegalStateException(format("Error shutting down %s", bisqAppConfig.appName), e); + } finally { + if (isAlive(pid)) + //noinspection ThrowFromFinallyBlock + throw new IllegalStateException(format("%s shutdown did not work", bisqAppConfig.appName)); + } + } + + public void verifyAppNotRunning() throws IOException, InterruptedException { + long pid = findBisqAppPid(); + if (pid >= 0) + throw new IllegalStateException(format("%s %s already running with pid %d", + bisqAppConfig.mainClassName, bisqAppConfig.appName, pid)); + } + + public void verifyAppDataDirInstalled() { + // If we're running an Alice or Bob daemon, make sure the dao-setup directory + // are installed. + switch (bisqAppConfig) { + case alicedaemon: + case alicedesktop: + case bobdaemon: + case bobdesktop: + File bisqDataDir = new File(config.rootAppDataDir, bisqAppConfig.appName); + if (!bisqDataDir.exists()) + throw new IllegalStateException(format("Application dataDir %s/%s not found", + config.rootAppDataDir, bisqAppConfig.appName)); + break; + default: + break; + } + } + + public boolean hasStartupExceptions() { + return !startupExceptions.isEmpty(); + } + + public List getStartupExceptions() { + return startupExceptions; + } + + // This is the non-default way of running a Bisq app (--runSubprojectJars=true). + // It runs a java cmd, and does not depend on a full build. Bisq jars are loaded + // from the :subproject/build/libs directories. + private void runJar() throws IOException, InterruptedException { + String java = getJavaExecutable().getAbsolutePath(); + String classpath = System.getProperty("java.class.path"); + String bisqCmd = getJavaOptsSpec() + + " " + java + " -cp " + classpath + + " " + bisqAppConfig.mainClassName + + " " + String.join(" ", getOptsList()) + + " &"; // run in background without nohup + runBashCommand(bisqCmd); + } + + // This is the default way of running a Bisq app (--runSubprojectJars=false). + // It runs a bisq-* startup script, and depends on a full build. Bisq jars + // are loaded from the root project's lib directory. + private void runStartupScript() throws IOException, InterruptedException { + String bisqCmd = getJavaOptsSpec() + + " " + config.userDir + "/" + bisqAppConfig.startupScript + + " " + String.join(" ", getOptsList()) + + " &"; // run in background without nohup + runBashCommand(bisqCmd); + } + + private void runBashCommand(String bisqCmd) throws IOException, InterruptedException { + String cmdDescription = config.runSubprojectJars + ? "java -> " + bisqAppConfig.mainClassName + " -> " + bisqAppConfig.appName + : bisqAppConfig.startupScript + " -> " + bisqAppConfig.appName; + BashCommand bashCommand = new BashCommand(bisqCmd); + log.info("Starting {} ...\n$ {}", cmdDescription, bashCommand.getCommand()); + bashCommand.run(); + + if (bashCommand.getExitStatus() != 0) + throw new IllegalStateException(format("Error starting BisqApp\n%s\nError: %s", + bashCommand.getError())); + + // Sometimes it takes a little extra time to find the linux process id. + // Wait up to two seconds before giving up and throwing an Exception. + for (int i = 0; i < 4; i++) { + pid = findBisqAppPid(); + if (pid != -1) + break; + + MILLISECONDS.sleep(500L); + } + if (!isAlive(pid)) + throw new IllegalStateException(format("Error finding pid for %s", this.name)); + + log.info("{} running with pid {}", cmdDescription, pid); + log.info("Log {}", config.rootAppDataDir + "/" + bisqAppConfig.appName + "/bisq.log"); + } + + private long findBisqAppPid() throws IOException, InterruptedException { + // Find the pid of the java process by grepping for the mainClassName and appName. + String findPidCmd = findBisqPidScript + " " + bisqAppConfig.mainClassName + " " + bisqAppConfig.appName; + String psCmdOutput = new BashCommand(findPidCmd).run().getOutput(); + return (psCmdOutput == null || psCmdOutput.isEmpty()) ? -1 : Long.parseLong(psCmdOutput); + } + + private String getJavaOptsSpec() { + return "export JAVA_OPTS=" + bisqAppConfig.javaOpts + "; "; + } + + private List getOptsList() { + return new ArrayList<>() {{ + add("--appName=" + bisqAppConfig.appName); + add("--appDataDir=" + config.rootAppDataDir.getAbsolutePath() + "/" + bisqAppConfig.appName); + add("--nodePort=" + bisqAppConfig.nodePort); + add("--rpcBlockNotificationPort=" + bisqAppConfig.rpcBlockNotificationPort); + add("--rpcUser=" + config.bitcoinRpcUser); + add("--rpcPassword=" + config.bitcoinRpcPassword); + add("--rpcPort=" + config.bitcoinRpcPort); + add("--daoActivated=" + daoActivated); + add("--fullDaoNode=" + fullDaoNode); + add("--seedNodes=" + seedNodes); + add("--baseCurrencyNetwork=" + baseCurrencyNetwork); + add("--useDevPrivilegeKeys=" + useDevPrivilegeKeys); + add("--useLocalhostForP2P=" + useLocalhostForP2P); + switch (bisqAppConfig) { + case seednode: + break; // no extra opts needed for seed node + case arbdaemon: + case arbdesktop: + case alicedaemon: + case alicedesktop: + case bobdaemon: + case bobdesktop: + add("--genesisBlockHeight=" + genesisBlockHeight); + add("--genesisTxId=" + genesisTxId); + if (bisqAppConfig.mainClassName.equals(BisqDaemonMain.class.getName())) { + add("--apiPassword=" + config.apiPassword); + add("--apiPort=" + bisqAppConfig.apiPort); + } + break; + default: + throw new IllegalStateException("Unknown BisqAppConfig " + bisqAppConfig.name()); + } + }}; + } + + private File getJavaExecutable() { + File javaHome = Paths.get(System.getProperty("java.home")).toFile(); + if (!javaHome.exists()) + throw new IllegalStateException(format("$JAVA_HOME not found, cannot run %s", bisqAppConfig.mainClassName)); + + File javaExecutable = Paths.get(javaHome.getAbsolutePath(), "bin", "java").toFile(); + if (javaExecutable.exists() || javaExecutable.canExecute()) + return javaExecutable; + else + throw new IllegalStateException("$JAVA_HOME/bin/java not found or executable"); + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java new file mode 100644 index 00000000000..2fd475cce77 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java @@ -0,0 +1,166 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.linux; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.apitest.config.ApiTestConfig; + +@Slf4j +public class BitcoinCli extends AbstractLinuxProcess implements LinuxProcess { + + private final String command; + + private String commandWithOptions; + private String output; + private boolean error; + + public BitcoinCli(ApiTestConfig config, String command) { + super("bitcoin-cli", config); + this.command = command; + this.error = false; + } + + public BitcoinCli run() throws IOException, InterruptedException { + this.start(); + return this; + } + + public String getCommandWithOptions() { + return commandWithOptions; + } + + public String getOutput() { + if (isError()) + throw new IllegalStateException(output); + + // Some responses are not in json format, such as what is returned by + // 'getnewaddress'. The raw output string is the value. + + return output; + } + + public String[] getOutputValueAsStringArray() { + if (isError()) + throw new IllegalStateException(output); + + if (!output.startsWith("[") && !output.endsWith("]")) + throw new IllegalStateException(output + "\nis not a json array"); + + String[] lines = output.split("\n"); + String[] array = new String[lines.length - 2]; + for (int i = 1; i < lines.length - 1; i++) { + array[i - 1] = lines[i].replaceAll("[^a-zA-Z0-9.]", ""); + } + + return array; + } + + public String getOutputValueAsString(String key) { + if (isError()) + throw new IllegalStateException(output); + + // Some assumptions about bitcoin-cli json string parsing: + // Every multi valued, non-error bitcoin-cli response will be a json string. + // Every key/value in the json string will terminate with a newline. + // Most key/value lines in json strings have a ',' char in front of the newline. + // e.g., bitcoin-cli 'getwalletinfo' output: + // { + // "walletname": "", + // "walletversion": 159900, + // "balance": 527.49941568, + // "unconfirmed_balance": 0.00000000, + // "immature_balance": 5000.00058432, + // "txcount": 114, + // "keypoololdest": 1528018235, + // "keypoolsize": 1000, + // "keypoolsize_hd_internal": 1000, + // "paytxfee": 0.00000000, + // "hdseedid": "179b609a60c2769138844c3e36eb430fd758a9c6", + // "private_keys_enabled": true, + // "avoid_reuse": false, + // "scanning": false + // } + + int keyIdx = output.indexOf("\"" + key + "\":"); + int eolIdx = output.indexOf("\n", keyIdx); + String valueLine = output.substring(keyIdx, eolIdx); // "balance": 527.49941568, + String[] keyValue = valueLine.split(":"); + + // Remove all but alphanumeric chars and decimal points from the return value, + // including quotes around strings, and trailing commas. + // Adjustments will be necessary as we begin to work with more complex + // json values, such as arrays. + return keyValue[1].replaceAll("[^a-zA-Z0-9.]", ""); + } + + public boolean getOutputValueAsBoolean(String key) { + String valueStr = getOutputValueAsString(key); + return Boolean.parseBoolean(valueStr); + } + + + public int getOutputValueAsInt(String key) { + String valueStr = getOutputValueAsString(key); + return Integer.parseInt(valueStr); + } + + public double getOutputValueAsDouble(String key) { + String valueStr = getOutputValueAsString(key); + return Double.parseDouble(valueStr); + } + + public long getOutputValueAsLong(String key) { + String valueStr = getOutputValueAsString(key); + return Long.parseLong(valueStr); + } + + public boolean isError() { + return error; + } + + @Override + public void start() throws InterruptedException, IOException { + verifyBitcoinConfig(false); + verifyBitcoindRunning(); + commandWithOptions = config.bitcoinPath + "/bitcoin-cli -regtest " + + " -rpcuser=" + config.bitcoinRpcUser + + " -rpcpassword=" + config.bitcoinRpcPassword + + " " + command; + output = new BashCommand(commandWithOptions).run().getOutput(); + error = output.startsWith("error"); + } + + @Override + public long getPid() { + // We don't cache the pid. The bitcoin-cli will quickly return a + // response, including server error info if any. + throw new UnsupportedOperationException("getPid not supported"); + } + + @Override + public void shutdown() { + // We don't try to shutdown the bitcoin-cli. It will quickly return a + // response, including server error info if any. + throw new UnsupportedOperationException("shutdown not supported"); + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java new file mode 100644 index 00000000000..ac958431a46 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java @@ -0,0 +1,91 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.linux; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.linux.BashCommand.isAlive; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + + + +import bisq.apitest.config.ApiTestConfig; + +// Some cmds: +// bitcoin-cli -regtest generatetoaddress 1 "2MyBq4jbtDF6CfKNrrQdp7qkRc8mKuCpKno" +// bitcoin-cli -regtest getbalance +// note: getbalance does not include immature coins (<100 blks deep) +// bitcoin-cli -regtest getbalances +// bitcoin-cli -regtest getrpcinfo + +@Slf4j +public class BitcoinDaemon extends AbstractLinuxProcess implements LinuxProcess { + + public BitcoinDaemon(ApiTestConfig config) { + super("bitcoind", config); + } + + @Override + public void start() throws InterruptedException, IOException { + String bitcoindCmd = "export LD_LIBRARY_PATH=" + config.berkeleyDbLibPath + ";" + + " " + config.bitcoinPath + "/bitcoind" + + " -datadir=" + config.bitcoinDatadir + + " -daemon"; + + BashCommand cmd = new BashCommand(bitcoindCmd).run(); + log.info("Starting ...\n$ {}", cmd.getCommand()); + + if (cmd.getExitStatus() != 0) + throw new IllegalStateException("Error starting bitcoind:\n" + cmd.getError()); + + pid = BashCommand.getPid("bitcoind"); + if (!isAlive(pid)) + throw new IllegalStateException("Error starting regtest bitcoind daemon:\n" + cmd.getCommand()); + + log.info("Running with pid {}", pid); + log.info("Log {}", config.bitcoinDatadir + "/regtest/debug.log"); + } + + @Override + public long getPid() { + return this.pid; + } + + @Override + public void shutdown() throws IOException, InterruptedException { + try { + log.info("Shutting down bitcoind daemon..."); + if (!isAlive(pid)) + throw new IllegalStateException("bitcoind already shut down"); + + if (new BashCommand("killall bitcoind").run().getExitStatus() != 0) + throw new IllegalStateException("Could not shut down bitcoind; probably already stopped."); + + MILLISECONDS.sleep(2000); // allow it time to shutdown + log.info("Stopped"); + } catch (Exception e) { + throw new IllegalStateException("Error shutting down bitcoind", e); + } finally { + if (isAlive(pid)) + //noinspection ThrowFromFinallyBlock + throw new IllegalStateException("bitcoind shutdown did not work"); + } + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java new file mode 100644 index 00000000000..aa673c8803d --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java @@ -0,0 +1,30 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.linux; + +import java.io.IOException; + +public interface LinuxProcess { + void start() throws InterruptedException, IOException; + + String getName(); + + long getPid(); + + void shutdown() throws IOException, InterruptedException; +} diff --git a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java new file mode 100644 index 00000000000..ece5df1e1c9 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java @@ -0,0 +1,119 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.linux; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +/** + * This class can be used to execute a system command from a Java application. + * See the documentation for the public methods of this class for more + * information. + * + * Documentation for this class is available at this URL: + * + * http://devdaily.com/java/java-processbuilder-process-system-exec + * + * Copyright 2010 alvin j. alexander, devdaily.com. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser Public License for more details. + * You should have received a copy of the GNU Lesser Public License + * along with this program. If not, see . + * + * Please ee the following page for the LGPL license: + * http://www.gnu.org/licenses/lgpl.txt + * + */ +@Slf4j +class SystemCommandExecutor { + private final List cmdOptions; + private final String sudoPassword; + private ThreadedStreamHandler inputStreamHandler; + private ThreadedStreamHandler errorStreamHandler; + + /* + * Note: I've removed the other constructor that was here to support executing + * the sudo command. I'll add that back in when I get the sudo command + * working to the point where it won't hang when the given password is + * wrong. + */ + public SystemCommandExecutor(final List cmdOptions) { + if (cmdOptions == null) + throw new NullPointerException("No command params specified."); + + this.cmdOptions = cmdOptions; + this.sudoPassword = null; + } + + // Execute a system command and return its status code (0 or 1). + // The system command's output (stderr or stdout) can be accessed from accessors. + public int execCommand() throws IOException, InterruptedException { + Process process = new ProcessBuilder(cmdOptions).start(); + + // you need this if you're going to write something to the command's input stream + // (such as when invoking the 'sudo' command, and it prompts you for a password). + OutputStream stdOutput = process.getOutputStream(); + + // i'm currently doing these on a separate line here in case i need to set them to null + // to get the threads to stop. + // see http://java.sun.com/j2se/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html + InputStream inputStream = process.getInputStream(); + InputStream errorStream = process.getErrorStream(); + + // these need to run as java threads to get the standard output and error from the command. + // the inputstream handler gets a reference to our stdOutput in case we need to write + // something to it, such as with the sudo command + inputStreamHandler = new ThreadedStreamHandler(inputStream, stdOutput, sudoPassword); + errorStreamHandler = new ThreadedStreamHandler(errorStream); + + // TODO the inputStreamHandler has a nasty side-effect of hanging if the given password is wrong; fix it. + inputStreamHandler.start(); + errorStreamHandler.start(); + + int exitStatus = process.waitFor(); + + inputStreamHandler.interrupt(); + errorStreamHandler.interrupt(); + inputStreamHandler.join(); + errorStreamHandler.join(); + return exitStatus; + } + + + // Get the standard error from an executed system command. + public StringBuilder getStandardErrorFromCommand() { + return errorStreamHandler.getOutputBuffer(); + } + + // Get the standard output from an executed system command. + public StringBuilder getStandardOutputFromCommand() { + return inputStreamHandler.getOutputBuffer(); + } +} diff --git a/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java new file mode 100644 index 00000000000..7390b8807e4 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java @@ -0,0 +1,128 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.linux; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; + +import lombok.extern.slf4j.Slf4j; + +/** + * This class is intended to be used with the SystemCommandExecutor + * class to let users execute system commands from Java applications. + * + * This class is based on work that was shared in a JavaWorld article + * named "When System.exec() won't". That article is available at this + * url: + * + * http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html + * + * Documentation for this class is available at this URL: + * + * http://devdaily.com/java/java-processbuilder-process-system-exec + * + * + * Copyright 2010 alvin j. alexander, devdaily.com. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser Public License for more details. + * You should have received a copy of the GNU Lesser Public License + * along with this program. If not, see . + * + * Please ee the following page for the LGPL license: + * http://www.gnu.org/licenses/lgpl.txt + * + */ +@Slf4j +class ThreadedStreamHandler extends Thread { + final InputStream inputStream; + String adminPassword; + @SuppressWarnings("unused") + OutputStream outputStream; + PrintWriter printWriter; + final StringBuilder outputBuffer = new StringBuilder(); + private boolean sudoIsRequested = false; + + /** + * A simple constructor for when the sudo command is not necessary. + * This constructor will just run the command you provide, without + * running sudo before the command, and without expecting a password. + * + * @param inputStream InputStream + */ + ThreadedStreamHandler(InputStream inputStream) { + this.inputStream = inputStream; + } + + /** + * Use this constructor when you want to invoke the 'sudo' command. + * The outputStream must not be null. If it is, you'll regret it. :) + * + * TODO this currently hangs if the admin password given for the sudo command is wrong. + * + * @param inputStream InputStream + * @param outputStream OutputStream + * @param adminPassword String + */ + ThreadedStreamHandler(InputStream inputStream, OutputStream outputStream, String adminPassword) { + this.inputStream = inputStream; + this.outputStream = outputStream; + this.printWriter = new PrintWriter(outputStream); + this.adminPassword = adminPassword; + this.sudoIsRequested = true; + } + + public void run() { + // On mac os x 10.5.x, the admin password needs to be written immediately. + if (sudoIsRequested) { + printWriter.println(adminPassword); + printWriter.flush(); + } + + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = bufferedReader.readLine()) != null) + outputBuffer.append(line).append("\n"); + + } catch (Throwable t) { + t.printStackTrace(); + } + } + + @SuppressWarnings("unused") + private void doSleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException ignored) { + } + } + + public StringBuilder getOutputBuffer() { + return outputBuffer; + } +} + From ae3b263cacbd699f62545b88070e10e25900f191 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 9 Jul 2020 15:49:12 -0300 Subject: [PATCH 05/88] Add :apitest main driver, setup task & dummy tests The driver class uses an ExecutorService to submit Callable tasks for starting bitcoind and Bisq nodes as Linux background processes. By default, ApiTestMain starts background processes to support regtest/dao testing, runs a few bitcoin-cli commands, then shuts down all background processes before exiting. (Actual API test suites have not been implemented yet.) ApiTestConfig options can be used to skip tests and/or leave background processes running indefinitely. --- .../main/java/bisq/apitest/ApiTestMain.java | 304 ++++++++++++++++++ .../src/main/java/bisq/apitest/SetupTask.java | 85 +++++ .../bisq/apitest/SmokeTestBashCommand.java | 51 +++ .../java/bisq/apitest/SmokeTestBitcoind.java | 72 +++++ 4 files changed, 512 insertions(+) create mode 100644 apitest/src/main/java/bisq/apitest/ApiTestMain.java create mode 100644 apitest/src/main/java/bisq/apitest/SetupTask.java create mode 100644 apitest/src/main/java/bisq/apitest/SmokeTestBashCommand.java create mode 100644 apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java diff --git a/apitest/src/main/java/bisq/apitest/ApiTestMain.java b/apitest/src/main/java/bisq/apitest/ApiTestMain.java new file mode 100644 index 00000000000..43f41e0cc69 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/ApiTestMain.java @@ -0,0 +1,304 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest; + +import bisq.common.config.BisqHelpFormatter; +import bisq.common.util.Utilities; + +import java.io.IOException; + +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.apitest.config.BisqAppConfig.*; +import static java.lang.String.format; +import static java.lang.System.err; +import static java.lang.System.exit; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.config.BisqAppConfig; +import bisq.apitest.linux.BisqApp; +import bisq.apitest.linux.BitcoinDaemon; + +/** + * ApiTest Application + * + * Requires bitcoind v0.19.1 + */ +@Slf4j +public class ApiTestMain { + + private static final int EXIT_SUCCESS = 0; + private static final int EXIT_FAILURE = 1; + + @Nullable + private SetupTask bitcoindTask; + @Nullable + private Future bitcoindTaskFuture; + @Nullable + private SetupTask seedNodeTask; + @Nullable + private Future seedNodeTaskFuture; + @Nullable + private SetupTask arbNodeTask; + @Nullable + private Future arbNodeTaskFuture; + @Nullable + private SetupTask aliceNodeTask; + @Nullable + private Future aliceNodeTaskFuture; + @Nullable + private SetupTask bobNodeTask; + @Nullable + private Future bobNodeTaskFuture; + + private ApiTestConfig config; + + public static void main(String[] args) { + new ApiTestMain().execute(args); + } + + public void execute(@SuppressWarnings("unused") String[] args) { + try { + verifyNotWindows(); + + log.info("Starting..."); + + config = new ApiTestConfig(args); + if (config.helpRequested) { + config.printHelp(System.out, + new BisqHelpFormatter( + "Bisq ApiTest", + "bisq-apitest", + "0.1.0")); + System.exit(EXIT_SUCCESS); + } + + // Start each background process from an executor, then add a shutdown hook. + ExecutorService executor = Executors.newFixedThreadPool(config.numSetupTasks); + CountDownLatch countdownLatch = new CountDownLatch(config.numSetupTasks); + startBackgroundProcesses(executor, countdownLatch); + installShutdownHook(); + + // Wait for all submitted startup tasks to decrement the count of the latch. + Objects.requireNonNull(countdownLatch).await(); + + // Verify each startup task's future is done. + verifyStartupCompleted(); + + if (config.skipTests) { + log.info("Skipping tests ..."); + } else { + log.info("Run tests now ..."); + SECONDS.sleep(3); + new SmokeTestBitcoind(config).runSmokeTest(); + } + + if (config.shutdownAfterTests) { + log.info("Shutting down executor service ..."); + executor.shutdownNow(); + executor.awaitTermination(10, SECONDS); + exit(EXIT_SUCCESS); + } else { + log.info("Not shutting down executor service ..."); + log.info("Test setup processes will run until ^C / kill -15 is rcvd ..."); + } + + } catch (Throwable ex) { + err.println("Fault: An unexpected error occurred. " + + "Please file a report at https://bisq.network/issues"); + ex.printStackTrace(err); + exit(EXIT_FAILURE); + } + } + + // Starts bitcoind and bisq apps (seednode, arbnode, etc...) + private void startBackgroundProcesses(ExecutorService executor, + CountDownLatch countdownLatch) + throws InterruptedException, IOException { + // The configured number of setup tasks determines which bisq apps are started in + // the background, and in what order. + // + // If config.numSetupTasks = 0, no setup tasks are run. If 1, the bitcoind + // process is started in the background. If 2, bitcoind and seednode. + // If 3, bitcoind, seednode and arbnode are started. If 4, bitcoind, seednode, + // arbnode, and alicenode are started. If 5, bitcoind, seednode, arbnode, + // alicenode and bobnode are started. + // + // This affords an easier way to choose which setup tasks are run, rather than + // commenting and uncommenting code blocks. You have to remember seednode + // depends on bitcoind, arbnode on seednode, and that bob & alice cannot trade + // unless arbnode is running with a registered mediator and refund agent. + if (config.numSetupTasks > 0) { + BitcoinDaemon bitcoinDaemon = new BitcoinDaemon(config); + bitcoinDaemon.verifyBitcoinConfig(true); + bitcoindTask = new SetupTask(bitcoinDaemon, countdownLatch); + bitcoindTaskFuture = executor.submit(bitcoindTask); + SECONDS.sleep(5); + bitcoinDaemon.verifyBitcoindRunning(); + } + if (config.numSetupTasks > 1) { + startBisqApp(seednode, executor, countdownLatch); + } + if (config.numSetupTasks > 2) { + startBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon, executor, countdownLatch); + } + if (config.numSetupTasks > 3) { + startBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon, executor, countdownLatch); + } + if (config.numSetupTasks > 4) { + startBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon, executor, countdownLatch); + } + } + + private void startBisqApp(BisqAppConfig bisqAppConfig, + ExecutorService executor, + CountDownLatch countdownLatch) + throws IOException, InterruptedException { + + BisqApp bisqApp; + switch (bisqAppConfig) { + case seednode: + bisqApp = createBisqApp(seednode); + seedNodeTask = new SetupTask(bisqApp, countdownLatch); + seedNodeTaskFuture = executor.submit(seedNodeTask); + break; + case arbdaemon: + case arbdesktop: + bisqApp = createBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon); + arbNodeTask = new SetupTask(bisqApp, countdownLatch); + arbNodeTaskFuture = executor.submit(arbNodeTask); + break; + case alicedaemon: + case alicedesktop: + bisqApp = createBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon); + aliceNodeTask = new SetupTask(bisqApp, countdownLatch); + aliceNodeTaskFuture = executor.submit(aliceNodeTask); + break; + case bobdaemon: + case bobdesktop: + bisqApp = createBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon); + bobNodeTask = new SetupTask(bisqApp, countdownLatch); + bobNodeTaskFuture = executor.submit(bobNodeTask); + break; + default: + throw new IllegalStateException("Unknown Bisq App " + bisqAppConfig.appName); + } + SECONDS.sleep(5); + if (bisqApp.hasStartupExceptions()) { + for (Throwable t : bisqApp.getStartupExceptions()) { + log.error("", t); + } + exit(EXIT_FAILURE); + } + } + + private BisqApp createBisqApp(BisqAppConfig bisqAppConfig) + throws IOException, InterruptedException { + BisqApp bisqNode = new BisqApp(bisqAppConfig, config); + bisqNode.verifyAppNotRunning(); + bisqNode.verifyAppDataDirInstalled(); + return bisqNode; + } + + private void verifyStartupCompleted() + throws ExecutionException, InterruptedException { + if (config.numSetupTasks > 0) + verifyStartupCompleted(bitcoindTaskFuture); + + if (config.numSetupTasks > 1) + verifyStartupCompleted(seedNodeTaskFuture); + + if (config.numSetupTasks > 2) + verifyStartupCompleted(arbNodeTaskFuture); + + if (config.numSetupTasks > 3) + verifyStartupCompleted(aliceNodeTaskFuture); + + if (config.numSetupTasks > 4) + verifyStartupCompleted(bobNodeTaskFuture); + } + + private void verifyStartupCompleted(Future futureStatus) + throws ExecutionException, InterruptedException { + for (int i = 0; i < 10; i++) { + if (futureStatus.isDone()) { + log.info("{} completed startup at {} {}", + futureStatus.get().getName(), + futureStatus.get().getStartTime().toLocalDate(), + futureStatus.get().getStartTime().toLocalTime()); + return; + } else { + // We are giving the thread more time to terminate after the countdown + // latch reached 0. If we are running only bitcoind, we need to be even + // more lenient. + SECONDS.sleep(config.numSetupTasks == 1 ? 2 : 1); + } + } + throw new IllegalStateException(format("%s did not complete startup", futureStatus.get().getName())); + } + + private void installShutdownHook() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + log.info("Running shutdown hook ..."); + if (bobNodeTask != null && bobNodeTask.getLinuxProcess() != null) + bobNodeTask.getLinuxProcess().shutdown(); + + SECONDS.sleep(3); + + if (aliceNodeTask != null && aliceNodeTask.getLinuxProcess() != null) + aliceNodeTask.getLinuxProcess().shutdown(); + + SECONDS.sleep(3); + + if (arbNodeTask != null && arbNodeTask.getLinuxProcess() != null) + arbNodeTask.getLinuxProcess().shutdown(); + + SECONDS.sleep(3); + + if (seedNodeTask != null && seedNodeTask.getLinuxProcess() != null) + seedNodeTask.getLinuxProcess().shutdown(); + + SECONDS.sleep(3); + + if (bitcoindTask != null && bitcoindTask.getLinuxProcess() != null) + bitcoindTask.getLinuxProcess().shutdown(); + + } catch (Exception ex) { + throw new RuntimeException(ex); + } + })); + } + + private void verifyNotWindows() { + if (Utilities.isWindows()) + throw new RuntimeException("ApiTest not supported on Windows"); + } +} diff --git a/apitest/src/main/java/bisq/apitest/SetupTask.java b/apitest/src/main/java/bisq/apitest/SetupTask.java new file mode 100644 index 00000000000..da81c885084 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/SetupTask.java @@ -0,0 +1,85 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest; + +import java.time.LocalDateTime; + +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; + +import lombok.extern.slf4j.Slf4j; + +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.apitest.linux.LinuxProcess; + +@Slf4j +public class SetupTask implements Callable { + + private final LinuxProcess linuxProcess; + private final CountDownLatch countdownLatch; + + public SetupTask(LinuxProcess linuxProcess, CountDownLatch countdownLatch) { + this.linuxProcess = linuxProcess; + this.countdownLatch = countdownLatch; + } + + @Override + public Status call() throws Exception { + try { + linuxProcess.start(); // always runs in background + SECONDS.sleep(10); // give time for bg process to init + } catch (InterruptedException ex) { + throw new IllegalStateException(format("Error starting %s", linuxProcess.getName()), ex); + } + Objects.requireNonNull(countdownLatch).countDown(); + return new Status(linuxProcess.getName(), LocalDateTime.now()); + } + + public LinuxProcess getLinuxProcess() { + return linuxProcess; + } + + public static class Status { + private final String name; + private final LocalDateTime startTime; + + public Status(String name, LocalDateTime startTime) { + super(); + this.name = name; + this.startTime = startTime; + } + + public String getName() { + return name; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + @Override + public String toString() { + return "SetupTask.Status [name=" + name + ", completionTime=" + startTime + "]"; + } + } +} diff --git a/apitest/src/main/java/bisq/apitest/SmokeTestBashCommand.java b/apitest/src/main/java/bisq/apitest/SmokeTestBashCommand.java new file mode 100644 index 00000000000..c3c8a26bc22 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/SmokeTestBashCommand.java @@ -0,0 +1,51 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.apitest.linux.BashCommand; + +@Slf4j +class SmokeTestBashCommand { + + public SmokeTestBashCommand() { + } + + public void runSmokeTest() { + try { + BashCommand cmd = new BashCommand("ls -l").run(); + log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); + + cmd = new BashCommand("free -g").run(); + log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); + + cmd = new BashCommand("date").run(); + log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); + + cmd = new BashCommand("netstat -a | grep localhost").run(); + log.info("$ {}\n{}", cmd.getCommand(), cmd.getOutput()); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java b/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java new file mode 100644 index 00000000000..509260763e9 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java @@ -0,0 +1,72 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import static java.lang.String.format; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.linux.BitcoinCli; + +@Slf4j +class SmokeTestBitcoind { + + private final ApiTestConfig config; + + public SmokeTestBitcoind(ApiTestConfig config) { + this.config = config; + } + + public void runSmokeTest() throws IOException, InterruptedException { + runBitcoinGetWalletInfo(); // smoke test bitcoin-cli + String newBitcoinAddress = getNewAddress(); + generateToAddress(1, newBitcoinAddress); + } + + public void runBitcoinGetWalletInfo() throws IOException, InterruptedException { + // This might be good for a sanity check to make sure the regtest data was installed. + log.info("Smoke test bitcoin-cli getwalletinfo"); + BitcoinCli walletInfo = new BitcoinCli(config, "getwalletinfo").run(); + log.info("{}\n{}", walletInfo.getCommandWithOptions(), walletInfo.getOutput()); + log.info("balance str = {}", walletInfo.getOutputValueAsString("balance")); + log.info("balance dbl = {}", walletInfo.getOutputValueAsDouble("balance")); + log.info("keypoololdest long = {}", walletInfo.getOutputValueAsLong("keypoololdest")); + log.info("paytxfee dbl = {}", walletInfo.getOutputValueAsDouble("paytxfee")); + log.info("keypoolsize_hd_internal int = {}", walletInfo.getOutputValueAsInt("keypoolsize_hd_internal")); + log.info("private_keys_enabled bool = {}", walletInfo.getOutputValueAsBoolean("private_keys_enabled")); + log.info("hdseedid str = {}", walletInfo.getOutputValueAsString("hdseedid")); + } + + public String getNewAddress() throws IOException, InterruptedException { + BitcoinCli newAddress = new BitcoinCli(config, "getnewaddress").run(); + log.info("{}\n{}", newAddress.getCommandWithOptions(), newAddress.getOutput()); + return newAddress.getOutput(); + } + + public void generateToAddress(int blocks, String address) throws IOException, InterruptedException { + String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address); + BitcoinCli newAddress = new BitcoinCli(config, generateToAddressCmd).run(); + // Return value is an array of TxIDs. + log.info("{}\n{}", newAddress.getCommandWithOptions(), newAddress.getOutputValueAsStringArray()); + } +} From e9baebc443567ad8835c68a8cae07bf3570aede2 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 9 Jul 2020 16:03:46 -0300 Subject: [PATCH 06/88] Add build tasks for installing dao-setup files This gradle file is 'applied' by the main build file. Usage: Run a full clean, build, download dao-setup.zip, and install the zip files contents in directory apitest/build/resources/main: ./gradlew clean build :apitest:installDaoSetup Download (if necessary) the dao-setup.zip file and install its contents in directory apitest/build/resources/main (no build). ./gradlew :apitest:installDaoSetup --- apitest/dao-setup.gradle | 84 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 apitest/dao-setup.gradle diff --git a/apitest/dao-setup.gradle b/apitest/dao-setup.gradle new file mode 100644 index 00000000000..e342928313f --- /dev/null +++ b/apitest/dao-setup.gradle @@ -0,0 +1,84 @@ +// This gradle file contains tasks to install and clean dao-setup files downloaded from +// https://github.com/bisq-network/bisq/raw/master/docs/dao-setup.zip +// These tasks are not run by the default build, but they can can be run during a full +// or partial builds, or by themselves. +// To run a full Bisq clean build, test, and install dao-setup files: +// ./gradlew clean build :apitest:installDaoSetup +// To install or re-install dao-setup file only: +// ./gradlew :apitest:installDaoSetup -x test +// To clean installed dao-setup files: +// ./gradlew :apitest:cleanDaoSetup -x test +// +// The :apitest subproject will not run on Windows, and these tasks have not been +// tested on Windows. +def buildResourcesDir = project(":apitest").buildDir.path + '/resources/main' + +// This task requires ant in the system $PATH. +task installDaoSetup(dependsOn: 'cleanDaoSetup') { + doLast { + println "Installing dao-setup directories in build dir $buildResourcesDir ..." + def src = 'https://github.com/bisq-network/bisq/raw/master/docs/dao-setup.zip' + def destfile = project.rootDir.path + '/apitest/src/main/resources/dao-setup.zip' + def url = new URL(src) + def f = new File(destfile) + if (f.exists()) { + println "File $destfile already exists, skipping download." + } else { + if (!f.parentFile.exists()) + mkdir "$buildResourcesDir" + + println "Downloading $url to $buildResourcesDir ..." + url.withInputStream { i -> f.withOutputStream { it << i } } + } + + // We need an ant task for unzipping the dao-setup.zip file. + println "Unzipping $destfile to $buildResourcesDir ..." + ant.unzip(src: 'src/main/resources/dao-setup.zip', + dest: 'src/main/resources', + overwrite: "true") { + // Warning: overwrite: "true" does not work if empty dirs exist, so the + // cleanDaoSetup task should be run before trying to re-install fresh + // dao-setup files. + patternset() { + include(name: '**') + exclude(name: '**/bitcoin.conf') // installed at runtime with correct blocknotify script path + exclude(name: '**/blocknotify') // installed from src/main/resources to allow port configs + } + mapper(type: "identity") + } + + // Copy files from unzip target dir 'dao-setup' to build/resources/main. + def daoSetupSrc = project.rootDir.path + '/apitest/src/main/resources/dao-setup' + def daoSetupDest = buildResourcesDir + '/dao-setup' + println "Copying $daoSetupSrc to $daoSetupDest ..." + copy { + from daoSetupSrc + into daoSetupDest + } + + // Move dao-setup files from build/resources/main/dao-setup to build/resources/main + file(buildResourcesDir + '/dao-setup/Bitcoin-regtest') + .renameTo(file(buildResourcesDir + '/Bitcoin-regtest')) + file(buildResourcesDir + '/dao-setup/bisq-BTC_REGTEST_Alice_dao') + .renameTo(file(buildResourcesDir + '/bisq-BTC_REGTEST_Alice_dao')) + file(buildResourcesDir + '/dao-setup/bisq-BTC_REGTEST_Bob_dao') + .renameTo(file(buildResourcesDir + '/bisq-BTC_REGTEST_Bob_dao')) + // delete dir build/resources/main/dao-setup + delete file(buildResourcesDir + '/dao-setup') + } +} + +task cleanDaoSetup { + doLast { + // When re-installing dao-setup files before re-running tests, the bitcoin + // datadir and dao-setup dirs have to be cleaned first. This task allows + // you to re-install dao-setup files and re-run tests without having to + // re-compile any code. + println "Deleting dao-setup directories in build dir $buildResourcesDir ..." + delete file(buildResourcesDir + '/Bitcoin-regtest') + delete file(buildResourcesDir + '/bisq-BTC_REGTEST_Seed_2002') + delete file(buildResourcesDir + '/bisq-BTC_REGTEST_Arb_dao') + delete file(buildResourcesDir + '/bisq-BTC_REGTEST_Alice_dao') + delete file(buildResourcesDir + '/bisq-BTC_REGTEST_Bob_dao') + } +} From 2cd80aaa6016de83840fd2e80020697d0006ec0f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 9 Jul 2020 16:15:29 -0300 Subject: [PATCH 07/88] Add subproject :apitest to gradle build file --- build.gradle | 65 ++++++++++++++++++++++++++++++++++++++++++++++++- settings.gradle | 1 + 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8e83abb1830..f455a86eb9b 100644 --- a/build.gradle +++ b/build.gradle @@ -98,7 +98,8 @@ configure([project(':cli'), project(':relay'), project(':seednode'), project(':statsnode'), - project(':pricenode')]) { + project(':pricenode'), + project(':apitest')]) { apply plugin: 'application' @@ -143,6 +144,16 @@ configure([project(':cli'), 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g"') } + if (applicationName == 'apitest') { + // Give apitest more MaxRAM, and pass the logback config file as a system + // property to avoid chatty logback startup due to multiple logback.xml + // files in the classpath (:daemon & :cli). + def script = file("${rootProject.projectDir}/bisq-$applicationName") + script.text = script.text.replace( + 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=6g' + + ' -Dlogback.configurationFile=apitest/build/resources/main/logback.xml"') + } + if (osdetector.os != 'windows') delete fileTree(dir: rootProject.projectDir, include: 'bisq-*.bat') else @@ -551,3 +562,55 @@ configure(project(':daemon')) { annotationProcessor "org.projectlombok:lombok:$lombokVersion" } } + +configure(project(':apitest')) { + mainClassName = 'bisq.apitest.ApiTestMain' + + // The external dao-setup.gradle file contains tasks to install and clean dao-setup + // files downloaded from + // https://github.com/bisq-network/bisq/raw/master/docs/dao-setup.zip + // These tasks are not run by the default build, but they can can be run during + // full or partial builds, or by themselves. + // To run a full Bisq clean build, test, and install dao-setup files: + // ./gradlew clean build :apitest:installDaoSetup + // To install or re-install dao-setup file only: + // ./gradlew :apitest:installDaoSetup -x test + // To clean installed dao-setup files: + // ./gradlew :apitest:cleanDaoSetup -x test + apply from: 'dao-setup.gradle' + + sourceSets { + main { + resources { + exclude 'dao-setup' + exclude 'dao-setup.zip' + } + } + } + + dependencies { + compile project(':common') + compile project(':seednode') + compile project(':desktop') + compile project(':daemon') + compile project(':cli') + implementation "net.sf.jopt-simple:jopt-simple:$joptVersion" + implementation "com.google.guava:guava:$guavaVersion" + implementation "com.google.protobuf:protobuf-java:$protobufVersion" + implementation("io.grpc:grpc-protobuf:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation("io.grpc:grpc-stub:$grpcVersion") { + exclude(module: 'guava') + exclude(module: 'animal-sniffer-annotations') + } + implementation "org.slf4j:slf4j-api:$slf4jVersion" + implementation "ch.qos.logback:logback-core:$logbackVersion" + implementation "ch.qos.logback:logback-classic:$logbackVersion" + compileOnly "org.projectlombok:lombok:$lombokVersion" + compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + } +} + diff --git a/settings.gradle b/settings.gradle index cdfa7c7126a..45ca5993367 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,5 +11,6 @@ include 'pricenode' include 'relay' include 'seednode' include 'statsnode' +include 'apitest' rootProject.name = 'bisq' From 09929c833f6e95e14d84cc1115605339c829c13b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 9 Jul 2020 16:49:16 -0300 Subject: [PATCH 08/88] Fix codacy problems Unnecessary use of fully qualified name 'System.exit' due to existing static import 'java.lang.System.exit'. (line 100) Avoid throwing raw exception types. (lines 295, 302) --- apitest/src/main/java/bisq/apitest/ApiTestMain.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/ApiTestMain.java b/apitest/src/main/java/bisq/apitest/ApiTestMain.java index 43f41e0cc69..fdbf304b657 100644 --- a/apitest/src/main/java/bisq/apitest/ApiTestMain.java +++ b/apitest/src/main/java/bisq/apitest/ApiTestMain.java @@ -37,6 +37,7 @@ import static java.lang.String.format; import static java.lang.System.err; import static java.lang.System.exit; +import static java.lang.System.out; import static java.util.concurrent.TimeUnit.SECONDS; @@ -92,12 +93,12 @@ public void execute(@SuppressWarnings("unused") String[] args) { config = new ApiTestConfig(args); if (config.helpRequested) { - config.printHelp(System.out, + config.printHelp(out, new BisqHelpFormatter( "Bisq ApiTest", "bisq-apitest", "0.1.0")); - System.exit(EXIT_SUCCESS); + exit(EXIT_SUCCESS); } // Start each background process from an executor, then add a shutdown hook. @@ -292,13 +293,13 @@ private void installShutdownHook() { bitcoindTask.getLinuxProcess().shutdown(); } catch (Exception ex) { - throw new RuntimeException(ex); + throw new IllegalStateException(ex); } })); } private void verifyNotWindows() { if (Utilities.isWindows()) - throw new RuntimeException("ApiTest not supported on Windows"); + throw new IllegalStateException("ApiTest not supported on Windows"); } } From efbaa5be732b17b52c7d08c34d429a7fc70daf15 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 9 Jul 2020 16:54:03 -0300 Subject: [PATCH 09/88] Fix codacy problems Avoid throwing raw exception types. Combine nested if statements. --- .../java/bisq/apitest/config/ApiTestConfig.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index 9d2bfea4dea..033a050023a 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -299,7 +299,7 @@ public ApiTestConfig(String... args) { installBitcoinBlocknotify(); } catch (OptionException ex) { - throw new RuntimeException(format("Problem parsing option '%s': %s", + throw new IllegalStateException(format("Problem parsing option '%s': %s", ex.options().get(0), ex.getCause() != null ? ex.getCause().getMessage() : @@ -351,10 +351,8 @@ private void installBitcoinBlocknotify() { } private Optional parseOptionsFrom(File configFile, OptionSpec[] disallowedOpts) { - if (!configFile.exists()) { - if (!configFile.equals(absoluteConfigFile(userDir, DEFAULT_CONFIG_FILE_NAME))) - throw new RuntimeException(format("The specified config file '%s' does not exist.", configFile)); - } + if (!configFile.exists() && !configFile.equals(absoluteConfigFile(userDir, DEFAULT_CONFIG_FILE_NAME))) + throw new IllegalStateException(format("The specified config file '%s' does not exist.", configFile)); Properties properties = getProperties(configFile); List optionLines = new ArrayList<>(); @@ -365,7 +363,7 @@ private Optional parseOptionsFrom(File configFile, OptionSpec[] di OptionSet configFileOpts = parser.parse(optionLines.toArray(new String[0])); for (OptionSpec disallowedOpt : disallowedOpts) if (configFileOpts.has(disallowedOpt)) - throw new RuntimeException( + throw new IllegalStateException( format("The '%s' option is disallowed in config files", disallowedOpt.options().get(0))); @@ -378,7 +376,7 @@ private Properties getProperties(File configFile) { properties.load(new FileInputStream(configFile.getAbsolutePath())); return properties; } catch (IOException ex) { - throw new RuntimeException( + throw new IllegalStateException( format("Could not load properties from config file %s", configFile.getAbsolutePath()), ex); } @@ -400,7 +398,7 @@ private void saveToFile(String content, FileUtil.renameFile(tempFile, file); Files.setPosixFilePermissions(Paths.get(file.toURI()), PosixFilePermissions.fromString(posixFilePermissions)); } catch (IOException ex) { - throw new RuntimeException(format("Error saving %s/%s to disk", parentDir, relativeConfigFilePath), ex); + throw new IllegalStateException(format("Error saving %s/%s to disk", parentDir, relativeConfigFilePath), ex); } finally { if (tempFile != null && tempFile.exists()) { log.warn("Temp file still exists after failed save; deleting {} now.", tempFile.getAbsolutePath()); From 05d0ce0ef8a22bd55988f8695f87bcb2afc7d0d8 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 9 Jul 2020 16:56:09 -0300 Subject: [PATCH 10/88] Fix codacy problem Avoid throwing null pointer exceptions. --- .../src/main/java/bisq/apitest/linux/SystemCommandExecutor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java index ece5df1e1c9..3aa26624265 100644 --- a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java +++ b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java @@ -66,7 +66,7 @@ class SystemCommandExecutor { */ public SystemCommandExecutor(final List cmdOptions) { if (cmdOptions == null) - throw new NullPointerException("No command params specified."); + throw new IllegalStateException("No command params specified."); this.cmdOptions = cmdOptions; this.sudoPassword = null; From ca378cdd132b6482fa3c22fdc10ba20a4a9f2279 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 10 Jul 2020 14:24:35 -0300 Subject: [PATCH 11/88] Fix error msg spacing --- .../java/bisq/apitest/linux/AbstractLinuxProcess.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index 19391898b52..0ab6630f436 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -64,23 +64,23 @@ public void verifyBitcoinConfig(boolean verbose) { File berkeleyDbLibPath = new File(config.berkeleyDbLibPath); if (!berkeleyDbLibPath.exists() || !berkeleyDbLibPath.canExecute()) - throw new IllegalStateException(berkeleyDbLibPath + "cannot be found or executed"); + throw new IllegalStateException(berkeleyDbLibPath + " cannot be found or executed"); File bitcoindExecutable = new File(config.bitcoinPath); if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute()) - throw new IllegalStateException(bitcoindExecutable + "cannot be found or executed"); + throw new IllegalStateException(bitcoindExecutable + " cannot be found or executed"); File bitcoindDatadir = new File(config.bitcoinDatadir); if (!bitcoindDatadir.exists() || !bitcoindDatadir.canWrite()) - throw new IllegalStateException(bitcoindDatadir + "cannot be found or written to"); + throw new IllegalStateException(bitcoindDatadir + " cannot be found or written to"); File bitcoinConf = new File(bitcoindDatadir, "bitcoin.conf"); if (!bitcoinConf.exists() || !bitcoinConf.canRead()) - throw new IllegalStateException(bitcoindDatadir + "cannot be found or read"); + throw new IllegalStateException(bitcoindDatadir + " cannot be found or read"); File blocknotify = new File(bitcoindDatadir, "blocknotify"); if (!blocknotify.exists() || !blocknotify.canExecute()) - throw new IllegalStateException(bitcoindDatadir + "cannot be found or executed"); + throw new IllegalStateException(bitcoindDatadir + " cannot be found or executed"); } public void verifyBitcoindRunning() throws IOException, InterruptedException { From 798fde847b23bd6cfda452ebe51d7bf000cdd984 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 10 Jul 2020 14:27:25 -0300 Subject: [PATCH 12/88] Fix error msgs --- .../main/java/bisq/apitest/linux/AbstractLinuxProcess.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index 0ab6630f436..a2559abb2ec 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -76,11 +76,11 @@ public void verifyBitcoinConfig(boolean verbose) { File bitcoinConf = new File(bitcoindDatadir, "bitcoin.conf"); if (!bitcoinConf.exists() || !bitcoinConf.canRead()) - throw new IllegalStateException(bitcoindDatadir + " cannot be found or read"); + throw new IllegalStateException(bitcoindDatadir + "/bitcoin.conf cannot be found or read"); File blocknotify = new File(bitcoindDatadir, "blocknotify"); if (!blocknotify.exists() || !blocknotify.canExecute()) - throw new IllegalStateException(bitcoindDatadir + " cannot be found or executed"); + throw new IllegalStateException(bitcoindDatadir + "/blocknotify cannot be found or executed"); } public void verifyBitcoindRunning() throws IOException, InterruptedException { From e61a42dbb44bdacaa15d3ee8e4f9f7720ab7ddf2 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 10 Jul 2020 15:00:50 -0300 Subject: [PATCH 13/88] Remove MaxRAM from DEFAULT_JVM_OPTS There is no need for this as any JavaFX based :desktop instances will be started as background linux processes. --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index f455a86eb9b..6c379b748a2 100644 --- a/build.gradle +++ b/build.gradle @@ -145,13 +145,13 @@ configure([project(':cli'), } if (applicationName == 'apitest') { - // Give apitest more MaxRAM, and pass the logback config file as a system - // property to avoid chatty logback startup due to multiple logback.xml - // files in the classpath (:daemon & :cli). + // Pass the logback config file as a system property to avoid chatty + // logback startup due to multiple logback.xml files in the classpath + // (:daemon & :cli). def script = file("${rootProject.projectDir}/bisq-$applicationName") script.text = script.text.replace( - 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=6g' + - ' -Dlogback.configurationFile=apitest/build/resources/main/logback.xml"') + 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="' + + '-Dlogback.configurationFile=apitest/build/resources/main/logback.xml"') } if (osdetector.os != 'windows') From 898219aba5671715020f877451adcaebbca8ef5e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 11 Jul 2020 10:26:26 -0300 Subject: [PATCH 14/88] Assume bitcoin-core is statically linked to berkeley-db The `berkeleyDbLibPath` option now defaults to an empty string. If not set to a berkeley lib path on the command line or the apitest.properties file, this option is ignored, and 'bitcoind' will be started without first exporting the berkeley db library path. In other words: If the bitcoind binary is dynamically linked to berkeley db libs, export the configured berkeley-db lib path before starting 'bitcoind'. If statically linked, the berkeley db lib path will not be exported. Also fixed exception msgs to show missing config file's absolute path. --- .../java/bisq/apitest/config/ApiTestConfig.java | 3 ++- .../bisq/apitest/linux/AbstractLinuxProcess.java | 14 +++++++++----- .../java/bisq/apitest/linux/BitcoinDaemon.java | 12 ++++++++++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index 033a050023a..0d80a69716b 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -50,6 +50,7 @@ import static java.lang.String.format; import static java.lang.System.getProperty; import static java.lang.System.getenv; +import static joptsimple.internal.Strings.EMPTY; @Slf4j public class ApiTestConfig { @@ -151,7 +152,7 @@ public ApiTestConfig(String... args) { ArgumentAcceptingOptionSpec berkeleyDbLibPathOpt = parser.accepts(BERKELEYDB_LIB_PATH, "Berkeley DB lib path") .withRequiredArg() - .ofType(String.class).defaultsTo("/usr/local/BerkeleyDB.4.8/lib"); + .ofType(String.class).defaultsTo(EMPTY); ArgumentAcceptingOptionSpec bitcoinPathOpt = parser.accepts(BITCOIN_PATH, "Bitcoin path") diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index a2559abb2ec..018aba40f73 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -24,6 +24,7 @@ import static bisq.apitest.linux.BashCommand.isAlive; import static java.lang.String.format; +import static joptsimple.internal.Strings.EMPTY; @@ -48,6 +49,7 @@ public String getName() { return this.name; } + @SuppressWarnings("unused") public void verifyBitcoinConfig() { verifyBitcoinConfig(false); } @@ -62,9 +64,11 @@ public void verifyBitcoinConfig(boolean verbose) { "bitcoin.conf", config.bitcoinDatadir + "/bitcoin.conf", "blocknotify", config.bitcoinDatadir + "/blocknotify")); - File berkeleyDbLibPath = new File(config.berkeleyDbLibPath); - if (!berkeleyDbLibPath.exists() || !berkeleyDbLibPath.canExecute()) - throw new IllegalStateException(berkeleyDbLibPath + " cannot be found or executed"); + if (!config.berkeleyDbLibPath.equals(EMPTY)) { + File berkeleyDbLibPath = new File(config.berkeleyDbLibPath); + if (!berkeleyDbLibPath.exists() || !berkeleyDbLibPath.canExecute()) + throw new IllegalStateException(berkeleyDbLibPath + " cannot be found or executed"); + } File bitcoindExecutable = new File(config.bitcoinPath); if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute()) @@ -76,11 +80,11 @@ public void verifyBitcoinConfig(boolean verbose) { File bitcoinConf = new File(bitcoindDatadir, "bitcoin.conf"); if (!bitcoinConf.exists() || !bitcoinConf.canRead()) - throw new IllegalStateException(bitcoindDatadir + "/bitcoin.conf cannot be found or read"); + throw new IllegalStateException(bitcoinConf.getAbsolutePath() + " cannot be found or read"); File blocknotify = new File(bitcoindDatadir, "blocknotify"); if (!blocknotify.exists() || !blocknotify.canExecute()) - throw new IllegalStateException(bitcoindDatadir + "/blocknotify cannot be found or executed"); + throw new IllegalStateException(blocknotify.getAbsolutePath() + " cannot be found or executed"); } public void verifyBitcoindRunning() throws IOException, InterruptedException { diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java index ac958431a46..bb29b630944 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java @@ -23,6 +23,7 @@ import static bisq.apitest.linux.BashCommand.isAlive; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static joptsimple.internal.Strings.EMPTY; @@ -44,8 +45,15 @@ public BitcoinDaemon(ApiTestConfig config) { @Override public void start() throws InterruptedException, IOException { - String bitcoindCmd = "export LD_LIBRARY_PATH=" + config.berkeleyDbLibPath + ";" - + " " + config.bitcoinPath + "/bitcoind" + + // If the bitcoind binary is dynamically linked to berkeley db libs, export the + // configured berkeley-db lib path. If statically linked, the berkeley db lib + // path will not be exported. + String berkeleyDbLibPathExport = config.berkeleyDbLibPath.equals(EMPTY) ? EMPTY + : "export LD_LIBRARY_PATH=" + config.berkeleyDbLibPath + "; "; + + String bitcoindCmd = berkeleyDbLibPathExport + + config.bitcoinPath + "/bitcoind" + " -datadir=" + config.bitcoinDatadir + " -daemon"; From a1e2536ed5822997d116e5ae3576053ec0dd6eb6 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 11 Jul 2020 18:15:17 -0300 Subject: [PATCH 15/88] Fix hanging background process problem SetupTask submissions for Bisq background apps seednode, arbnode, etc., would not always complete due to a blocking stderr stream handler thread.join() call. This change makes waiting on a bash process err stream optional. --- .../java/bisq/apitest/linux/BashCommand.java | 16 +++++++++++++--- .../main/java/bisq/apitest/linux/BisqApp.java | 2 +- .../apitest/linux/SystemCommandExecutor.java | 15 +++++++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/BashCommand.java b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java index 2eaa0b9f8e4..725e325ea7c 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BashCommand.java +++ b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java @@ -53,15 +53,26 @@ public BashCommand(String command, int numResponseLines) { public BashCommand run() throws IOException, InterruptedException { SystemCommandExecutor commandExecutor = new SystemCommandExecutor(tokenizeSystemCommand()); - exitStatus = commandExecutor.execCommand(); + exitStatus = commandExecutor.exec(); + processOutput(commandExecutor); + return this; + } + public BashCommand runInBackground() throws IOException, InterruptedException { + SystemCommandExecutor commandExecutor = new SystemCommandExecutor(tokenizeSystemCommand()); + exitStatus = commandExecutor.exec(false); + processOutput(commandExecutor); + return this; + } + + private void processOutput(SystemCommandExecutor commandExecutor) throws IOException, InterruptedException { // Get the error status and stderr from system command. StringBuilder stderr = commandExecutor.getStandardErrorFromCommand(); if (stderr.length() > 0) error = stderr.toString(); if (exitStatus != 0) - return this; + return; // Format and cache the stdout from system command. StringBuilder stdout = commandExecutor.getStandardOutputFromCommand(); @@ -73,7 +84,6 @@ public BashCommand run() throws IOException, InterruptedException { truncatedLines.append(line).append((i < limit - 1) ? "\n" : ""); } output = truncatedLines.toString(); - return this; } public String getCommand() { diff --git a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java index 2ed3d3540f0..651766efd57 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java +++ b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java @@ -173,7 +173,7 @@ private void runBashCommand(String bisqCmd) throws IOException, InterruptedExcep : bisqAppConfig.startupScript + " -> " + bisqAppConfig.appName; BashCommand bashCommand = new BashCommand(bisqCmd); log.info("Starting {} ...\n$ {}", cmdDescription, bashCommand.getCommand()); - bashCommand.run(); + bashCommand.runInBackground(); if (bashCommand.getExitStatus() != 0) throw new IllegalStateException(format("Error starting BisqApp\n%s\nError: %s", diff --git a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java index 3aa26624265..ee6b4711a78 100644 --- a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java +++ b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java @@ -74,7 +74,15 @@ public SystemCommandExecutor(final List cmdOptions) { // Execute a system command and return its status code (0 or 1). // The system command's output (stderr or stdout) can be accessed from accessors. - public int execCommand() throws IOException, InterruptedException { + public int exec() throws IOException, InterruptedException { + return exec(true); + } + + // Execute a system command and return its status code (0 or 1). + // The system command's output (stderr or stdout) can be accessed from accessors + // if the waitOnErrStream flag is true, else the method will not wait on (join) + // the error stream handler thread. + public int exec(boolean waitOnErrStream) throws IOException, InterruptedException { Process process = new ProcessBuilder(cmdOptions).start(); // you need this if you're going to write something to the command's input stream @@ -101,8 +109,11 @@ public int execCommand() throws IOException, InterruptedException { inputStreamHandler.interrupt(); errorStreamHandler.interrupt(); + inputStreamHandler.join(); - errorStreamHandler.join(); + if (waitOnErrStream) + errorStreamHandler.join(); + return exitStatus; } From 851902117d238939f633a1dcb38a47ecc5958d33 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 13:23:53 -0300 Subject: [PATCH 16/88] Add line break in front of port config --- apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java b/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java index 30046d7262d..662956462ed 100644 --- a/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/BisqAppConfig.java @@ -80,7 +80,8 @@ public enum BisqAppConfig { "\"-XX:MaxRAM=4g -Dlogback.configurationFile=apitest/build/resources/main/logback.xml\"", BisqAppMain.class.getName(), 8888, - 5123, -1); + 5123, + -1); public final String appName; public final String startupScript; From 87525ca85db342c4e2db5b59e684694055d652db Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 13:36:10 -0300 Subject: [PATCH 17/88] Move test setup scaffolding into new Scaffold class ApiTestMain will run all defined tests, but we also want to run individual test suites and test cases, and they will need to run the setup tasks as well. --- .../main/java/bisq/apitest/ApiTestMain.java | 255 +--------------- .../src/main/java/bisq/apitest/Scaffold.java | 271 ++++++++++++++++++ .../java/bisq/apitest/SmokeTestBitcoind.java | 2 +- 3 files changed, 282 insertions(+), 246 deletions(-) create mode 100644 apitest/src/main/java/bisq/apitest/Scaffold.java diff --git a/apitest/src/main/java/bisq/apitest/ApiTestMain.java b/apitest/src/main/java/bisq/apitest/ApiTestMain.java index fdbf304b657..ea58c33e837 100644 --- a/apitest/src/main/java/bisq/apitest/ApiTestMain.java +++ b/apitest/src/main/java/bisq/apitest/ApiTestMain.java @@ -17,118 +17,47 @@ package bisq.apitest; -import bisq.common.config.BisqHelpFormatter; -import bisq.common.util.Utilities; - -import java.io.IOException; - -import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - import lombok.extern.slf4j.Slf4j; -import javax.annotation.Nullable; - -import static bisq.apitest.config.BisqAppConfig.*; -import static java.lang.String.format; +import static bisq.apitest.Scaffold.EXIT_FAILURE; +import static bisq.apitest.Scaffold.EXIT_SUCCESS; import static java.lang.System.err; import static java.lang.System.exit; -import static java.lang.System.out; -import static java.util.concurrent.TimeUnit.SECONDS; import bisq.apitest.config.ApiTestConfig; -import bisq.apitest.config.BisqAppConfig; -import bisq.apitest.linux.BisqApp; -import bisq.apitest.linux.BitcoinDaemon; /** * ApiTest Application * - * Requires bitcoind v0.19.1 + * Runs all method tests, scenario tests, and end to end tests (e2e). + * + * Requires bitcoind v0.19.x */ @Slf4j public class ApiTestMain { - private static final int EXIT_SUCCESS = 0; - private static final int EXIT_FAILURE = 1; - - @Nullable - private SetupTask bitcoindTask; - @Nullable - private Future bitcoindTaskFuture; - @Nullable - private SetupTask seedNodeTask; - @Nullable - private Future seedNodeTaskFuture; - @Nullable - private SetupTask arbNodeTask; - @Nullable - private Future arbNodeTaskFuture; - @Nullable - private SetupTask aliceNodeTask; - @Nullable - private Future aliceNodeTaskFuture; - @Nullable - private SetupTask bobNodeTask; - @Nullable - private Future bobNodeTaskFuture; - - private ApiTestConfig config; - public static void main(String[] args) { new ApiTestMain().execute(args); } public void execute(@SuppressWarnings("unused") String[] args) { try { - verifyNotWindows(); - - log.info("Starting..."); - - config = new ApiTestConfig(args); - if (config.helpRequested) { - config.printHelp(out, - new BisqHelpFormatter( - "Bisq ApiTest", - "bisq-apitest", - "0.1.0")); - exit(EXIT_SUCCESS); - } - - // Start each background process from an executor, then add a shutdown hook. - ExecutorService executor = Executors.newFixedThreadPool(config.numSetupTasks); - CountDownLatch countdownLatch = new CountDownLatch(config.numSetupTasks); - startBackgroundProcesses(executor, countdownLatch); - installShutdownHook(); - - // Wait for all submitted startup tasks to decrement the count of the latch. - Objects.requireNonNull(countdownLatch).await(); - - // Verify each startup task's future is done. - verifyStartupCompleted(); + Scaffold scaffold = new Scaffold(args).setUp(); + ApiTestConfig config = scaffold.config; if (config.skipTests) { log.info("Skipping tests ..."); } else { - log.info("Run tests now ..."); - SECONDS.sleep(3); - new SmokeTestBitcoind(config).runSmokeTest(); + new SmokeTestBitcoind(config).run(); } if (config.shutdownAfterTests) { - log.info("Shutting down executor service ..."); - executor.shutdownNow(); - executor.awaitTermination(10, SECONDS); + scaffold.tearDown(); exit(EXIT_SUCCESS); } else { - log.info("Not shutting down executor service ..."); - log.info("Test setup processes will run until ^C / kill -15 is rcvd ..."); + log.info("Not shutting down scaffolding background processes will run until ^C / kill -15 is rcvd ..."); } } catch (Throwable ex) { @@ -138,168 +67,4 @@ public void execute(@SuppressWarnings("unused") String[] args) { exit(EXIT_FAILURE); } } - - // Starts bitcoind and bisq apps (seednode, arbnode, etc...) - private void startBackgroundProcesses(ExecutorService executor, - CountDownLatch countdownLatch) - throws InterruptedException, IOException { - // The configured number of setup tasks determines which bisq apps are started in - // the background, and in what order. - // - // If config.numSetupTasks = 0, no setup tasks are run. If 1, the bitcoind - // process is started in the background. If 2, bitcoind and seednode. - // If 3, bitcoind, seednode and arbnode are started. If 4, bitcoind, seednode, - // arbnode, and alicenode are started. If 5, bitcoind, seednode, arbnode, - // alicenode and bobnode are started. - // - // This affords an easier way to choose which setup tasks are run, rather than - // commenting and uncommenting code blocks. You have to remember seednode - // depends on bitcoind, arbnode on seednode, and that bob & alice cannot trade - // unless arbnode is running with a registered mediator and refund agent. - if (config.numSetupTasks > 0) { - BitcoinDaemon bitcoinDaemon = new BitcoinDaemon(config); - bitcoinDaemon.verifyBitcoinConfig(true); - bitcoindTask = new SetupTask(bitcoinDaemon, countdownLatch); - bitcoindTaskFuture = executor.submit(bitcoindTask); - SECONDS.sleep(5); - bitcoinDaemon.verifyBitcoindRunning(); - } - if (config.numSetupTasks > 1) { - startBisqApp(seednode, executor, countdownLatch); - } - if (config.numSetupTasks > 2) { - startBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon, executor, countdownLatch); - } - if (config.numSetupTasks > 3) { - startBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon, executor, countdownLatch); - } - if (config.numSetupTasks > 4) { - startBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon, executor, countdownLatch); - } - } - - private void startBisqApp(BisqAppConfig bisqAppConfig, - ExecutorService executor, - CountDownLatch countdownLatch) - throws IOException, InterruptedException { - - BisqApp bisqApp; - switch (bisqAppConfig) { - case seednode: - bisqApp = createBisqApp(seednode); - seedNodeTask = new SetupTask(bisqApp, countdownLatch); - seedNodeTaskFuture = executor.submit(seedNodeTask); - break; - case arbdaemon: - case arbdesktop: - bisqApp = createBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon); - arbNodeTask = new SetupTask(bisqApp, countdownLatch); - arbNodeTaskFuture = executor.submit(arbNodeTask); - break; - case alicedaemon: - case alicedesktop: - bisqApp = createBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon); - aliceNodeTask = new SetupTask(bisqApp, countdownLatch); - aliceNodeTaskFuture = executor.submit(aliceNodeTask); - break; - case bobdaemon: - case bobdesktop: - bisqApp = createBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon); - bobNodeTask = new SetupTask(bisqApp, countdownLatch); - bobNodeTaskFuture = executor.submit(bobNodeTask); - break; - default: - throw new IllegalStateException("Unknown Bisq App " + bisqAppConfig.appName); - } - SECONDS.sleep(5); - if (bisqApp.hasStartupExceptions()) { - for (Throwable t : bisqApp.getStartupExceptions()) { - log.error("", t); - } - exit(EXIT_FAILURE); - } - } - - private BisqApp createBisqApp(BisqAppConfig bisqAppConfig) - throws IOException, InterruptedException { - BisqApp bisqNode = new BisqApp(bisqAppConfig, config); - bisqNode.verifyAppNotRunning(); - bisqNode.verifyAppDataDirInstalled(); - return bisqNode; - } - - private void verifyStartupCompleted() - throws ExecutionException, InterruptedException { - if (config.numSetupTasks > 0) - verifyStartupCompleted(bitcoindTaskFuture); - - if (config.numSetupTasks > 1) - verifyStartupCompleted(seedNodeTaskFuture); - - if (config.numSetupTasks > 2) - verifyStartupCompleted(arbNodeTaskFuture); - - if (config.numSetupTasks > 3) - verifyStartupCompleted(aliceNodeTaskFuture); - - if (config.numSetupTasks > 4) - verifyStartupCompleted(bobNodeTaskFuture); - } - - private void verifyStartupCompleted(Future futureStatus) - throws ExecutionException, InterruptedException { - for (int i = 0; i < 10; i++) { - if (futureStatus.isDone()) { - log.info("{} completed startup at {} {}", - futureStatus.get().getName(), - futureStatus.get().getStartTime().toLocalDate(), - futureStatus.get().getStartTime().toLocalTime()); - return; - } else { - // We are giving the thread more time to terminate after the countdown - // latch reached 0. If we are running only bitcoind, we need to be even - // more lenient. - SECONDS.sleep(config.numSetupTasks == 1 ? 2 : 1); - } - } - throw new IllegalStateException(format("%s did not complete startup", futureStatus.get().getName())); - } - - private void installShutdownHook() { - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { - log.info("Running shutdown hook ..."); - if (bobNodeTask != null && bobNodeTask.getLinuxProcess() != null) - bobNodeTask.getLinuxProcess().shutdown(); - - SECONDS.sleep(3); - - if (aliceNodeTask != null && aliceNodeTask.getLinuxProcess() != null) - aliceNodeTask.getLinuxProcess().shutdown(); - - SECONDS.sleep(3); - - if (arbNodeTask != null && arbNodeTask.getLinuxProcess() != null) - arbNodeTask.getLinuxProcess().shutdown(); - - SECONDS.sleep(3); - - if (seedNodeTask != null && seedNodeTask.getLinuxProcess() != null) - seedNodeTask.getLinuxProcess().shutdown(); - - SECONDS.sleep(3); - - if (bitcoindTask != null && bitcoindTask.getLinuxProcess() != null) - bitcoindTask.getLinuxProcess().shutdown(); - - } catch (Exception ex) { - throw new IllegalStateException(ex); - } - })); - } - - private void verifyNotWindows() { - if (Utilities.isWindows()) - throw new IllegalStateException("ApiTest not supported on Windows"); - } } diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java new file mode 100644 index 00000000000..f1b2b68539b --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -0,0 +1,271 @@ +package bisq.apitest; + +import bisq.common.config.BisqHelpFormatter; +import bisq.common.util.Utilities; + +import java.io.IOException; + +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.apitest.config.BisqAppConfig.*; +import static java.lang.String.format; +import static java.lang.System.err; +import static java.lang.System.exit; +import static java.lang.System.out; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.config.BisqAppConfig; +import bisq.apitest.linux.BisqApp; +import bisq.apitest.linux.BitcoinDaemon; + +@Slf4j +public class Scaffold { + + public static final int EXIT_SUCCESS = 0; + public static final int EXIT_FAILURE = 1; + + final ApiTestConfig config; + + @Nullable + private SetupTask bitcoindTask; + @Nullable + private Future bitcoindTaskFuture; + @Nullable + private SetupTask seedNodeTask; + @Nullable + private Future seedNodeTaskFuture; + @Nullable + private SetupTask arbNodeTask; + @Nullable + private Future arbNodeTaskFuture; + @Nullable + private SetupTask aliceNodeTask; + @Nullable + private Future aliceNodeTaskFuture; + @Nullable + private SetupTask bobNodeTask; + @Nullable + private Future bobNodeTaskFuture; + + private final ExecutorService executor; + + public Scaffold(String[] args) { + this(new ApiTestConfig(args)); + } + + public Scaffold(ApiTestConfig config) { + verifyNotWindows(); + this.config = config; + this.executor = Executors.newFixedThreadPool(config.numSetupTasks); + if (config.helpRequested) { + config.printHelp(out, + new BisqHelpFormatter( + "Bisq ApiTest", + "bisq-apitest", + "0.1.0")); + exit(EXIT_SUCCESS); + } + } + + + public Scaffold setUp() { + try { + // Start each background process from an executor, then add a shutdown hook. + CountDownLatch countdownLatch = new CountDownLatch(config.numSetupTasks); + startBackgroundProcesses(executor, countdownLatch); + installShutdownHook(); + + // Wait for all submitted startup tasks to decrement the count of the latch. + Objects.requireNonNull(countdownLatch).await(); + + // Verify each startup task's future is done. + verifyStartupCompleted(); + + } catch (Throwable ex) { + err.println("Fault: An unexpected error occurred. " + + "Please file a report at https://bisq.network/issues"); + ex.printStackTrace(err); + exit(EXIT_FAILURE); + } + return this; + } + + public ApiTestConfig getConfig() { + return config; + } + + public void tearDown() { + if (!executor.isTerminated()) { + try { + log.info("Shutting down executor service ..."); + executor.shutdownNow(); + executor.awaitTermination(config.numSetupTasks * 2000, MILLISECONDS); + SetupTask[] orderedTasks = new SetupTask[]{bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask}; + Arrays.stream(orderedTasks).filter(t -> t != null && t.getLinuxProcess() != null).forEachOrdered(t -> { + try { + t.getLinuxProcess().shutdown(); + MILLISECONDS.sleep(1500); + } catch (IOException | InterruptedException ex) { + throw new IllegalStateException(ex); + } + }); + log.info("Teardown complete"); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + } + + private void installShutdownHook() { + // A test may shut down background apps, or they may be left running until + // the jvm is manually shutdown, so we add a shutdown hook for that use case. + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + tearDown(); + })); + } + + // Starts bitcoind and bisq apps (seednode, arbnode, etc...) + private void startBackgroundProcesses(ExecutorService executor, + CountDownLatch countdownLatch) + throws InterruptedException, IOException { + // The configured number of setup tasks determines which bisq apps are started in + // the background, and in what order. + // + // If config.numSetupTasks = 0, no setup tasks are run. If 1, the bitcoind + // process is started in the background. If 2, bitcoind and seednode. + // If 3, bitcoind, seednode and arbnode are started. If 4, bitcoind, seednode, + // arbnode, and alicenode are started. If 5, bitcoind, seednode, arbnode, + // alicenode and bobnode are started. + // + // This affords an easier way to choose which setup tasks are run, rather than + // commenting and uncommenting code blocks. You have to remember seednode + // depends on bitcoind, arbnode on seednode, and that bob & alice cannot trade + // unless arbnode is running with a registered mediator and refund agent. + if (config.numSetupTasks > 0) { + BitcoinDaemon bitcoinDaemon = new BitcoinDaemon(config); + bitcoinDaemon.verifyBitcoinConfig(true); + bitcoindTask = new SetupTask(bitcoinDaemon, countdownLatch); + bitcoindTaskFuture = executor.submit(bitcoindTask); + SECONDS.sleep(5); + bitcoinDaemon.verifyBitcoindRunning(); + } + if (config.numSetupTasks > 1) { + startBisqApp(seednode, executor, countdownLatch); + } + if (config.numSetupTasks > 2) { + startBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon, executor, countdownLatch); + } + if (config.numSetupTasks > 3) { + startBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon, executor, countdownLatch); + } + if (config.numSetupTasks > 4) { + startBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon, executor, countdownLatch); + } + } + + private void startBisqApp(BisqAppConfig bisqAppConfig, + ExecutorService executor, + CountDownLatch countdownLatch) + throws IOException, InterruptedException { + + BisqApp bisqApp; + switch (bisqAppConfig) { + case seednode: + bisqApp = createBisqApp(seednode); + seedNodeTask = new SetupTask(bisqApp, countdownLatch); + seedNodeTaskFuture = executor.submit(seedNodeTask); + break; + case arbdaemon: + case arbdesktop: + bisqApp = createBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon); + arbNodeTask = new SetupTask(bisqApp, countdownLatch); + arbNodeTaskFuture = executor.submit(arbNodeTask); + break; + case alicedaemon: + case alicedesktop: + bisqApp = createBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon); + aliceNodeTask = new SetupTask(bisqApp, countdownLatch); + aliceNodeTaskFuture = executor.submit(aliceNodeTask); + break; + case bobdaemon: + case bobdesktop: + bisqApp = createBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon); + bobNodeTask = new SetupTask(bisqApp, countdownLatch); + bobNodeTaskFuture = executor.submit(bobNodeTask); + break; + default: + throw new IllegalStateException("Unknown Bisq App " + bisqAppConfig.appName); + } + SECONDS.sleep(5); + if (bisqApp.hasStartupExceptions()) { + for (Throwable t : bisqApp.getStartupExceptions()) { + log.error("", t); + } + exit(EXIT_FAILURE); + } + } + + private BisqApp createBisqApp(BisqAppConfig bisqAppConfig) + throws IOException, InterruptedException { + BisqApp bisqNode = new BisqApp(bisqAppConfig, config); + bisqNode.verifyAppNotRunning(); + bisqNode.verifyAppDataDirInstalled(); + return bisqNode; + } + + private void verifyStartupCompleted() + throws ExecutionException, InterruptedException { + if (config.numSetupTasks > 0) + verifyStartupCompleted(bitcoindTaskFuture); + + if (config.numSetupTasks > 1) + verifyStartupCompleted(seedNodeTaskFuture); + + if (config.numSetupTasks > 2) + verifyStartupCompleted(arbNodeTaskFuture); + + if (config.numSetupTasks > 3) + verifyStartupCompleted(aliceNodeTaskFuture); + + if (config.numSetupTasks > 4) + verifyStartupCompleted(bobNodeTaskFuture); + } + + private void verifyStartupCompleted(Future futureStatus) + throws ExecutionException, InterruptedException { + for (int i = 0; i < 10; i++) { + if (futureStatus.isDone()) { + log.info("{} completed startup at {} {}", + futureStatus.get().getName(), + futureStatus.get().getStartTime().toLocalDate(), + futureStatus.get().getStartTime().toLocalTime()); + return; + } else { + // We are giving the thread more time to terminate after the countdown + // latch reached 0. If we are running only bitcoind, we need to be even + // more lenient. + SECONDS.sleep(config.numSetupTasks == 1 ? 2 : 1); + } + } + throw new IllegalStateException(format("%s did not complete startup", futureStatus.get().getName())); + } + + private void verifyNotWindows() { + if (Utilities.isWindows()) + throw new IllegalStateException("ApiTest not supported on Windows"); + } +} diff --git a/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java b/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java index 509260763e9..cac1a21e157 100644 --- a/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java +++ b/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java @@ -37,7 +37,7 @@ public SmokeTestBitcoind(ApiTestConfig config) { this.config = config; } - public void runSmokeTest() throws IOException, InterruptedException { + public void run() throws IOException, InterruptedException { runBitcoinGetWalletInfo(); // smoke test bitcoin-cli String newBitcoinAddress = getNewAddress(); generateToAddress(1, newBitcoinAddress); From 2c10836a69d18eff24073cd8cfdac4305314df14 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 13:55:08 -0300 Subject: [PATCH 18/88] Expose grpc service stubs Added :proto to the :apitest classpath for access to grpc service stubs (to be) used in method (unit) tests. Added new GrpcStubs class to expose the grpc service stubs to method and scenario tests. The larger goal of :apitest is end to end testing, where :cli's console output is checked for correctness. This change partially addresses two other important use cases: * "method" testing -- an analog to unit testing * "scenario" testing -- an analog to narrow functional testing For example, tests in the apitest.method package will directly call grpc services, and asserts will be made on the return values instead of console output. Tests in the apitest.scenario package will check correctness for broader use cases, such as funding a wallet, encrypting then unlocking a wallet for a specific time frame, or checking error messages from the server when a "getbalance" call is made after an "unlockwallet" timeout has expired. The broader end to end tests will not use grpc stubs. --- .../src/main/java/bisq/apitest/GrpcStubs.java | 107 ++++++++++++++++++ build.gradle | 1 + 2 files changed, 108 insertions(+) create mode 100644 apitest/src/main/java/bisq/apitest/GrpcStubs.java diff --git a/apitest/src/main/java/bisq/apitest/GrpcStubs.java b/apitest/src/main/java/bisq/apitest/GrpcStubs.java new file mode 100644 index 00000000000..9f942c3f518 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/GrpcStubs.java @@ -0,0 +1,107 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest; + +import bisq.proto.grpc.GetVersionGrpc; +import bisq.proto.grpc.OffersGrpc; +import bisq.proto.grpc.PaymentAccountsGrpc; +import bisq.proto.grpc.WalletsGrpc; + +import io.grpc.CallCredentials; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; + +import java.util.concurrent.Executor; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static io.grpc.Status.UNAUTHENTICATED; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.config.BisqAppConfig; + +public class GrpcStubs { + + public final CallCredentials credentials; + public final String host; + public final int port; + + public GetVersionGrpc.GetVersionBlockingStub versionService; + public OffersGrpc.OffersBlockingStub offersService; + public PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService; + public WalletsGrpc.WalletsBlockingStub walletsService; + + public GrpcStubs(BisqAppConfig bisqAppConfig, ApiTestConfig config) { + this.credentials = new PasswordCallCredentials(config.apiPassword); + this.host = "localhost"; + this.port = bisqAppConfig.apiPort; + } + + GrpcStubs init() { + var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + channel.shutdown().awaitTermination(1, SECONDS); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + })); + + this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + + return this; + } + + static class PasswordCallCredentials extends CallCredentials { + + public static final String PASSWORD_KEY = "password"; + private final String passwordValue; + + public PasswordCallCredentials(String passwordValue) { + if (passwordValue == null) + throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY)); + this.passwordValue = passwordValue; + } + + @Override + public void applyRequestMetadata(RequestInfo requestInfo, + Executor appExecutor, + MetadataApplier metadataApplier) { + appExecutor.execute(() -> { + try { + var headers = new Metadata(); + var passwordKey = Metadata.Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER); + headers.put(passwordKey, passwordValue); + metadataApplier.apply(headers); + } catch (Throwable ex) { + metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); + } + }); + } + + @Override + public void thisUsesUnstableApi() { + } + } +} diff --git a/build.gradle b/build.gradle index 6c379b748a2..6ff8439da99 100644 --- a/build.gradle +++ b/build.gradle @@ -589,6 +589,7 @@ configure(project(':apitest')) { } dependencies { + compile project(':proto') compile project(':common') compile project(':seednode') compile project(':desktop') From ffe376e8dcf8dabdf9357e873f255611a4a71a3e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 14:39:20 -0300 Subject: [PATCH 19/88] Fix codacy problems Avoid throwing raw exception types. Document empty method body. --- apitest/src/main/java/bisq/apitest/GrpcStubs.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apitest/src/main/java/bisq/apitest/GrpcStubs.java b/apitest/src/main/java/bisq/apitest/GrpcStubs.java index 9f942c3f518..1df323bf31a 100644 --- a/apitest/src/main/java/bisq/apitest/GrpcStubs.java +++ b/apitest/src/main/java/bisq/apitest/GrpcStubs.java @@ -61,7 +61,7 @@ GrpcStubs init() { try { channel.shutdown().awaitTermination(1, SECONDS); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + throw new IllegalStateException(ex); } })); @@ -100,6 +100,10 @@ public void applyRequestMetadata(RequestInfo requestInfo, }); } + /** + * An experimental api. A noop but never called; tries to make it clearer to + * implementors that they may break in the future. + */ @Override public void thisUsesUnstableApi() { } From 390cba1b75cfd10835a6999f119c912c200afa48 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 14:53:32 -0300 Subject: [PATCH 20/88] Fix codacy problem Codacy wants comments inside an empty method. --- apitest/src/main/java/bisq/apitest/GrpcStubs.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/GrpcStubs.java b/apitest/src/main/java/bisq/apitest/GrpcStubs.java index 1df323bf31a..0a461c9214c 100644 --- a/apitest/src/main/java/bisq/apitest/GrpcStubs.java +++ b/apitest/src/main/java/bisq/apitest/GrpcStubs.java @@ -100,12 +100,10 @@ public void applyRequestMetadata(RequestInfo requestInfo, }); } - /** - * An experimental api. A noop but never called; tries to make it clearer to - * implementors that they may break in the future. - */ @Override public void thisUsesUnstableApi() { + // An experimental api. A noop but never called; tries to make it clearer to + // implementors that they may break in the future. } } } From 96cabfb17792b106b6d378738b399a32cefffc51 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 21:44:21 -0300 Subject: [PATCH 21/88] Support @Skip on test classes and methods Add super class for all test types (method, scenario, end-to-end), and an class & method level annotation for skipping tests. --- .../main/java/bisq/apitest/ApiTestCase.java | 81 +++++++++++++++++++ .../java/bisq/apitest/annotation/Skip.java | 38 +++++++++ 2 files changed, 119 insertions(+) create mode 100644 apitest/src/main/java/bisq/apitest/ApiTestCase.java create mode 100644 apitest/src/main/java/bisq/apitest/annotation/Skip.java diff --git a/apitest/src/main/java/bisq/apitest/ApiTestCase.java b/apitest/src/main/java/bisq/apitest/ApiTestCase.java new file mode 100644 index 00000000000..b196c4a34fa --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/ApiTestCase.java @@ -0,0 +1,81 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest; + +import java.util.function.Predicate; + +import java.lang.reflect.Method; + +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + + + +import bisq.apitest.annotation.Skip; + +public class ApiTestCase { + + protected static final char CHECK = '\u2714'; + protected static final char CROSS_MARK = '\u274c'; + + public int countTestCases; + public int countFailedTestCases; + public int countSkippedTestCases; + public int countPassedTestCases; + + private final Predicate> skipAll = (c) -> c.getAnnotation(Skip.class) != null; + private final Predicate skip = (m) -> m.getAnnotation(Skip.class) != null; + + protected boolean isSkipped(String methodName) { + try { + if (skipAll.test(this.getClass()) || skip.test(getMethod(methodName))) { + countSkippedTestCases++; + return true; + } else { + return false; + } + } finally { + countTestCases++; // Increment the test case count, skipped or not. + } + } + + protected Method getMethod(String methodName) { + try { + return this.getClass().getMethod(methodName); + } catch (NoSuchMethodException ex) { + throw new IllegalStateException(format("No method '%s' exists in class '%s'", + methodName, this.getClass().getName()), + ex); + } + } + + protected String reportString() { + return format("Total %d Passed %d Failed %d Skipped %d", + countTestCases, + countPassedTestCases, + countFailedTestCases, + countSkippedTestCases); + } + + protected void sleep(long ms) { + try { + MILLISECONDS.sleep(ms); + } catch (InterruptedException ignored) { + } + } +} diff --git a/apitest/src/main/java/bisq/apitest/annotation/Skip.java b/apitest/src/main/java/bisq/apitest/annotation/Skip.java new file mode 100644 index 00000000000..a08784154d6 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/annotation/Skip.java @@ -0,0 +1,38 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Sometimes you want to temporarily disable a test or a group of tests. Methods that + * are annotated with @Skip will not be executed as tests. + * Also, you can annotate a class containing test methods with @Skip + * and none of the containing tests will be executed. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface Skip { + /** + * The optional reason why the test is skipped. + */ + String value() default ""; +} From 0edcc3baa829613b2586225989c8412544e6031f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 21:48:22 -0300 Subject: [PATCH 22/88] Add first method test cases These are method test cases for gRPC methods that have already been well tested by :cli/test.sh --- .../bisq/apitest/method/GetBalanceTest.java | 49 +++++++++ .../bisq/apitest/method/GetVersionTest.java | 51 +++++++++ .../java/bisq/apitest/method/MethodTest.java | 63 +++++++++++ .../method/SimpleWalletPasswordTest.java | 101 ++++++++++++++++++ 4 files changed, 264 insertions(+) create mode 100644 apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java create mode 100644 apitest/src/main/java/bisq/apitest/method/GetVersionTest.java create mode 100644 apitest/src/main/java/bisq/apitest/method/MethodTest.java create mode 100644 apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java diff --git a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java new file mode 100644 index 00000000000..048c30afddd --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java @@ -0,0 +1,49 @@ +package bisq.apitest.method; + +import bisq.proto.grpc.GetBalanceRequest; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.apitest.GrpcStubs; + +@Slf4j +public class GetBalanceTest extends MethodTest { + + public GetBalanceTest(GrpcStubs grpcStubs) { + super(grpcStubs); + } + + public void setUp() { + log.info("{} ...", this.getClass().getSimpleName()); + } + + public void run() { + setUp(); + testGetBalance(); + report(); + tearDown(); + } + + public void testGetBalance() { + if (isSkipped("testGetBalance")) + return; + + var balance = grpcStubs.walletsService.getBalance(GetBalanceRequest.newBuilder().build()).getBalance(); + if (balance == 1000000000) { + log.info("{} testGetBalance passed", CHECK); + countPassedTestCases++; + } else { + log.info("{} testGetBalance failed, expected {} actual {}", CROSS_MARK, 1000000000, balance); + countFailedTestCases++; + } + } + + public void report() { + log.info(reportString()); + } + + public void tearDown() { + } +} diff --git a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java new file mode 100644 index 00000000000..070445a8803 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java @@ -0,0 +1,51 @@ +package bisq.apitest.method; + +import bisq.proto.grpc.GetVersionRequest; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.common.app.Version.VERSION; + + + +import bisq.apitest.GrpcStubs; + +@Slf4j +public class GetVersionTest extends MethodTest { + + public GetVersionTest(GrpcStubs grpcStubs) { + super(grpcStubs); + } + + public void setUp() { + log.info("{} ...", this.getClass().getSimpleName()); + } + + public void run() { + setUp(); + testGetVersion(); + report(); + tearDown(); + } + + public void testGetVersion() { + if (isSkipped("testGetVersion")) + return; + + var version = grpcStubs.versionService.getVersion(GetVersionRequest.newBuilder().build()).getVersion(); + if (version.equals(VERSION)) { + log.info("{} testGetVersion passed", CHECK); + countPassedTestCases++; + } else { + log.info("{} testGetVersion failed, expected {} actual {}", CROSS_MARK, VERSION, version); + countFailedTestCases++; + } + } + + public void report() { + log.info(reportString()); + } + + public void tearDown() { + } +} diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTest.java b/apitest/src/main/java/bisq/apitest/method/MethodTest.java new file mode 100644 index 00000000000..e2778938be2 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/method/MethodTest.java @@ -0,0 +1,63 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method; + +import bisq.proto.grpc.GetBalanceRequest; +import bisq.proto.grpc.RemoveWalletPasswordRequest; +import bisq.proto.grpc.SetWalletPasswordRequest; + + + +import bisq.apitest.ApiTestCase; +import bisq.apitest.GrpcStubs; + +abstract class MethodTest extends ApiTestCase { + + protected final GrpcStubs grpcStubs; + + public MethodTest(GrpcStubs grpcStubs) { + super(); + this.grpcStubs = grpcStubs; + } + + public abstract void setUp(); + + public abstract void run(); + + public abstract void tearDown(); + + // Convenience methods for building gRPC request objects + + protected final GetBalanceRequest createBalanceRequest() { + return GetBalanceRequest.newBuilder().build(); + } + + protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) { + return SetWalletPasswordRequest.newBuilder().setPassword(password).build(); + } + + protected final RemoveWalletPasswordRequest createRemoveWalletPasswordRequest(String password) { + return RemoveWalletPasswordRequest.newBuilder().setPassword("password").build(); + } + + // Convenience methods for calling frequently used & thoroughly tested gRPC services. + + protected final long getBalance() { + return grpcStubs.walletsService.getBalance(createBalanceRequest()).getBalance(); + } +} diff --git a/apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java b/apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java new file mode 100644 index 00000000000..56715d0d9f4 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java @@ -0,0 +1,101 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.apitest.GrpcStubs; + +@Slf4j +public class SimpleWalletPasswordTest extends MethodTest { + + public SimpleWalletPasswordTest(GrpcStubs grpcStubs) { + super(grpcStubs); + } + + public void setUp() { + log.info("{} ...", this.getClass().getSimpleName()); + } + + public void run() { + setUp(); + testSetWalletPassword(); + testRemoveWalletPassword(); + report(); + tearDown(); + } + + public void testSetWalletPassword() { + if (isSkipped("testSetWalletPassword")) + return; + + // Set a password on the wallet, give time for the wallet to be persisted to disk, + // and attempt to get the balance of the locked wallet. + // If the gRPC GetBalanceService throws an exception with a msg saying the wallet + // is locked, the test passes. + var setPasswordRequest = createSetWalletPasswordRequest("password"); + grpcStubs.walletsService.setWalletPassword(setPasswordRequest); + sleep(1500); + + try { + getBalance(); + } catch (Throwable t) { + if (t.getMessage().contains("wallet is locked")) { + log.info("{} testSetWalletPassword passed", CHECK); + countPassedTestCases++; + } else { + log.info("{} testSetWalletPassword failed, expected '{}' exception, actual '{}'", + CROSS_MARK, "wallet is locked", t.getMessage()); + log.error("", t); + countFailedTestCases++; + } + } + } + + public void testRemoveWalletPassword() { + if (isSkipped("testRemoveWalletPassword")) + return; + + // Remove the password on the wallet, give time for the wallet to be persisted + // to disk, and attempt to get the balance of the locked wallet. + // If the gRPC GetBalanceService throws an exception with a msg saying the wallet + // is locked, the test fails. + var removePasswordRequest = createRemoveWalletPasswordRequest("password"); + grpcStubs.walletsService.removeWalletPassword(removePasswordRequest); + sleep(1500); + + try { + getBalance(); + log.info("{} testRemoveWalletPassword passed", CHECK); + countPassedTestCases++; + } catch (Throwable t) { + log.info("{} testRemoveWalletPassword failed", CROSS_MARK); + log.error("", t); + countFailedTestCases++; + } + } + + public void report() { + log.info(reportString()); + } + + public void tearDown() { + } +} From 81aeeddc4acda6dfba1671aa464894fe5519a284 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 21:52:23 -0300 Subject: [PATCH 23/88] Add MethodTestSuite --- .../bisq/apitest/method/MethodTestSuite.java | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 apitest/src/main/java/bisq/apitest/method/MethodTestSuite.java diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTestSuite.java b/apitest/src/main/java/bisq/apitest/method/MethodTestSuite.java new file mode 100644 index 00000000000..3d1ccc7cc14 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/method/MethodTestSuite.java @@ -0,0 +1,110 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.Scaffold.EXIT_FAILURE; +import static bisq.apitest.Scaffold.EXIT_SUCCESS; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static java.lang.String.format; +import static java.lang.System.err; +import static java.lang.System.exit; + + + +import bisq.apitest.GrpcStubs; +import bisq.apitest.Scaffold; +import bisq.apitest.config.ApiTestConfig; + +@Slf4j +public class MethodTestSuite { + + private int countTestCases; + private int countFailedTestCases; + private int countSkippedTestCases; + private int countPassedTestCases; + + private final GrpcStubs grpcStubs; + + public MethodTestSuite(GrpcStubs grpcStubs) { + this.grpcStubs = grpcStubs; + } + + public void run() { + log.info("{} ...", this.getClass().getSimpleName()); + + MethodTest getVersionTest = new GetVersionTest(grpcStubs); + getVersionTest.run(); + updateTally(getVersionTest); + + MethodTest getBalanceTest = new GetBalanceTest(grpcStubs); + getBalanceTest.run(); + updateTally(getBalanceTest); + + MethodTest simpleWalletPasswordTest = new SimpleWalletPasswordTest(grpcStubs); + simpleWalletPasswordTest.run(); + updateTally(simpleWalletPasswordTest); + + log.info(reportString()); + } + + private void updateTally(MethodTest methodTest) { + countTestCases += methodTest.countTestCases; + countPassedTestCases += methodTest.countPassedTestCases; + countFailedTestCases += methodTest.countFailedTestCases; + countSkippedTestCases += methodTest.countSkippedTestCases; + } + + private String reportString() { + return format("Total: %d Passed: %d Failed: %d Skipped: %d", + countTestCases, + countPassedTestCases, + countFailedTestCases, + countSkippedTestCases); + } + + public static void main(String[] args) { + try { + Scaffold scaffold = new Scaffold(args).setUp(); + ApiTestConfig config = scaffold.config; + + if (config.skipTests) { + log.info("Skipping tests ..."); + } else { + GrpcStubs grpcStubs = new GrpcStubs(alicedaemon, config).init(); + MethodTestSuite methodTestSuite = new MethodTestSuite(grpcStubs); + methodTestSuite.run(); + } + + if (config.shutdownAfterTests) { + scaffold.tearDown(); + exit(EXIT_SUCCESS); + } else { + log.info("Not shutting down scaffolding background processes will run until ^C / kill -15 is rcvd ..."); + } + + } catch (Throwable ex) { + err.println("Fault: An unexpected error occurred. " + + "Please file a report at https://bisq.network/issues"); + ex.printStackTrace(err); + exit(EXIT_FAILURE); + } + } +} + From 8da46465844bd7bde70ac1cef4f42830ff9be365 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 21:53:58 -0300 Subject: [PATCH 24/88] Update bats version test --- cli/test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/test.sh b/cli/test.sh index 54af77c717b..9878d93fd02 100755 --- a/cli/test.sh +++ b/cli/test.sh @@ -48,14 +48,14 @@ run ./bisq-cli --password="xyz" getversion [ "$status" -eq 0 ] echo "actual output: $output" >&2 - [ "$output" = "1.3.4" ] + [ "$output" = "1.3.5" ] } @test "test getversion" { run ./bisq-cli --password=xyz getversion [ "$status" -eq 0 ] echo "actual output: $output" >&2 - [ "$output" = "1.3.4" ] + [ "$output" = "1.3.5" ] } @test "test setwalletpassword \"a b c\"" { From db5a685468dbc1ea236fc4615e466747b71e131f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 21:56:51 -0300 Subject: [PATCH 25/88] Make init() method public --- apitest/src/main/java/bisq/apitest/GrpcStubs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apitest/src/main/java/bisq/apitest/GrpcStubs.java b/apitest/src/main/java/bisq/apitest/GrpcStubs.java index 0a461c9214c..6279c61489f 100644 --- a/apitest/src/main/java/bisq/apitest/GrpcStubs.java +++ b/apitest/src/main/java/bisq/apitest/GrpcStubs.java @@ -55,7 +55,7 @@ public GrpcStubs(BisqAppConfig bisqAppConfig, ApiTestConfig config) { this.port = bisqAppConfig.apiPort; } - GrpcStubs init() { + public GrpcStubs init() { var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { From 65e3370d5137d135f5d975ea256068f13c2ff394 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 22:03:46 -0300 Subject: [PATCH 26/88] Add license note, format tearDown(), fix comment --- .../src/main/java/bisq/apitest/Scaffold.java | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index f1b2b68539b..6216869c427 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + package bisq.apitest; import bisq.common.config.BisqHelpFormatter; @@ -5,7 +22,6 @@ import java.io.IOException; -import java.util.Arrays; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -22,6 +38,7 @@ import static java.lang.System.err; import static java.lang.System.exit; import static java.lang.System.out; +import static java.util.Arrays.stream; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; @@ -38,7 +55,7 @@ public class Scaffold { public static final int EXIT_SUCCESS = 0; public static final int EXIT_FAILURE = 1; - final ApiTestConfig config; + public final ApiTestConfig config; @Nullable private SetupTask bitcoindTask; @@ -104,25 +121,25 @@ public Scaffold setUp() { return this; } - public ApiTestConfig getConfig() { - return config; - } - public void tearDown() { if (!executor.isTerminated()) { try { log.info("Shutting down executor service ..."); executor.shutdownNow(); executor.awaitTermination(config.numSetupTasks * 2000, MILLISECONDS); - SetupTask[] orderedTasks = new SetupTask[]{bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask}; - Arrays.stream(orderedTasks).filter(t -> t != null && t.getLinuxProcess() != null).forEachOrdered(t -> { - try { - t.getLinuxProcess().shutdown(); - MILLISECONDS.sleep(1500); - } catch (IOException | InterruptedException ex) { - throw new IllegalStateException(ex); - } - }); + + SetupTask[] orderedTasks = new SetupTask[]{ + bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask}; + stream(orderedTasks).filter(t -> t != null && t.getLinuxProcess() != null) + .forEachOrdered(t -> { + try { + t.getLinuxProcess().shutdown(); + MILLISECONDS.sleep(1000); + } catch (IOException | InterruptedException ex) { + throw new IllegalStateException(ex); + } + }); + log.info("Teardown complete"); } catch (Exception ex) { throw new IllegalStateException(ex); @@ -131,11 +148,9 @@ public void tearDown() { } private void installShutdownHook() { - // A test may shut down background apps, or they may be left running until - // the jvm is manually shutdown, so we add a shutdown hook for that use case. - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - tearDown(); - })); + // Background apps can be left running until the jvm is manually shutdown, + // so we add a shutdown hook for that use case. + Runtime.getRuntime().addShutdownHook(new Thread(this::tearDown)); } // Starts bitcoind and bisq apps (seednode, arbnode, etc...) From fae661c9125910773a32ec77db6ea9c8f2db8d74 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 22:05:36 -0300 Subject: [PATCH 27/88] Run MethodTestSuite --- apitest/src/main/java/bisq/apitest/ApiTestMain.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apitest/src/main/java/bisq/apitest/ApiTestMain.java b/apitest/src/main/java/bisq/apitest/ApiTestMain.java index ea58c33e837..35f19cfda4b 100644 --- a/apitest/src/main/java/bisq/apitest/ApiTestMain.java +++ b/apitest/src/main/java/bisq/apitest/ApiTestMain.java @@ -21,12 +21,14 @@ import static bisq.apitest.Scaffold.EXIT_FAILURE; import static bisq.apitest.Scaffold.EXIT_SUCCESS; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static java.lang.System.err; import static java.lang.System.exit; import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.method.MethodTestSuite; /** * ApiTest Application @@ -51,6 +53,9 @@ public void execute(@SuppressWarnings("unused") String[] args) { log.info("Skipping tests ..."); } else { new SmokeTestBitcoind(config).run(); + GrpcStubs grpcStubs = new GrpcStubs(alicedaemon, config).init(); + MethodTestSuite methodTestSuite = new MethodTestSuite(grpcStubs); + methodTestSuite.run(); } if (config.shutdownAfterTests) { From 458d2f3f4f624028131e0b8a98011719f2f18021 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 12 Jul 2020 22:15:16 -0300 Subject: [PATCH 28/88] Add license note --- .../bisq/apitest/method/GetBalanceTest.java | 17 +++++++++++++++++ .../bisq/apitest/method/GetVersionTest.java | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java index 048c30afddd..24c5d79293e 100644 --- a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java +++ b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + package bisq.apitest.method; import bisq.proto.grpc.GetBalanceRequest; diff --git a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java index 070445a8803..9e4654832ed 100644 --- a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java +++ b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + package bisq.apitest.method; import bisq.proto.grpc.GetVersionRequest; From 4d5c7679020670f0b4d28e77d93f7149e6fb4b9b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 13 Jul 2020 10:28:31 -0300 Subject: [PATCH 29/88] Fix codacy problem Add comment inside empty method. --- apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java | 1 + apitest/src/main/java/bisq/apitest/method/GetVersionTest.java | 1 + .../main/java/bisq/apitest/method/SimpleWalletPasswordTest.java | 1 + 3 files changed, 3 insertions(+) diff --git a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java index 24c5d79293e..4c2f84ee160 100644 --- a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java +++ b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java @@ -62,5 +62,6 @@ public void report() { } public void tearDown() { + // noop } } diff --git a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java index 9e4654832ed..40c93d79444 100644 --- a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java +++ b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java @@ -64,5 +64,6 @@ public void report() { } public void tearDown() { + // noop } } diff --git a/apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java b/apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java index 56715d0d9f4..676989657b9 100644 --- a/apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java +++ b/apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java @@ -97,5 +97,6 @@ public void report() { } public void tearDown() { + // noop } } From 35ff4e5d793dffb3cec7801dffb06839f0ad06f9 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 13 Jul 2020 19:48:59 -0300 Subject: [PATCH 30/88] Delete commented statement --- apitest/dao-setup.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/apitest/dao-setup.gradle b/apitest/dao-setup.gradle index e342928313f..5f55ce72e5c 100644 --- a/apitest/dao-setup.gradle +++ b/apitest/dao-setup.gradle @@ -63,7 +63,6 @@ task installDaoSetup(dependsOn: 'cleanDaoSetup') { .renameTo(file(buildResourcesDir + '/bisq-BTC_REGTEST_Alice_dao')) file(buildResourcesDir + '/dao-setup/bisq-BTC_REGTEST_Bob_dao') .renameTo(file(buildResourcesDir + '/bisq-BTC_REGTEST_Bob_dao')) - // delete dir build/resources/main/dao-setup delete file(buildResourcesDir + '/dao-setup') } } From d782e8d8ecc645b0c4ef5631d462f478f25ac1d1 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 13 Jul 2020 19:50:33 -0300 Subject: [PATCH 31/88] Do not run dummy test from driver --- apitest/src/main/java/bisq/apitest/ApiTestMain.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/ApiTestMain.java b/apitest/src/main/java/bisq/apitest/ApiTestMain.java index 35f19cfda4b..ea58c33e837 100644 --- a/apitest/src/main/java/bisq/apitest/ApiTestMain.java +++ b/apitest/src/main/java/bisq/apitest/ApiTestMain.java @@ -21,14 +21,12 @@ import static bisq.apitest.Scaffold.EXIT_FAILURE; import static bisq.apitest.Scaffold.EXIT_SUCCESS; -import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static java.lang.System.err; import static java.lang.System.exit; import bisq.apitest.config.ApiTestConfig; -import bisq.apitest.method.MethodTestSuite; /** * ApiTest Application @@ -53,9 +51,6 @@ public void execute(@SuppressWarnings("unused") String[] args) { log.info("Skipping tests ..."); } else { new SmokeTestBitcoind(config).run(); - GrpcStubs grpcStubs = new GrpcStubs(alicedaemon, config).init(); - MethodTestSuite methodTestSuite = new MethodTestSuite(grpcStubs); - methodTestSuite.run(); } if (config.shutdownAfterTests) { From 84976fef35004d37860e4b3ecf949f566d14d4e8 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 13 Jul 2020 19:51:27 -0300 Subject: [PATCH 32/88] Fix varible names --- apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java b/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java index cac1a21e157..92d1ba8051f 100644 --- a/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java +++ b/apitest/src/main/java/bisq/apitest/SmokeTestBitcoind.java @@ -65,8 +65,8 @@ public String getNewAddress() throws IOException, InterruptedException { public void generateToAddress(int blocks, String address) throws IOException, InterruptedException { String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address); - BitcoinCli newAddress = new BitcoinCli(config, generateToAddressCmd).run(); + BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); // Return value is an array of TxIDs. - log.info("{}\n{}", newAddress.getCommandWithOptions(), newAddress.getOutputValueAsStringArray()); + log.info("{}\n{}", generateToAddress.getCommandWithOptions(), generateToAddress.getOutputValueAsStringArray()); } } From 2678b31a18cfe71e51512fde7d6bcddcffb5ad9e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 13 Jul 2020 19:52:21 -0300 Subject: [PATCH 33/88] Remove test scaffolding logic from ApiTestConfig --- .../bisq/apitest/config/ApiTestConfig.java | 72 ------------------- 1 file changed, 72 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index 0d80a69716b..3532a937055 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -17,8 +17,6 @@ package bisq.apitest.config; -import bisq.common.storage.FileUtil; - import joptsimple.AbstractOptionSpec; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.HelpFormatter; @@ -27,17 +25,12 @@ import joptsimple.OptionSet; import joptsimple.OptionSpec; -import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.PosixFilePermissions; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; -import java.io.PrintWriter; import java.io.UncheckedIOException; import java.util.ArrayList; @@ -294,11 +287,6 @@ public ApiTestConfig(String... args) { // Assign values to special-case static fields. BASH_PATH_VALUE = bashPath; - // Write and save bitcoin.conf to disk, with the correct path to - // the blocknotify script. - installBitcoinConf(); - installBitcoinBlocknotify(); - } catch (OptionException ex) { throw new IllegalStateException(format("Problem parsing option '%s': %s", ex.options().get(0), @@ -317,40 +305,6 @@ public void printHelp(OutputStream sink, HelpFormatter formatter) { } } - private void installBitcoinConf() { - // We write out and install a bitcoin.conf file for regtest/dao mode because - // the path to the blocknotify script is not known until runtime. - String bitcoinConf = "\n" - + "regtest=1\n" - + "[regtest]\n" - + "peerbloomfilters=1\n" - + "rpcport=18443\n" - + "server=1\n" - + "txindex=1\n" - + "debug=net\n" - + "deprecatedrpc=generate\n" - + "rpcuser=apitest\n" - + "rpcpassword=apitest\n" - + "blocknotify=" + bashPath + " " + bitcoinDatadir + "/blocknotify %\n"; - String chmod644Perms = "rw-r--r--"; - saveToFile(bitcoinConf, bitcoinDatadir, "bitcoin.conf", chmod644Perms); - log.info("Installed {} with perms {}.", bitcoinDatadir + "/bitcoin.conf", chmod644Perms); - } - - private void installBitcoinBlocknotify() { - // gradle is not working for this - try { - Path srcPath = Paths.get("apitest", "src", "main", "resources", "blocknotify"); - Path destPath = Paths.get(bitcoinDatadir, "blocknotify"); - Files.copy(srcPath, destPath, StandardCopyOption.REPLACE_EXISTING); - String chmod700Perms = "rwx------"; - Files.setPosixFilePermissions(destPath, PosixFilePermissions.fromString(chmod700Perms)); - log.info("Installed {} with perms {}.", destPath.toString(), chmod700Perms); - } catch (IOException e) { - e.printStackTrace(); - } - } - private Optional parseOptionsFrom(File configFile, OptionSpec[] disallowedOpts) { if (!configFile.exists() && !configFile.equals(absoluteConfigFile(userDir, DEFAULT_CONFIG_FILE_NAME))) throw new IllegalStateException(format("The specified config file '%s' does not exist.", configFile)); @@ -383,32 +337,6 @@ private Properties getProperties(File configFile) { } } - private void saveToFile(String content, - String parentDir, - @SuppressWarnings("SameParameterValue") String relativeConfigFilePath, - String posixFilePermissions) { - File tempFile = null; - File file; - try { - file = absoluteConfigFile(parentDir, relativeConfigFilePath); - tempFile = File.createTempFile("temp", relativeConfigFilePath, file.getParentFile()); - tempFile.deleteOnExit(); - try (PrintWriter out = new PrintWriter(tempFile)) { - out.println(content); - } - FileUtil.renameFile(tempFile, file); - Files.setPosixFilePermissions(Paths.get(file.toURI()), PosixFilePermissions.fromString(posixFilePermissions)); - } catch (IOException ex) { - throw new IllegalStateException(format("Error saving %s/%s to disk", parentDir, relativeConfigFilePath), ex); - } finally { - if (tempFile != null && tempFile.exists()) { - log.warn("Temp file still exists after failed save; deleting {} now.", tempFile.getAbsolutePath()); - if (!tempFile.delete()) - log.error("Cannot delete temp file."); - } - } - } - private static File absoluteConfigFile(String parentDir, String relativeConfigFilePath) { return new File(parentDir, relativeConfigFilePath); } From 486f06beefb0d8bade970784437b8993298ad0db Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 13 Jul 2020 19:54:39 -0300 Subject: [PATCH 34/88] Refresh dao-setup files in Scaffold setup Many test cases will need to run on a clean, initialized dao-setup; we do not want call dao-setup.gradle tasks from Java. For now, every JUnit test case will refresh the dao-setup data in @BeforeClass, but @AfterClass will not clean out the dao-setup files. We want to be able to look at bitcoin-core and bisq app data & log files after a test. Given the difficulty of refreshing dao-setup files using Java, I took a short cut and used the apitest.linux pkg's BashCommand to replace dao-setup files. Some bitcoin.conf and blocknotify configuratation logic was also moved from ApiTestConfig to Scaffold in this commit. --- .../src/main/java/bisq/apitest/Scaffold.java | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index 6216869c427..8ad789e7ff2 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -18,9 +18,18 @@ package bisq.apitest; import bisq.common.config.BisqHelpFormatter; +import bisq.common.storage.FileUtil; import bisq.common.util.Utilities; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; + +import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.PrintWriter; import java.util.Objects; import java.util.concurrent.CountDownLatch; @@ -38,6 +47,7 @@ import static java.lang.System.err; import static java.lang.System.exit; import static java.lang.System.out; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static java.util.Arrays.stream; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; @@ -46,6 +56,7 @@ import bisq.apitest.config.ApiTestConfig; import bisq.apitest.config.BisqAppConfig; +import bisq.apitest.linux.BashCommand; import bisq.apitest.linux.BisqApp; import bisq.apitest.linux.BitcoinDaemon; @@ -101,6 +112,8 @@ public Scaffold(ApiTestConfig config) { public Scaffold setUp() { try { + installDaoSetupDirectories(); + // Start each background process from an executor, then add a shutdown hook. CountDownLatch countdownLatch = new CountDownLatch(config.numSetupTasks); startBackgroundProcesses(executor, countdownLatch); @@ -147,6 +160,142 @@ public void tearDown() { } } + public void installDaoSetupDirectories() { + cleanDaoSetupDirectories(); + + String srcResourcesDir = Paths.get("apitest", "src", "main", "resources", "dao-setup").toFile().getAbsolutePath(); + String buildDataDir = config.rootAppDataDir.getAbsolutePath(); + try { + if (!new File(srcResourcesDir).exists()) + throw new FileNotFoundException( + format("Dao setup dir '%s' not found. Run gradle :apitest:installDaoSetup" + + " to download dao-setup.zip and extract contents to resources folder", + srcResourcesDir)); + + BashCommand copyBitcoinRegtestDir = new BashCommand( + "cp -rf " + srcResourcesDir + "/Bitcoin-regtest/regtest" + + " " + config.bitcoinDatadir); + if (copyBitcoinRegtestDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not install bitcoin regtest dir"); + + BashCommand copyAliceDataDir = new BashCommand( + "cp -rf " + srcResourcesDir + "/" + alicedaemon.appName + + " " + config.rootAppDataDir); + if (copyAliceDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not install alice data dir"); + + BashCommand copyBobDataDir = new BashCommand( + "cp -rf " + srcResourcesDir + "/" + bobdaemon.appName + + " " + config.rootAppDataDir); + if (copyBobDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not install bob data dir"); + + log.info("Installed dao-setup files into {}", buildDataDir); + + // Write a bitcoin.conf file with the correct path to the blocknotify script, + // and save it to the build resource dir. + installBitcoinConf(); + + // Copy the blocknotify script from the src resources dir to the + // build resources dir. Users may want to edit it sometimes, + // when all default block notifcation ports are being used. + installBitcoinBlocknotify(); + + } catch (IOException | InterruptedException ex) { + throw new IllegalStateException("Could not install dao-setup files from " + srcResourcesDir, ex); + } + } + + private void cleanDaoSetupDirectories() { + String buildDataDir = config.rootAppDataDir.getAbsolutePath(); + log.info("Cleaning dao-setup data in {}", buildDataDir); + + try { + BashCommand rmBobDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + bobdaemon.appName); + if (rmBobDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not delete bob data dir"); + + BashCommand rmAliceDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + alicedaemon.appName); + if (rmAliceDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not delete alice data dir"); + + BashCommand rmArbNodeDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + arbdaemon.appName); + if (rmArbNodeDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not delete arbitrator data dir"); + + BashCommand rmSeedNodeDataDir = new BashCommand("rm -rf " + config.rootAppDataDir + "/" + seednode.appName); + if (rmSeedNodeDataDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not delete seednode data dir"); + + BashCommand rmBitcoinRegtestDir = new BashCommand("rm -rf " + config.bitcoinDatadir + "/regtest"); + if (rmBitcoinRegtestDir.run().getExitStatus() != 0) + throw new IllegalStateException("Could not clean bitcoind regtest dir"); + + } catch (IOException | InterruptedException ex) { + throw new IllegalStateException("Could not clean dao-setup files from " + buildDataDir, ex); + } + } + + private void installBitcoinConf() { + // We write out and install a bitcoin.conf file for regtest/dao mode because + // the path to the blocknotify script is not known until runtime. + String bitcoinConf = "\n" + + "regtest=1\n" + + "[regtest]\n" + + "peerbloomfilters=1\n" + + "rpcport=18443\n" + + "server=1\n" + + "txindex=1\n" + + "debug=net\n" + + "deprecatedrpc=generate\n" + + "rpcuser=apitest\n" + + "rpcpassword=apitest\n" + + "blocknotify=" + config.bashPath + " " + config.bitcoinDatadir + "/blocknotify %\n"; + String chmod644Perms = "rw-r--r--"; + saveToFile(bitcoinConf, config.bitcoinDatadir, "bitcoin.conf", chmod644Perms); + log.info("Installed {} with perms {}.", config.bitcoinDatadir + "/bitcoin.conf", chmod644Perms); + } + + private void installBitcoinBlocknotify() { + // gradle is not working for this + try { + Path srcPath = Paths.get("apitest", "src", "main", "resources", "blocknotify"); + Path destPath = Paths.get(config.bitcoinDatadir, "blocknotify"); + Files.copy(srcPath, destPath, REPLACE_EXISTING); + String chmod700Perms = "rwx------"; + Files.setPosixFilePermissions(destPath, PosixFilePermissions.fromString(chmod700Perms)); + log.info("Installed {} with perms {}.", destPath.toString(), chmod700Perms); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void saveToFile(String content, + String parentDir, + @SuppressWarnings("SameParameterValue") String relativeFilePath, + String posixFilePermissions) { + File tempFile = null; + File file; + try { + file = Paths.get(parentDir, relativeFilePath).toFile(); + tempFile = File.createTempFile("temp", relativeFilePath, file.getParentFile()); + tempFile.deleteOnExit(); + try (PrintWriter out = new PrintWriter(tempFile)) { + out.println(content); + } + FileUtil.renameFile(tempFile, file); + Files.setPosixFilePermissions(Paths.get(file.toURI()), PosixFilePermissions.fromString(posixFilePermissions)); + } catch (IOException ex) { + throw new IllegalStateException(format("Error saving %s/%s to disk", parentDir, relativeFilePath), ex); + } finally { + if (tempFile != null && tempFile.exists()) { + log.warn("Temp file still exists after failed save; deleting {} now.", tempFile.getAbsolutePath()); + if (!tempFile.delete()) + log.error("Cannot delete temp file."); + } + } + } + private void installShutdownHook() { // Background apps can be left running until the jvm is manually shutdown, // so we add a shutdown hook for that use case. From e17480ad8f2f5a749cbad908fc7a7a1e011fa520 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 13 Jul 2020 20:13:30 -0300 Subject: [PATCH 35/88] Remove hacked method tests Although JUnit is geared towards running randomly ordered unit tests, we can use it to write API tests to be run in a pre-defined order. This is necessary because API tests will be functional (narrow to broad) and end-to-end; we want to be able to write @Test methods that run in @Order. The next commit will add a JUnit OrderedRunner class, and an @Order annotation. --- .../bisq/apitest/method/GetBalanceTest.java | 67 ----------- .../bisq/apitest/method/GetVersionTest.java | 69 ----------- .../java/bisq/apitest/method/MethodTest.java | 63 ---------- .../bisq/apitest/method/MethodTestSuite.java | 110 ------------------ .../method/SimpleWalletPasswordTest.java | 102 ---------------- 5 files changed, 411 deletions(-) delete mode 100644 apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java delete mode 100644 apitest/src/main/java/bisq/apitest/method/GetVersionTest.java delete mode 100644 apitest/src/main/java/bisq/apitest/method/MethodTest.java delete mode 100644 apitest/src/main/java/bisq/apitest/method/MethodTestSuite.java delete mode 100644 apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java diff --git a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java deleted file mode 100644 index 4c2f84ee160..00000000000 --- a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.method; - -import bisq.proto.grpc.GetBalanceRequest; - -import lombok.extern.slf4j.Slf4j; - - - -import bisq.apitest.GrpcStubs; - -@Slf4j -public class GetBalanceTest extends MethodTest { - - public GetBalanceTest(GrpcStubs grpcStubs) { - super(grpcStubs); - } - - public void setUp() { - log.info("{} ...", this.getClass().getSimpleName()); - } - - public void run() { - setUp(); - testGetBalance(); - report(); - tearDown(); - } - - public void testGetBalance() { - if (isSkipped("testGetBalance")) - return; - - var balance = grpcStubs.walletsService.getBalance(GetBalanceRequest.newBuilder().build()).getBalance(); - if (balance == 1000000000) { - log.info("{} testGetBalance passed", CHECK); - countPassedTestCases++; - } else { - log.info("{} testGetBalance failed, expected {} actual {}", CROSS_MARK, 1000000000, balance); - countFailedTestCases++; - } - } - - public void report() { - log.info(reportString()); - } - - public void tearDown() { - // noop - } -} diff --git a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java deleted file mode 100644 index 40c93d79444..00000000000 --- a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.method; - -import bisq.proto.grpc.GetVersionRequest; - -import lombok.extern.slf4j.Slf4j; - -import static bisq.common.app.Version.VERSION; - - - -import bisq.apitest.GrpcStubs; - -@Slf4j -public class GetVersionTest extends MethodTest { - - public GetVersionTest(GrpcStubs grpcStubs) { - super(grpcStubs); - } - - public void setUp() { - log.info("{} ...", this.getClass().getSimpleName()); - } - - public void run() { - setUp(); - testGetVersion(); - report(); - tearDown(); - } - - public void testGetVersion() { - if (isSkipped("testGetVersion")) - return; - - var version = grpcStubs.versionService.getVersion(GetVersionRequest.newBuilder().build()).getVersion(); - if (version.equals(VERSION)) { - log.info("{} testGetVersion passed", CHECK); - countPassedTestCases++; - } else { - log.info("{} testGetVersion failed, expected {} actual {}", CROSS_MARK, VERSION, version); - countFailedTestCases++; - } - } - - public void report() { - log.info(reportString()); - } - - public void tearDown() { - // noop - } -} diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTest.java b/apitest/src/main/java/bisq/apitest/method/MethodTest.java deleted file mode 100644 index e2778938be2..00000000000 --- a/apitest/src/main/java/bisq/apitest/method/MethodTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.method; - -import bisq.proto.grpc.GetBalanceRequest; -import bisq.proto.grpc.RemoveWalletPasswordRequest; -import bisq.proto.grpc.SetWalletPasswordRequest; - - - -import bisq.apitest.ApiTestCase; -import bisq.apitest.GrpcStubs; - -abstract class MethodTest extends ApiTestCase { - - protected final GrpcStubs grpcStubs; - - public MethodTest(GrpcStubs grpcStubs) { - super(); - this.grpcStubs = grpcStubs; - } - - public abstract void setUp(); - - public abstract void run(); - - public abstract void tearDown(); - - // Convenience methods for building gRPC request objects - - protected final GetBalanceRequest createBalanceRequest() { - return GetBalanceRequest.newBuilder().build(); - } - - protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) { - return SetWalletPasswordRequest.newBuilder().setPassword(password).build(); - } - - protected final RemoveWalletPasswordRequest createRemoveWalletPasswordRequest(String password) { - return RemoveWalletPasswordRequest.newBuilder().setPassword("password").build(); - } - - // Convenience methods for calling frequently used & thoroughly tested gRPC services. - - protected final long getBalance() { - return grpcStubs.walletsService.getBalance(createBalanceRequest()).getBalance(); - } -} diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTestSuite.java b/apitest/src/main/java/bisq/apitest/method/MethodTestSuite.java deleted file mode 100644 index 3d1ccc7cc14..00000000000 --- a/apitest/src/main/java/bisq/apitest/method/MethodTestSuite.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.method; - -import lombok.extern.slf4j.Slf4j; - -import static bisq.apitest.Scaffold.EXIT_FAILURE; -import static bisq.apitest.Scaffold.EXIT_SUCCESS; -import static bisq.apitest.config.BisqAppConfig.alicedaemon; -import static java.lang.String.format; -import static java.lang.System.err; -import static java.lang.System.exit; - - - -import bisq.apitest.GrpcStubs; -import bisq.apitest.Scaffold; -import bisq.apitest.config.ApiTestConfig; - -@Slf4j -public class MethodTestSuite { - - private int countTestCases; - private int countFailedTestCases; - private int countSkippedTestCases; - private int countPassedTestCases; - - private final GrpcStubs grpcStubs; - - public MethodTestSuite(GrpcStubs grpcStubs) { - this.grpcStubs = grpcStubs; - } - - public void run() { - log.info("{} ...", this.getClass().getSimpleName()); - - MethodTest getVersionTest = new GetVersionTest(grpcStubs); - getVersionTest.run(); - updateTally(getVersionTest); - - MethodTest getBalanceTest = new GetBalanceTest(grpcStubs); - getBalanceTest.run(); - updateTally(getBalanceTest); - - MethodTest simpleWalletPasswordTest = new SimpleWalletPasswordTest(grpcStubs); - simpleWalletPasswordTest.run(); - updateTally(simpleWalletPasswordTest); - - log.info(reportString()); - } - - private void updateTally(MethodTest methodTest) { - countTestCases += methodTest.countTestCases; - countPassedTestCases += methodTest.countPassedTestCases; - countFailedTestCases += methodTest.countFailedTestCases; - countSkippedTestCases += methodTest.countSkippedTestCases; - } - - private String reportString() { - return format("Total: %d Passed: %d Failed: %d Skipped: %d", - countTestCases, - countPassedTestCases, - countFailedTestCases, - countSkippedTestCases); - } - - public static void main(String[] args) { - try { - Scaffold scaffold = new Scaffold(args).setUp(); - ApiTestConfig config = scaffold.config; - - if (config.skipTests) { - log.info("Skipping tests ..."); - } else { - GrpcStubs grpcStubs = new GrpcStubs(alicedaemon, config).init(); - MethodTestSuite methodTestSuite = new MethodTestSuite(grpcStubs); - methodTestSuite.run(); - } - - if (config.shutdownAfterTests) { - scaffold.tearDown(); - exit(EXIT_SUCCESS); - } else { - log.info("Not shutting down scaffolding background processes will run until ^C / kill -15 is rcvd ..."); - } - - } catch (Throwable ex) { - err.println("Fault: An unexpected error occurred. " + - "Please file a report at https://bisq.network/issues"); - ex.printStackTrace(err); - exit(EXIT_FAILURE); - } - } -} - diff --git a/apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java b/apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java deleted file mode 100644 index 676989657b9..00000000000 --- a/apitest/src/main/java/bisq/apitest/method/SimpleWalletPasswordTest.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.method; - -import lombok.extern.slf4j.Slf4j; - - - -import bisq.apitest.GrpcStubs; - -@Slf4j -public class SimpleWalletPasswordTest extends MethodTest { - - public SimpleWalletPasswordTest(GrpcStubs grpcStubs) { - super(grpcStubs); - } - - public void setUp() { - log.info("{} ...", this.getClass().getSimpleName()); - } - - public void run() { - setUp(); - testSetWalletPassword(); - testRemoveWalletPassword(); - report(); - tearDown(); - } - - public void testSetWalletPassword() { - if (isSkipped("testSetWalletPassword")) - return; - - // Set a password on the wallet, give time for the wallet to be persisted to disk, - // and attempt to get the balance of the locked wallet. - // If the gRPC GetBalanceService throws an exception with a msg saying the wallet - // is locked, the test passes. - var setPasswordRequest = createSetWalletPasswordRequest("password"); - grpcStubs.walletsService.setWalletPassword(setPasswordRequest); - sleep(1500); - - try { - getBalance(); - } catch (Throwable t) { - if (t.getMessage().contains("wallet is locked")) { - log.info("{} testSetWalletPassword passed", CHECK); - countPassedTestCases++; - } else { - log.info("{} testSetWalletPassword failed, expected '{}' exception, actual '{}'", - CROSS_MARK, "wallet is locked", t.getMessage()); - log.error("", t); - countFailedTestCases++; - } - } - } - - public void testRemoveWalletPassword() { - if (isSkipped("testRemoveWalletPassword")) - return; - - // Remove the password on the wallet, give time for the wallet to be persisted - // to disk, and attempt to get the balance of the locked wallet. - // If the gRPC GetBalanceService throws an exception with a msg saying the wallet - // is locked, the test fails. - var removePasswordRequest = createRemoveWalletPasswordRequest("password"); - grpcStubs.walletsService.removeWalletPassword(removePasswordRequest); - sleep(1500); - - try { - getBalance(); - log.info("{} testRemoveWalletPassword passed", CHECK); - countPassedTestCases++; - } catch (Throwable t) { - log.info("{} testRemoveWalletPassword failed", CROSS_MARK); - log.error("", t); - countFailedTestCases++; - } - } - - public void report() { - log.info(reportString()); - } - - public void tearDown() { - // noop - } -} From 080952b1d7efcdadc9a30e29ba2a27e0e8787ec2 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 13 Jul 2020 20:30:48 -0300 Subject: [PATCH 36/88] Support @Order-ing of JUnit tests --- .../main/java/bisq/apitest/OrderedRunner.java | 56 +++++++++++++++++++ .../java/bisq/apitest/annotation/Order.java | 32 +++++++++++ build.gradle | 1 + 3 files changed, 89 insertions(+) create mode 100644 apitest/src/main/java/bisq/apitest/OrderedRunner.java create mode 100644 apitest/src/main/java/bisq/apitest/annotation/Order.java diff --git a/apitest/src/main/java/bisq/apitest/OrderedRunner.java b/apitest/src/main/java/bisq/apitest/OrderedRunner.java new file mode 100644 index 00000000000..5b4faa61cf5 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/OrderedRunner.java @@ -0,0 +1,56 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; + + + +import bisq.apitest.annotation.Order; + +// Credit to Aman Goel +// https://stackoverflow.com/questions/3693626/how-to-run-test-methods-in-specific-order-in-junit4 + +public class OrderedRunner extends BlockJUnit4ClassRunner { + + public OrderedRunner(Class clazz) throws InitializationError { + super(clazz); + } + + @Override + protected List computeTestMethods() { + List list = super.computeTestMethods(); + List copy = new ArrayList<>(list); + copy.sort((f1, f2) -> { + Order o1 = f1.getAnnotation(Order.class); + Order o2 = f2.getAnnotation(Order.class); + + if (o1 == null || o2 == null) { + return -1; + } + + return o1.value() - o2.value(); + }); + return copy; + } +} diff --git a/apitest/src/main/java/bisq/apitest/annotation/Order.java b/apitest/src/main/java/bisq/apitest/annotation/Order.java new file mode 100644 index 00000000000..1d0bd2021d2 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/annotation/Order.java @@ -0,0 +1,32 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// Credit to Aman Goel +// https://stackoverflow.com/questions/3693626/how-to-run-test-methods-in-specific-order-in-junit4 + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface Order { + int value(); +} diff --git a/build.gradle b/build.gradle index 6ff8439da99..ad902e11739 100644 --- a/build.gradle +++ b/build.gradle @@ -609,6 +609,7 @@ configure(project(':apitest')) { implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" + implementation "junit:junit:$junitVersion" compileOnly "org.projectlombok:lombok:$lombokVersion" compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" annotationProcessor "org.projectlombok:lombok:$lombokVersion" From 4f08ec3edc01f39446f92bf8f1c45934efd0ea41 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 13 Jul 2020 20:34:25 -0300 Subject: [PATCH 37/88] Add first JUnit 'method' tests These @Order-ed test cases should be run with OrderedRunner. They are called 'method' tests because they are the closest thing to an API 'unit' test. Method tests are not end to end tests; they have access to gRPC service stubs and do asserts on Java values. We can write 'method' tests for the new gRPC methods we need to implement (test driven development). Some notes about the three API test categories... The plan is to categorize some of the narrower functional tests as 'scenario' tests. Like 'method' tests, 'scenario' tests will have access to gRPC service stubs and do asserts on Java values, but they will cover use cases requiring several gRPC and bitcoin-cli method calls. One example is funding a wallet: using bitcoin-cli and bisq-cli to generate new addresses, send BTC from bitcoin-core to a Bisq wallet, generate a bitcoin block, and check the Bisq wallet's balance. Another example is wallet unlocking/locking scenarios that are a bit more complex than 'unit' or 'method' tests. The third category of tests will be end to end, and live in an 'e2e' package. They will cover more complex use cases and do asserts on the Bisq CLI's console output (stdout/stderr), not return values from gRPC service stubs. There may sometimes be a fine line between what is a 'scenario' test and an 'e2e' test. But what starts out as a 'scenario' test during test driven development can migrate to an 'e2e' test after thorough testing in the quicker 'scenario' dev/test cycle. --- .../bisq/apitest/method/GetBalanceTest.java | 74 +++++++++++++++++++ .../bisq/apitest/method/GetVersionTest.java | 57 ++++++++++++++ .../java/bisq/apitest/method/MethodTest.java | 68 +++++++++++++++++ .../apitest/method/WalletProtectionTest.java | 58 +++++++++++++++ 4 files changed, 257 insertions(+) create mode 100644 apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java create mode 100644 apitest/src/main/java/bisq/apitest/method/GetVersionTest.java create mode 100644 apitest/src/main/java/bisq/apitest/method/MethodTest.java create mode 100644 apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java diff --git a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java new file mode 100644 index 00000000000..fb4d49d32c6 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java @@ -0,0 +1,74 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method; + +import bisq.proto.grpc.GetBalanceRequest; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + + + +import bisq.apitest.OrderedRunner; +import bisq.apitest.annotation.Order; +import bisq.apitest.linux.BitcoinCli; + +@Slf4j +@RunWith(OrderedRunner.class) +public class GetBalanceTest extends MethodTest { + + @BeforeClass + public static void setUp() { + try { + setUpScaffold(); + + String newAddress = new BitcoinCli(config, "getnewaddress").run().getOutput(); + String generateToAddressCmd = format("generatetoaddress %d \"%s\"", 1, newAddress); + + BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); + log.info("{}\n{}", generateToAddress.getCommandWithOptions(), generateToAddress.getOutputValueAsStringArray()); + MILLISECONDS.sleep(1500); // give bisq app time to parse block + + } catch (IOException | InterruptedException ex) { + fail(ex.getMessage()); + } + } + + @Test + @Order(1) + public void testGetBalance() { + var balance = grpcStubs.walletsService.getBalance(GetBalanceRequest.newBuilder().build()).getBalance(); + assertEquals(1000000000, balance); + } + + @AfterClass + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java new file mode 100644 index 00000000000..2e974597baf --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java @@ -0,0 +1,57 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method; + +import bisq.proto.grpc.GetVersionRequest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static bisq.common.app.Version.VERSION; +import static org.junit.Assert.assertEquals; + + + +import bisq.apitest.OrderedRunner; +import bisq.apitest.annotation.Order; + +@Slf4j +@RunWith(OrderedRunner.class) +public class GetVersionTest extends MethodTest { + + @BeforeClass + public static void setUp() { + setUpScaffold(); + } + + @Test + @Order(1) + public void testGetVersion() { + var version = grpcStubs.versionService.getVersion(GetVersionRequest.newBuilder().build()).getVersion(); + assertEquals(VERSION, version); + } + + @AfterClass + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTest.java b/apitest/src/main/java/bisq/apitest/method/MethodTest.java new file mode 100644 index 00000000000..3fe700a0b89 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/method/MethodTest.java @@ -0,0 +1,68 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method; + +import bisq.proto.grpc.GetBalanceRequest; +import bisq.proto.grpc.RemoveWalletPasswordRequest; +import bisq.proto.grpc.SetWalletPasswordRequest; + +import static bisq.apitest.config.BisqAppConfig.alicedaemon; + + + +import bisq.apitest.ApiTestCase; +import bisq.apitest.GrpcStubs; +import bisq.apitest.Scaffold; +import bisq.apitest.config.ApiTestConfig; + +public class MethodTest extends ApiTestCase { + + static Scaffold scaffold; + static ApiTestConfig config; + static GrpcStubs grpcStubs; + + public static void setUpScaffold() { + scaffold = new Scaffold(new String[]{}).setUp(); + config = scaffold.config; + grpcStubs = new GrpcStubs(alicedaemon, config).init(); + } + + public static void tearDownScaffold() { + scaffold.tearDown(); + } + + // Convenience methods for building gRPC request objects + + protected final GetBalanceRequest createBalanceRequest() { + return GetBalanceRequest.newBuilder().build(); + } + + protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) { + return SetWalletPasswordRequest.newBuilder().setPassword(password).build(); + } + + protected final RemoveWalletPasswordRequest createRemoveWalletPasswordRequest(String password) { + return RemoveWalletPasswordRequest.newBuilder().setPassword("password").build(); + } + + // Convenience methods for calling frequently used & thoroughly tested gRPC services. + + protected final long getBalance() { + return grpcStubs.walletsService.getBalance(createBalanceRequest()).getBalance(); + } +} diff --git a/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java new file mode 100644 index 00000000000..bc833b36865 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java @@ -0,0 +1,58 @@ +package bisq.apitest.method; + +import io.grpc.StatusRuntimeException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; + + + +import bisq.apitest.OrderedRunner; +import bisq.apitest.annotation.Order; + +@Slf4j +@RunWith(OrderedRunner.class) +public class WalletProtectionTest extends MethodTest { + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + @BeforeClass + public static void setUp() { + setUpScaffold(); + } + + @Test + @Order(1) + public void testSetWalletPassword() { + var setPasswordRequest = createSetWalletPasswordRequest("password"); + grpcStubs.walletsService.setWalletPassword(setPasswordRequest); + } + + @Test + @Order(2) + public void testGetBalanceOnEncryptedWalletShouldThrowException() { + exceptionRule.expect(StatusRuntimeException.class); + exceptionRule.expectMessage("UNKNOWN: wallet is locked"); + getBalance(); + } + + @Test + @Order(3) + public void testRemoveWalletPassword() { + var removePasswordRequest = createRemoveWalletPasswordRequest("password"); + grpcStubs.walletsService.removeWalletPassword(removePasswordRequest); + getBalance(); // should not throw exception + } + + @AfterClass + public static void tearDown() { + tearDownScaffold(); + } +} From 84af0924010b2225e6f899a098e6ebce8c38ac3e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 13 Jul 2020 22:17:05 -0300 Subject: [PATCH 38/88] Add driver for running method tests API test cases are not in a maven/gradle project test folder. IDEs may not automatically configure JUnit test launchers, and a gradle build command will not automatically run tests. This class is provided as a convenience until gradle tasks for running test cases are implemented. --- .../bisq/apitest/method/MethodTestMain.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 apitest/src/main/java/bisq/apitest/method/MethodTestMain.java diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java b/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java new file mode 100644 index 00000000000..19daa8fe66c --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java @@ -0,0 +1,63 @@ +package bisq.apitest.method; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.runner.Description; +import org.junit.runner.JUnitCore; +import org.junit.runner.Result; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; + +import static java.lang.String.format; + +/** + * Driver for running API method tests. + * + * This may not seem necessary, but test cases are contained in the apitest sub + * project's main sources, not its test sources. An IDE will not automatically configure + * JUnit test launchers, and a gradle build will not automatically run the test cases. + * + * However, it is easy to manually configure an IDE launcher to run all, some or one + * JUnit test, and new gradle tasks should be provided to run all, some, or one test. + */ +@Slf4j +public class MethodTestMain { + + public static void main(String[] args) { + JUnitCore jUnitCore = new JUnitCore(); + jUnitCore.addListener(new RunListener() { + public void testStarted(Description description) { + log.info("{}", description); + } + + public void testIgnored(Description description) { + log.info("Ignored {}", description); + } + + public void testFailure(Failure failure) { + log.error("Failed {}", failure.getTrace()); + } + }); + Result result = jUnitCore.run(GetVersionTest.class, GetBalanceTest.class, WalletProtectionTest.class); + ResultUtil.printResult(result); + } + + private static class ResultUtil { + public static void printResult(Result result) { + log.info("Total tests: {}, Failed: {}, Ignored: {}", + result.getRunCount(), + result.getFailureCount(), + result.getIgnoreCount()); + + if (result.wasSuccessful()) { + log.info("All tests passed", result.getRunCount()); + } else if (result.getFailureCount() > 0) { + log.error("{} test(s) failed", result.getFailureCount()); + result.getFailures().iterator().forEachRemaining(f -> log.error(format("%s.%s()%n\t%s", + f.getDescription().getTestClass().getName(), + f.getDescription().getMethodName(), + f.getTrace()))); + } + } + } +} From 1acf340f796944befbdefb2f9a06968ed0641cd7 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 14 Jul 2020 10:35:14 -0300 Subject: [PATCH 39/88] Get rid of references to removed @Skip annotation --- .../main/java/bisq/apitest/ApiTestCase.java | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/ApiTestCase.java b/apitest/src/main/java/bisq/apitest/ApiTestCase.java index b196c4a34fa..a46dd618714 100644 --- a/apitest/src/main/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/main/java/bisq/apitest/ApiTestCase.java @@ -17,43 +17,16 @@ package bisq.apitest; -import java.util.function.Predicate; - import java.lang.reflect.Method; import static java.lang.String.format; import static java.util.concurrent.TimeUnit.MILLISECONDS; - - -import bisq.apitest.annotation.Skip; - public class ApiTestCase { protected static final char CHECK = '\u2714'; protected static final char CROSS_MARK = '\u274c'; - public int countTestCases; - public int countFailedTestCases; - public int countSkippedTestCases; - public int countPassedTestCases; - - private final Predicate> skipAll = (c) -> c.getAnnotation(Skip.class) != null; - private final Predicate skip = (m) -> m.getAnnotation(Skip.class) != null; - - protected boolean isSkipped(String methodName) { - try { - if (skipAll.test(this.getClass()) || skip.test(getMethod(methodName))) { - countSkippedTestCases++; - return true; - } else { - return false; - } - } finally { - countTestCases++; // Increment the test case count, skipped or not. - } - } - protected Method getMethod(String methodName) { try { return this.getClass().getMethod(methodName); @@ -63,15 +36,7 @@ protected Method getMethod(String methodName) { ex); } } - - protected String reportString() { - return format("Total %d Passed %d Failed %d Skipped %d", - countTestCases, - countPassedTestCases, - countFailedTestCases, - countSkippedTestCases); - } - + protected void sleep(long ms) { try { MILLISECONDS.sleep(ms); From 8ed44b8ceee4e77f3b5b53f06c205cb47757b19e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 14 Jul 2020 10:37:32 -0300 Subject: [PATCH 40/88] Remove @Skip annotaion We are using JUnit now and can use @Ignore --- .../java/bisq/apitest/annotation/Skip.java | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 apitest/src/main/java/bisq/apitest/annotation/Skip.java diff --git a/apitest/src/main/java/bisq/apitest/annotation/Skip.java b/apitest/src/main/java/bisq/apitest/annotation/Skip.java deleted file mode 100644 index a08784154d6..00000000000 --- a/apitest/src/main/java/bisq/apitest/annotation/Skip.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Sometimes you want to temporarily disable a test or a group of tests. Methods that - * are annotated with @Skip will not be executed as tests. - * Also, you can annotate a class containing test methods with @Skip - * and none of the containing tests will be executed. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface Skip { - /** - * The optional reason why the test is skipped. - */ - String value() default ""; -} From 45c1a97938a0ae896336a8b5e95ae704e557c58e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 14 Jul 2020 12:21:13 -0300 Subject: [PATCH 41/88] Fix codacy problem in bash script Double quote to prevent globbing and word splitting. --- apitest/scripts/get-bisq-pid.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apitest/scripts/get-bisq-pid.sh b/apitest/scripts/get-bisq-pid.sh index 0897cccb227..450f1290c00 100755 --- a/apitest/scripts/get-bisq-pid.sh +++ b/apitest/scripts/get-bisq-pid.sh @@ -12,4 +12,4 @@ APP_NAME=$2 # TODO args validation -ps aux | grep java | grep ${MAIN_CLASS_NAME} | grep ${APP_NAME} | awk '{print $2}' +ps aux | grep java | grep "${MAIN_CLASS_NAME}" | grep "${APP_NAME}" | awk '{print $2}' From 6b738f74439257d29815260e0134908b374f5cad Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 14 Jul 2020 12:37:13 -0300 Subject: [PATCH 42/88] Replace config 'numSetupTasks' with 'supportingApps' This change replaces the non-intuitive numSetupTasks= configuration option with a supportingApps= option. It accepts a comma delimited list of app names, and determines which background apps will be started in @BeforeClass. None of the current method tests need all supporting apps, and this change will reduce scaffolding setup and teardown time. The current method test cases were changed to use this option. WalletProtectionTest and GetVersionTest only need to start "alicedaemon". GetBalanceTest needs "bitcoind,seednode,arbdaemon,alicedaemon". --- .../main/java/bisq/apitest/ApiTestCase.java | 2 +- .../src/main/java/bisq/apitest/Scaffold.java | 74 +++++++++++-------- .../bisq/apitest/config/ApiTestConfig.java | 22 ++++-- .../bisq/apitest/method/GetBalanceTest.java | 2 +- .../bisq/apitest/method/GetVersionTest.java | 3 +- .../java/bisq/apitest/method/MethodTest.java | 8 ++ .../bisq/apitest/method/MethodTestMain.java | 2 +- .../apitest/method/WalletProtectionTest.java | 4 +- 8 files changed, 72 insertions(+), 45 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/ApiTestCase.java b/apitest/src/main/java/bisq/apitest/ApiTestCase.java index a46dd618714..65cd3b57af0 100644 --- a/apitest/src/main/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/main/java/bisq/apitest/ApiTestCase.java @@ -36,7 +36,7 @@ protected Method getMethod(String methodName) { ex); } } - + protected void sleep(long ms) { try { MILLISECONDS.sleep(ms); diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index 8ad789e7ff2..ddee850a6bb 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -91,14 +91,34 @@ public class Scaffold { private final ExecutorService executor; + /** + * Constructor for passing comma delimited list of supporting apps to + * ApiTestConfig, e.g., "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon". + * + * @param supportingApps String + */ + public Scaffold(String supportingApps) { + this(new ApiTestConfig(new String[]{"--supportingApps", supportingApps})); + } + + /** + * Constructor for passing options accepted by ApiTestConfig. + * + * @param args String[] + */ public Scaffold(String[] args) { this(new ApiTestConfig(args)); } + /** + * Constructor for passing ApiTestConfig instance. + * + * @param config ApiTestConfig + */ public Scaffold(ApiTestConfig config) { verifyNotWindows(); this.config = config; - this.executor = Executors.newFixedThreadPool(config.numSetupTasks); + this.executor = Executors.newFixedThreadPool(config.supportingApps.size()); if (config.helpRequested) { config.printHelp(out, new BisqHelpFormatter( @@ -115,7 +135,7 @@ public Scaffold setUp() { installDaoSetupDirectories(); // Start each background process from an executor, then add a shutdown hook. - CountDownLatch countdownLatch = new CountDownLatch(config.numSetupTasks); + CountDownLatch countdownLatch = new CountDownLatch(config.supportingApps.size()); startBackgroundProcesses(executor, countdownLatch); installShutdownHook(); @@ -139,7 +159,7 @@ public void tearDown() { try { log.info("Shutting down executor service ..."); executor.shutdownNow(); - executor.awaitTermination(config.numSetupTasks * 2000, MILLISECONDS); + executor.awaitTermination(config.supportingApps.size() * 2000, MILLISECONDS); SetupTask[] orderedTasks = new SetupTask[]{ bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask}; @@ -306,39 +326,29 @@ private void installShutdownHook() { private void startBackgroundProcesses(ExecutorService executor, CountDownLatch countdownLatch) throws InterruptedException, IOException { - // The configured number of setup tasks determines which bisq apps are started in - // the background, and in what order. - // - // If config.numSetupTasks = 0, no setup tasks are run. If 1, the bitcoind - // process is started in the background. If 2, bitcoind and seednode. - // If 3, bitcoind, seednode and arbnode are started. If 4, bitcoind, seednode, - // arbnode, and alicenode are started. If 5, bitcoind, seednode, arbnode, - // alicenode and bobnode are started. - // - // This affords an easier way to choose which setup tasks are run, rather than - // commenting and uncommenting code blocks. You have to remember seednode - // depends on bitcoind, arbnode on seednode, and that bob & alice cannot trade - // unless arbnode is running with a registered mediator and refund agent. - if (config.numSetupTasks > 0) { + + log.info("Starting supporting apps {}", config.supportingApps.toString()); + + if (config.hasSupportingApp("bitcoind")) { BitcoinDaemon bitcoinDaemon = new BitcoinDaemon(config); bitcoinDaemon.verifyBitcoinConfig(true); bitcoindTask = new SetupTask(bitcoinDaemon, countdownLatch); bitcoindTaskFuture = executor.submit(bitcoindTask); - SECONDS.sleep(5); + MILLISECONDS.sleep(3500); bitcoinDaemon.verifyBitcoindRunning(); } - if (config.numSetupTasks > 1) { + + if (config.hasSupportingApp(seednode.name())) startBisqApp(seednode, executor, countdownLatch); - } - if (config.numSetupTasks > 2) { + + if (config.hasSupportingApp(arbdaemon.name(), arbdesktop.name())) startBisqApp(config.runArbNodeAsDesktop ? arbdesktop : arbdaemon, executor, countdownLatch); - } - if (config.numSetupTasks > 3) { + + if (config.hasSupportingApp(alicedaemon.name(), alicedesktop.name())) startBisqApp(config.runAliceNodeAsDesktop ? alicedesktop : alicedaemon, executor, countdownLatch); - } - if (config.numSetupTasks > 4) { + + if (config.hasSupportingApp(bobdaemon.name(), bobdesktop.name())) startBisqApp(config.runBobNodeAsDesktop ? bobdesktop : bobdaemon, executor, countdownLatch); - } } private void startBisqApp(BisqAppConfig bisqAppConfig, @@ -393,19 +403,19 @@ private BisqApp createBisqApp(BisqAppConfig bisqAppConfig) private void verifyStartupCompleted() throws ExecutionException, InterruptedException { - if (config.numSetupTasks > 0) + if (bitcoindTaskFuture != null) verifyStartupCompleted(bitcoindTaskFuture); - if (config.numSetupTasks > 1) + if (seedNodeTaskFuture != null) verifyStartupCompleted(seedNodeTaskFuture); - if (config.numSetupTasks > 2) + if (arbNodeTaskFuture != null) verifyStartupCompleted(arbNodeTaskFuture); - if (config.numSetupTasks > 3) + if (aliceNodeTaskFuture != null) verifyStartupCompleted(aliceNodeTaskFuture); - if (config.numSetupTasks > 4) + if (bobNodeTaskFuture != null) verifyStartupCompleted(bobNodeTaskFuture); } @@ -422,7 +432,7 @@ private void verifyStartupCompleted(Future futureStatus) // We are giving the thread more time to terminate after the countdown // latch reached 0. If we are running only bitcoind, we need to be even // more lenient. - SECONDS.sleep(config.numSetupTasks == 1 ? 2 : 1); + SECONDS.sleep(config.supportingApps.size() == 1 ? 2 : 1); } } throw new IllegalStateException(format("%s did not complete startup", futureStatus.get().getName())); diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index 3532a937055..a46f1e6ec3f 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -43,6 +43,8 @@ import static java.lang.String.format; import static java.lang.System.getProperty; import static java.lang.System.getenv; +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; import static joptsimple.internal.Strings.EMPTY; @Slf4j @@ -66,7 +68,7 @@ public class ApiTestConfig { static final String RUN_BOB_NODE_AS_DESKTOP = "runBobNodeAsDesktop"; static final String SKIP_TESTS = "skipTests"; static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests"; - static final String NUM_SETUP_TASKS = "numSetupTasks"; + static final String SUPPORTING_APPS = "supportingApps"; // Default values for certain options static final String DEFAULT_CONFIG_FILE_NAME = "apitest.properties"; @@ -98,7 +100,7 @@ public class ApiTestConfig { public final boolean runBobNodeAsDesktop; public final boolean skipTests; public final boolean shutdownAfterTests; - public final int numSetupTasks; + public final List supportingApps; // Immutable system configurations. public final String bitcoinDatadir; @@ -219,12 +221,12 @@ public ApiTestConfig(String... args) { .ofType(Boolean.class) .defaultsTo(true); - ArgumentAcceptingOptionSpec numSetupTasksOpt = - parser.accepts(NUM_SETUP_TASKS, - "Number of test setup tasks") + ArgumentAcceptingOptionSpec supportingAppsOpt = + parser.accepts(SUPPORTING_APPS, + "Comma delimited list of supporting apps (bitcoind,seednode,arbdaemon,...") .withRequiredArg() - .ofType(Integer.class) - .defaultsTo(4); + .ofType(String.class) + .defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon"); try { CompositeOptionSet options = new CompositeOptionSet(); @@ -282,7 +284,7 @@ public ApiTestConfig(String... args) { this.runBobNodeAsDesktop = options.valueOf(runBobNodeAsDesktopOpt); this.skipTests = options.valueOf(skipTestsOpt); this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt); - this.numSetupTasks = options.valueOf(numSetupTasksOpt); + this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(",")); // Assign values to special-case static fields. BASH_PATH_VALUE = bashPath; @@ -296,6 +298,10 @@ public ApiTestConfig(String... args) { } } + public boolean hasSupportingApp(String... supportingApp) { + return stream(supportingApp).anyMatch(a -> this.supportingApps.contains(a)); + } + public void printHelp(OutputStream sink, HelpFormatter formatter) { try { parser.formatHelpWith(formatter); diff --git a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java index fb4d49d32c6..2ae85820b23 100644 --- a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java +++ b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java @@ -46,7 +46,7 @@ public class GetBalanceTest extends MethodTest { @BeforeClass public static void setUp() { try { - setUpScaffold(); + setUpScaffold("bitcoind,seednode,arbdaemon,alicedaemon"); String newAddress = new BitcoinCli(config, "getnewaddress").run().getOutput(); String generateToAddressCmd = format("generatetoaddress %d \"%s\"", 1, newAddress); diff --git a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java index 2e974597baf..ab262db6e3a 100644 --- a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java +++ b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java @@ -26,6 +26,7 @@ import org.junit.Test; import org.junit.runner.RunWith; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static bisq.common.app.Version.VERSION; import static org.junit.Assert.assertEquals; @@ -40,7 +41,7 @@ public class GetVersionTest extends MethodTest { @BeforeClass public static void setUp() { - setUpScaffold(); + setUpScaffold(alicedaemon.name()); } @Test diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTest.java b/apitest/src/main/java/bisq/apitest/method/MethodTest.java index 3fe700a0b89..3bb4e6c24f6 100644 --- a/apitest/src/main/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/main/java/bisq/apitest/method/MethodTest.java @@ -36,6 +36,14 @@ public class MethodTest extends ApiTestCase { static ApiTestConfig config; static GrpcStubs grpcStubs; + public static void setUpScaffold(String supportingApps) { + // The supportingApps argument is a comma delimited string of supporting app + // names, e.g. "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon" + scaffold = new Scaffold(supportingApps).setUp(); + config = scaffold.config; + grpcStubs = new GrpcStubs(alicedaemon, config).init(); + } + public static void setUpScaffold() { scaffold = new Scaffold(new String[]{}).setUp(); config = scaffold.config; diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java b/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java index 19daa8fe66c..504520b6337 100644 --- a/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java +++ b/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java @@ -50,7 +50,7 @@ public static void printResult(Result result) { result.getIgnoreCount()); if (result.wasSuccessful()) { - log.info("All tests passed", result.getRunCount()); + log.info("All tests passed"); } else if (result.getFailureCount() > 0) { log.error("{} test(s) failed", result.getFailureCount()); result.getFailures().iterator().forEachRemaining(f -> log.error(format("%s.%s()%n\t%s", diff --git a/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java index bc833b36865..d101059ef00 100644 --- a/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java +++ b/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java @@ -11,6 +11,8 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; + import bisq.apitest.OrderedRunner; @@ -25,7 +27,7 @@ public class WalletProtectionTest extends MethodTest { @BeforeClass public static void setUp() { - setUpScaffold(); + setUpScaffold(alicedaemon.name()); } @Test From 498939a9fff0eaeb2b2bb49f144674fc84363ec7 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 14 Jul 2020 12:41:41 -0300 Subject: [PATCH 43/88] Allow more time for background app shutdown --- .../main/java/bisq/apitest/linux/BisqApp.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java index 651766efd57..7a008cc2b2d 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java +++ b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java @@ -98,14 +98,20 @@ public void shutdown() throws IOException, InterruptedException { if (new BashCommand(killCmd).run().getExitStatus() != 0) throw new IllegalStateException(format("Could not shut down %s", bisqAppConfig.appName)); - MILLISECONDS.sleep(4000); // allow it time to shutdown - log.info("{} stopped", bisqAppConfig.appName); - } catch (Exception e) { - throw new IllegalStateException(format("Error shutting down %s", bisqAppConfig.appName), e); - } finally { + // Be lenient about the time it takes for a java app to shut down. + for (int i = 0; i < 5; i++) { + if (!isAlive(pid)) { + log.info("{} stopped", bisqAppConfig.appName); + break; + } + MILLISECONDS.sleep(2500); + } + if (isAlive(pid)) - //noinspection ThrowFromFinallyBlock throw new IllegalStateException(format("%s shutdown did not work", bisqAppConfig.appName)); + + } catch (Exception e) { + throw new IllegalStateException(format("Error shutting down %s", bisqAppConfig.appName), e); } } From 5df0b1ec4bbfee3f5c2993099ef430e3c78c9e30 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 14 Jul 2020 14:20:26 -0300 Subject: [PATCH 44/88] Refactor ApiTestCase class hierarchy Also added a new base ScenarioTest class, which extends MethodTest; Method and Scenario tests cases have access to gRPC service stubs, but end to end test cases should never use them. --- .../main/java/bisq/apitest/ApiTestCase.java | 43 +++++++++++++------ .../java/bisq/apitest/method/MethodTest.java | 27 ------------ .../apitest/method/WalletProtectionTest.java | 5 --- .../bisq/apitest/scenario/ScenarioTest.java | 6 +++ 4 files changed, 37 insertions(+), 44 deletions(-) create mode 100644 apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java diff --git a/apitest/src/main/java/bisq/apitest/ApiTestCase.java b/apitest/src/main/java/bisq/apitest/ApiTestCase.java index 65cd3b57af0..841d5e60bae 100644 --- a/apitest/src/main/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/main/java/bisq/apitest/ApiTestCase.java @@ -17,24 +17,43 @@ package bisq.apitest; -import java.lang.reflect.Method; +import org.junit.Rule; +import org.junit.rules.ExpectedException; -import static java.lang.String.format; +import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static java.util.concurrent.TimeUnit.MILLISECONDS; + + +import bisq.apitest.config.ApiTestConfig; + public class ApiTestCase { - protected static final char CHECK = '\u2714'; - protected static final char CROSS_MARK = '\u274c'; + // The gRPC service stubs are used by method & scenario tests, but not e2e tests. + protected static GrpcStubs grpcStubs; - protected Method getMethod(String methodName) { - try { - return this.getClass().getMethod(methodName); - } catch (NoSuchMethodException ex) { - throw new IllegalStateException(format("No method '%s' exists in class '%s'", - methodName, this.getClass().getName()), - ex); - } + protected static Scaffold scaffold; + protected static ApiTestConfig config; + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + public static void setUpScaffold(String supportingApps) { + // The supportingApps argument is a comma delimited string of supporting app + // names, e.g. "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon" + scaffold = new Scaffold(supportingApps).setUp(); + config = scaffold.config; + grpcStubs = new GrpcStubs(alicedaemon, config).init(); + } + + public static void setUpScaffold() { + scaffold = new Scaffold(new String[]{}).setUp(); + config = scaffold.config; + grpcStubs = new GrpcStubs(alicedaemon, config).init(); + } + + public static void tearDownScaffold() { + scaffold.tearDown(); } protected void sleep(long ms) { diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTest.java b/apitest/src/main/java/bisq/apitest/method/MethodTest.java index 3bb4e6c24f6..9fa1db888aa 100644 --- a/apitest/src/main/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/main/java/bisq/apitest/method/MethodTest.java @@ -21,39 +21,12 @@ import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest; -import static bisq.apitest.config.BisqAppConfig.alicedaemon; - import bisq.apitest.ApiTestCase; -import bisq.apitest.GrpcStubs; -import bisq.apitest.Scaffold; -import bisq.apitest.config.ApiTestConfig; public class MethodTest extends ApiTestCase { - static Scaffold scaffold; - static ApiTestConfig config; - static GrpcStubs grpcStubs; - - public static void setUpScaffold(String supportingApps) { - // The supportingApps argument is a comma delimited string of supporting app - // names, e.g. "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon" - scaffold = new Scaffold(supportingApps).setUp(); - config = scaffold.config; - grpcStubs = new GrpcStubs(alicedaemon, config).init(); - } - - public static void setUpScaffold() { - scaffold = new Scaffold(new String[]{}).setUp(); - config = scaffold.config; - grpcStubs = new GrpcStubs(alicedaemon, config).init(); - } - - public static void tearDownScaffold() { - scaffold.tearDown(); - } - // Convenience methods for building gRPC request objects protected final GetBalanceRequest createBalanceRequest() { diff --git a/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java index d101059ef00..75429850bab 100644 --- a/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java +++ b/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java @@ -6,9 +6,7 @@ import org.junit.AfterClass; import org.junit.BeforeClass; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import static bisq.apitest.config.BisqAppConfig.alicedaemon; @@ -22,9 +20,6 @@ @RunWith(OrderedRunner.class) public class WalletProtectionTest extends MethodTest { - @Rule - public ExpectedException exceptionRule = ExpectedException.none(); - @BeforeClass public static void setUp() { setUpScaffold(alicedaemon.name()); diff --git a/apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java b/apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java new file mode 100644 index 00000000000..782b237c3e9 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java @@ -0,0 +1,6 @@ +package bisq.apitest.scenario; + +import bisq.apitest.method.MethodTest; + +public class ScenarioTest extends MethodTest { +} From 2cf7915f252b0ce205f4a5cc3601f7273fee55c9 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 14 Jul 2020 15:04:42 -0300 Subject: [PATCH 45/88] Add wallet protect method tests Some of these 'method' tests make more than one gRPC call, but they are for checking correctness of a single gRPC method, and don't quite fall into the 'scenario' test category. --- .../java/bisq/apitest/method/MethodTest.java | 18 +++++ .../apitest/method/WalletProtectionTest.java | 65 +++++++++++++++++-- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTest.java b/apitest/src/main/java/bisq/apitest/method/MethodTest.java index 9fa1db888aa..e8e30bf81c8 100644 --- a/apitest/src/main/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/main/java/bisq/apitest/method/MethodTest.java @@ -18,8 +18,10 @@ package bisq.apitest.method; import bisq.proto.grpc.GetBalanceRequest; +import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest; +import bisq.proto.grpc.UnlockWalletRequest; @@ -41,9 +43,25 @@ protected final RemoveWalletPasswordRequest createRemoveWalletPasswordRequest(St return RemoveWalletPasswordRequest.newBuilder().setPassword("password").build(); } + protected final UnlockWalletRequest createUnlockWalletRequest(String password, long timeout) { + return UnlockWalletRequest.newBuilder().setPassword(password).setTimeout(timeout).build(); + } + + protected final LockWalletRequest createLockWalletRequest() { + return LockWalletRequest.newBuilder().build(); + } + // Convenience methods for calling frequently used & thoroughly tested gRPC services. protected final long getBalance() { return grpcStubs.walletsService.getBalance(createBalanceRequest()).getBalance(); } + + protected final void unlockWallet(String password, long timeout) { + grpcStubs.walletsService.unlockWallet(createUnlockWalletRequest(password, timeout)); + } + + protected final void lockWallet() { + grpcStubs.walletsService.lockWallet(createLockWalletRequest()); + } } diff --git a/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java index 75429850bab..621b973137b 100644 --- a/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java +++ b/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java @@ -28,8 +28,8 @@ public static void setUp() { @Test @Order(1) public void testSetWalletPassword() { - var setPasswordRequest = createSetWalletPasswordRequest("password"); - grpcStubs.walletsService.setWalletPassword(setPasswordRequest); + var request = createSetWalletPasswordRequest("password"); + grpcStubs.walletsService.setWalletPassword(request); } @Test @@ -42,10 +42,65 @@ public void testGetBalanceOnEncryptedWalletShouldThrowException() { @Test @Order(3) + public void testUnlockWalletFor4Seconds() { + var request = createUnlockWalletRequest("password", 4); + grpcStubs.walletsService.unlockWallet(request); + getBalance(); // should not throw 'wallet locked' exception + + sleep(4500); // let unlock timeout expire + exceptionRule.expect(StatusRuntimeException.class); + exceptionRule.expectMessage("UNKNOWN: wallet is locked"); + getBalance(); + } + + @Test + @Order(4) + public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() { + var request = createUnlockWalletRequest("password", 3); + grpcStubs.walletsService.unlockWallet(request); + sleep(4000); // let unlock timeout expire + exceptionRule.expect(StatusRuntimeException.class); + exceptionRule.expectMessage("UNKNOWN: wallet is locked"); + getBalance(); + } + + @Test + @Order(5) + public void testLockWalletBeforeUnlockTimeoutExpiry() { + unlockWallet("password", 60); + var request = createLockWalletRequest(); + grpcStubs.walletsService.lockWallet(request); + + exceptionRule.expect(StatusRuntimeException.class); + exceptionRule.expectMessage("UNKNOWN: wallet is locked"); + getBalance(); + } + + @Test + @Order(6) + public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() { + exceptionRule.expect(StatusRuntimeException.class); + exceptionRule.expectMessage("UNKNOWN: wallet is already locked"); + var request = createLockWalletRequest(); + grpcStubs.walletsService.lockWallet(request); + } + + @Test + @Order(7) + public void testUnlockWalletTimeoutOverride() { + unlockWallet("password", 2); + sleep(500); // override unlock timeout after 0.5s + unlockWallet("password", 6); + sleep(5000); + getBalance(); // getbalance 5s after resetting unlock timeout to 6s + } + + @Test + @Order(8) public void testRemoveWalletPassword() { - var removePasswordRequest = createRemoveWalletPasswordRequest("password"); - grpcStubs.walletsService.removeWalletPassword(removePasswordRequest); - getBalance(); // should not throw exception + var request = createRemoveWalletPasswordRequest("password"); + grpcStubs.walletsService.removeWalletPassword(request); + getBalance(); // should not throw 'wallet locked' exception } @AfterClass From 28aefc5cb1b669b7bbd35becafaa2e48c6643fec Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 14 Jul 2020 16:17:10 -0300 Subject: [PATCH 46/88] Add tests for resetting a wallet password --- .../java/bisq/apitest/method/MethodTest.java | 6 +++- .../apitest/method/WalletProtectionTest.java | 36 ++++++++++++++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTest.java b/apitest/src/main/java/bisq/apitest/method/MethodTest.java index e8e30bf81c8..5c8e7a64949 100644 --- a/apitest/src/main/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/main/java/bisq/apitest/method/MethodTest.java @@ -39,8 +39,12 @@ protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String p return SetWalletPasswordRequest.newBuilder().setPassword(password).build(); } + protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String oldPassword, String newPassword) { + return SetWalletPasswordRequest.newBuilder().setPassword(oldPassword).setNewPassword(newPassword).build(); + } + protected final RemoveWalletPasswordRequest createRemoveWalletPasswordRequest(String password) { - return RemoveWalletPasswordRequest.newBuilder().setPassword("password").build(); + return RemoveWalletPasswordRequest.newBuilder().setPassword(password).build(); } protected final UnlockWalletRequest createUnlockWalletRequest(String password, long timeout) { diff --git a/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java index 621b973137b..4183cb6d98c 100644 --- a/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java +++ b/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java @@ -28,7 +28,7 @@ public static void setUp() { @Test @Order(1) public void testSetWalletPassword() { - var request = createSetWalletPasswordRequest("password"); + var request = createSetWalletPasswordRequest("first-password"); grpcStubs.walletsService.setWalletPassword(request); } @@ -43,7 +43,7 @@ public void testGetBalanceOnEncryptedWalletShouldThrowException() { @Test @Order(3) public void testUnlockWalletFor4Seconds() { - var request = createUnlockWalletRequest("password", 4); + var request = createUnlockWalletRequest("first-password", 4); grpcStubs.walletsService.unlockWallet(request); getBalance(); // should not throw 'wallet locked' exception @@ -56,7 +56,7 @@ public void testUnlockWalletFor4Seconds() { @Test @Order(4) public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() { - var request = createUnlockWalletRequest("password", 3); + var request = createUnlockWalletRequest("first-password", 3); grpcStubs.walletsService.unlockWallet(request); sleep(4000); // let unlock timeout expire exceptionRule.expect(StatusRuntimeException.class); @@ -67,7 +67,7 @@ public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() { @Test @Order(5) public void testLockWalletBeforeUnlockTimeoutExpiry() { - unlockWallet("password", 60); + unlockWallet("first-password", 60); var request = createLockWalletRequest(); grpcStubs.walletsService.lockWallet(request); @@ -88,17 +88,37 @@ public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() { @Test @Order(7) public void testUnlockWalletTimeoutOverride() { - unlockWallet("password", 2); + unlockWallet("first-password", 2); sleep(500); // override unlock timeout after 0.5s - unlockWallet("password", 6); + unlockWallet("first-password", 6); sleep(5000); getBalance(); // getbalance 5s after resetting unlock timeout to 6s } @Test @Order(8) - public void testRemoveWalletPassword() { - var request = createRemoveWalletPasswordRequest("password"); + public void testSetNewWalletPassword() { + var request = createSetWalletPasswordRequest("first-password", "second-password"); + grpcStubs.walletsService.setWalletPassword(request); + + unlockWallet("second-password", 2); + getBalance(); + sleep(2500); // allow time for wallet save + } + + @Test + @Order(9) + public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() { + exceptionRule.expect(StatusRuntimeException.class); + exceptionRule.expectMessage("UNKNOWN: incorrect old password"); + var request = createSetWalletPasswordRequest("bad old password", "irrelevant"); + grpcStubs.walletsService.setWalletPassword(request); + } + + @Test + @Order(10) + public void testRemoveNewWalletPassword() { + var request = createRemoveWalletPasswordRequest("second-password"); grpcStubs.walletsService.removeWalletPassword(request); getBalance(); // should not throw 'wallet locked' exception } From a00bc4b414de79720b4c57cc0c2ca2efd63bba22 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 15 Jul 2020 10:45:05 -0300 Subject: [PATCH 47/88] Add --bisqAppInitTime= config option This change makes configurable the amount of time (ms) each Bisq instance is given to initialize before starting (a) another Bisq instance, or (b) tests. --- apitest/src/main/java/bisq/apitest/Scaffold.java | 5 +++-- .../main/java/bisq/apitest/config/ApiTestConfig.java | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index ddee850a6bb..3951aee9494 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -382,9 +382,10 @@ private void startBisqApp(BisqAppConfig bisqAppConfig, bobNodeTaskFuture = executor.submit(bobNodeTask); break; default: - throw new IllegalStateException("Unknown Bisq App " + bisqAppConfig.appName); + throw new IllegalStateException("Unknown BisqAppConfig " + bisqAppConfig.name()); } - SECONDS.sleep(5); + log.info("Giving {} ms for {} to initialize ...", config.bisqAppInitTime, bisqAppConfig.appName); + MILLISECONDS.sleep(config.bisqAppInitTime); if (bisqApp.hasStartupExceptions()) { for (Throwable t : bisqApp.getStartupExceptions()) { log.error("", t); diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index a46f1e6ec3f..78bcdeea7f7 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -66,6 +66,7 @@ public class ApiTestConfig { static final String RUN_ARB_NODE_AS_DESKTOP = "runArbNodeAsDesktop"; static final String RUN_ALICE_NODE_AS_DESKTOP = "runAliceNodeAsDesktop"; static final String RUN_BOB_NODE_AS_DESKTOP = "runBobNodeAsDesktop"; + static final String BISQ_APP_INIT_TIME = "bisqAppInitTime"; static final String SKIP_TESTS = "skipTests"; static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests"; static final String SUPPORTING_APPS = "supportingApps"; @@ -98,6 +99,7 @@ public class ApiTestConfig { public final boolean runArbNodeAsDesktop; public final boolean runAliceNodeAsDesktop; public final boolean runBobNodeAsDesktop; + public final long bisqAppInitTime; public final boolean skipTests; public final boolean shutdownAfterTests; public final List supportingApps; @@ -207,6 +209,13 @@ public ApiTestConfig(String... args) { .ofType(Boolean.class) .defaultsTo(false); + ArgumentAcceptingOptionSpec bisqAppInitTimeOpt = + parser.accepts(BISQ_APP_INIT_TIME, + "Amount of time (ms) to wait for a Bisq instance's initialization") + .withRequiredArg() + .ofType(Long.class) + .defaultsTo(4000L); + ArgumentAcceptingOptionSpec skipTestsOpt = parser.accepts(SKIP_TESTS, "Start apps, but skip tests") @@ -282,6 +291,7 @@ public ApiTestConfig(String... args) { this.runArbNodeAsDesktop = options.valueOf(runArbNodeAsDesktopOpt); this.runAliceNodeAsDesktop = options.valueOf(runAliceNodeAsDesktopOpt); this.runBobNodeAsDesktop = options.valueOf(runBobNodeAsDesktopOpt); + this.bisqAppInitTime = options.valueOf(bisqAppInitTimeOpt); this.skipTests = options.valueOf(skipTestsOpt); this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt); this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(",")); From cf3b54517337a22568ee5883eebb135059a6fe79 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 15 Jul 2020 10:56:35 -0300 Subject: [PATCH 48/88] Fix hardcoded bitcoin.conf property values --- apitest/src/main/java/bisq/apitest/Scaffold.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index 3951aee9494..d2d8b077791 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -263,13 +263,13 @@ private void installBitcoinConf() { + "regtest=1\n" + "[regtest]\n" + "peerbloomfilters=1\n" - + "rpcport=18443\n" + + "rpcport=" + config.bitcoinRpcPort + "\n" + "server=1\n" + "txindex=1\n" + "debug=net\n" + "deprecatedrpc=generate\n" - + "rpcuser=apitest\n" - + "rpcpassword=apitest\n" + + "rpcuser=" + config.bitcoinRpcUser + "\n" + + "rpcpassword=" + config.bitcoinRpcPassword + "\n" + "blocknotify=" + config.bashPath + " " + config.bitcoinDatadir + "/blocknotify %\n"; String chmod644Perms = "rw-r--r--"; saveToFile(bitcoinConf, config.bitcoinDatadir, "bitcoin.conf", chmod644Perms); From d108d89bb2f2881b45f361f6a5e1a973acaf16eb Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 15 Jul 2020 11:13:00 -0300 Subject: [PATCH 49/88] Fix comment and code styling --- apitest/src/main/java/bisq/apitest/OrderedRunner.java | 6 ++---- apitest/src/main/java/bisq/apitest/annotation/Order.java | 3 --- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/OrderedRunner.java b/apitest/src/main/java/bisq/apitest/OrderedRunner.java index 5b4faa61cf5..d206987e75e 100644 --- a/apitest/src/main/java/bisq/apitest/OrderedRunner.java +++ b/apitest/src/main/java/bisq/apitest/OrderedRunner.java @@ -28,7 +28,7 @@ import bisq.apitest.annotation.Order; -// Credit to Aman Goel +// Credit to Aman Goel for providing an example of ordering JUnit tests at // https://stackoverflow.com/questions/3693626/how-to-run-test-methods-in-specific-order-in-junit4 public class OrderedRunner extends BlockJUnit4ClassRunner { @@ -44,10 +44,8 @@ protected List computeTestMethods() { copy.sort((f1, f2) -> { Order o1 = f1.getAnnotation(Order.class); Order o2 = f2.getAnnotation(Order.class); - - if (o1 == null || o2 == null) { + if (o1 == null || o2 == null) return -1; - } return o1.value() - o2.value(); }); diff --git a/apitest/src/main/java/bisq/apitest/annotation/Order.java b/apitest/src/main/java/bisq/apitest/annotation/Order.java index 1d0bd2021d2..428f9ac19ac 100644 --- a/apitest/src/main/java/bisq/apitest/annotation/Order.java +++ b/apitest/src/main/java/bisq/apitest/annotation/Order.java @@ -22,9 +22,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -// Credit to Aman Goel -// https://stackoverflow.com/questions/3693626/how-to-run-test-methods-in-specific-order-in-junit4 - @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Order { From c19afebc048fdbf1af706c22d1320878ba8dbe8c Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 15 Jul 2020 19:26:16 -0300 Subject: [PATCH 50/88] Fix 'bitcoind not found' error message --- .../java/bisq/apitest/linux/AbstractLinuxProcess.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index 018aba40f73..d644087a2ef 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -17,6 +17,8 @@ package bisq.apitest.linux; +import java.nio.file.Paths; + import java.io.File; import java.io.IOException; @@ -70,9 +72,13 @@ public void verifyBitcoinConfig(boolean verbose) { throw new IllegalStateException(berkeleyDbLibPath + " cannot be found or executed"); } - File bitcoindExecutable = new File(config.bitcoinPath); + File bitcoindExecutable = Paths.get(config.bitcoinPath, "bitcoind").toFile(); if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute()) - throw new IllegalStateException(bitcoindExecutable + " cannot be found or executed"); + throw new IllegalStateException(format("'%s' cannot be found or executed.%n" + + "A bitcoin-core v0.19.X installation is required, and" + + " a '--bitcoinPath' option must be passed on the command line" + + " or added to 'apitest.properties'", + bitcoindExecutable.getAbsolutePath())); File bitcoindDatadir = new File(config.bitcoinDatadir); if (!bitcoindDatadir.exists() || !bitcoindDatadir.canWrite()) From b2417d3e5f326f54dcd95b1ce136d63cc026ee9f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 15 Jul 2020 20:01:30 -0300 Subject: [PATCH 51/88] Delete throws clause from method signature --- apitest/src/main/java/bisq/apitest/linux/BashCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/BashCommand.java b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java index 725e325ea7c..688358b5da5 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BashCommand.java +++ b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java @@ -65,7 +65,7 @@ public BashCommand runInBackground() throws IOException, InterruptedException { return this; } - private void processOutput(SystemCommandExecutor commandExecutor) throws IOException, InterruptedException { + private void processOutput(SystemCommandExecutor commandExecutor) { // Get the error status and stderr from system command. StringBuilder stderr = commandExecutor.getStandardErrorFromCommand(); if (stderr.length() > 0) From e0ea9db32ede7512abad818ab8b6ad628a096c04 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 15 Jul 2020 20:03:47 -0300 Subject: [PATCH 52/88] Fix bitcoind startup error handling Like Bisq instance startup, bitcoind startup should save any error msg from bash, print the error msg, and System.exit(1). --- apitest/src/main/java/bisq/apitest/SetupTask.java | 15 ++++++++++++--- .../bisq/apitest/linux/AbstractLinuxProcess.java | 15 +++++++++++++++ .../src/main/java/bisq/apitest/linux/BisqApp.java | 10 ---------- .../java/bisq/apitest/linux/BitcoinDaemon.java | 9 +++++++-- .../java/bisq/apitest/linux/LinuxProcess.java | 6 ++++++ 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/SetupTask.java b/apitest/src/main/java/bisq/apitest/SetupTask.java index da81c885084..56f3af07140 100644 --- a/apitest/src/main/java/bisq/apitest/SetupTask.java +++ b/apitest/src/main/java/bisq/apitest/SetupTask.java @@ -25,8 +25,10 @@ import lombok.extern.slf4j.Slf4j; +import static bisq.apitest.Scaffold.EXIT_FAILURE; import static java.lang.String.format; -import static java.util.concurrent.TimeUnit.SECONDS; +import static java.lang.System.exit; +import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -46,8 +48,15 @@ public SetupTask(LinuxProcess linuxProcess, CountDownLatch countdownLatch) { @Override public Status call() throws Exception { try { - linuxProcess.start(); // always runs in background - SECONDS.sleep(10); // give time for bg process to init + linuxProcess.start(); // always runs in background + MILLISECONDS.sleep(1000); // give 1s for bg process to init + if (linuxProcess.hasStartupExceptions()) { + for (Throwable t : linuxProcess.getStartupExceptions()) { + log.error("", t); + } + exit(EXIT_FAILURE); + } + } catch (InterruptedException ex) { throw new IllegalStateException(format("Error starting %s", linuxProcess.getName()), ex); } diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index d644087a2ef..3c5cfd4388f 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -22,6 +22,9 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + import lombok.extern.slf4j.Slf4j; import static bisq.apitest.linux.BashCommand.isAlive; @@ -40,10 +43,12 @@ abstract class AbstractLinuxProcess implements LinuxProcess { protected long pid; protected final ApiTestConfig config; + protected final List startupExceptions; public AbstractLinuxProcess(String name, ApiTestConfig config) { this.name = name; this.config = config; + this.startupExceptions = new ArrayList<>(); } @Override @@ -51,6 +56,16 @@ public String getName() { return this.name; } + @Override + public boolean hasStartupExceptions() { + return !startupExceptions.isEmpty(); + } + + @Override + public List getStartupExceptions() { + return startupExceptions; + } + @SuppressWarnings("unused") public void verifyBitcoinConfig() { verifyBitcoinConfig(false); diff --git a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java index 7a008cc2b2d..f5c4fb48a0f 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java +++ b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java @@ -53,7 +53,6 @@ public class BisqApp extends AbstractLinuxProcess implements LinuxProcess { private final boolean useLocalhostForP2P; public final boolean useDevPrivilegeKeys; private final String findBisqPidScript; - private final List startupExceptions; public BisqApp(BisqAppConfig bisqAppConfig, ApiTestConfig config) { super(bisqAppConfig.appName, config); @@ -67,7 +66,6 @@ public BisqApp(BisqAppConfig bisqAppConfig, ApiTestConfig config) { this.useLocalhostForP2P = true; this.useDevPrivilegeKeys = true; this.findBisqPidScript = config.userDir + "/apitest/scripts/get-bisq-pid.sh"; - this.startupExceptions = new ArrayList<>(); } @Override @@ -140,14 +138,6 @@ public void verifyAppDataDirInstalled() { } } - public boolean hasStartupExceptions() { - return !startupExceptions.isEmpty(); - } - - public List getStartupExceptions() { - return startupExceptions; - } - // This is the non-default way of running a Bisq app (--runSubprojectJars=true). // It runs a java cmd, and does not depend on a full build. Bisq jars are loaded // from the :subproject/build/libs directories. diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java index bb29b630944..f4594a10ba4 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java @@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import static bisq.apitest.linux.BashCommand.isAlive; +import static java.lang.String.format; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static joptsimple.internal.Strings.EMPTY; @@ -60,8 +61,12 @@ public void start() throws InterruptedException, IOException { BashCommand cmd = new BashCommand(bitcoindCmd).run(); log.info("Starting ...\n$ {}", cmd.getCommand()); - if (cmd.getExitStatus() != 0) - throw new IllegalStateException("Error starting bitcoind:\n" + cmd.getError()); + if (cmd.getExitStatus() != 0) { + startupExceptions.add(new IllegalStateException( + format("Error starting bitcoind%nstatus: %d%nerror msg: %s", + cmd.getExitStatus(), cmd.getError()))); + return; + } pid = BashCommand.getPid("bitcoind"); if (!isAlive(pid)) diff --git a/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java index aa673c8803d..5c453de0b10 100644 --- a/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java @@ -19,6 +19,8 @@ import java.io.IOException; +import java.util.List; + public interface LinuxProcess { void start() throws InterruptedException, IOException; @@ -26,5 +28,9 @@ public interface LinuxProcess { long getPid(); + boolean hasStartupExceptions(); + + List getStartupExceptions(); + void shutdown() throws IOException, InterruptedException; } From 431cbe727f9bee25b5d479e2a96bf5ac89295211 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 15 Jul 2020 20:46:46 -0300 Subject: [PATCH 53/88] Bump bisqAppInitTime default back up to 5s --- apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index 78bcdeea7f7..edddfbe2fb6 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -211,10 +211,10 @@ public ApiTestConfig(String... args) { ArgumentAcceptingOptionSpec bisqAppInitTimeOpt = parser.accepts(BISQ_APP_INIT_TIME, - "Amount of time (ms) to wait for a Bisq instance's initialization") + "Amount of time (ms) to wait on a Bisq instance's initialization") .withRequiredArg() .ofType(Long.class) - .defaultsTo(4000L); + .defaultsTo(5000L); ArgumentAcceptingOptionSpec skipTestsOpt = parser.accepts(SKIP_TESTS, From 4296d96d4056d537c1473da1a0341194880cd810 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 16 Jul 2020 16:11:10 -0300 Subject: [PATCH 54/88] Remove all sudo related logic from the linux pkg The BashCommand wrapper should never be used run run sudo commands, and if the word "sudo" is found in a bash command, System.exit(1) will be called. --- .../java/bisq/apitest/linux/BashCommand.java | 7 +--- .../apitest/linux/SystemCommandExecutor.java | 33 +++++++--------- .../apitest/linux/ThreadedStreamHandler.java | 38 ------------------- 3 files changed, 16 insertions(+), 62 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/BashCommand.java b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java index 688358b5da5..f40d9b06c9d 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BashCommand.java +++ b/apitest/src/main/java/bisq/apitest/linux/BashCommand.java @@ -26,12 +26,9 @@ import org.jetbrains.annotations.NotNull; +import static bisq.apitest.config.ApiTestConfig.BASH_PATH_VALUE; import static java.lang.management.ManagementFactory.getRuntimeMXBean; - - -import bisq.apitest.config.ApiTestConfig; - @Slf4j public class BashCommand { @@ -107,7 +104,7 @@ public String getError() { @NotNull private List tokenizeSystemCommand() { return new ArrayList<>() {{ - add(ApiTestConfig.BASH_PATH_VALUE); + add(BASH_PATH_VALUE); add("-c"); add(command); }}; diff --git a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java index ee6b4711a78..f0960ba77aa 100644 --- a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java +++ b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java @@ -19,12 +19,13 @@ import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.util.List; import lombok.extern.slf4j.Slf4j; +import static java.lang.System.exit; + /** * This class can be used to execute a system command from a Java application. * See the documentation for the public methods of this class for more @@ -54,22 +55,22 @@ @Slf4j class SystemCommandExecutor { private final List cmdOptions; - private final String sudoPassword; private ThreadedStreamHandler inputStreamHandler; private ThreadedStreamHandler errorStreamHandler; - /* - * Note: I've removed the other constructor that was here to support executing - * the sudo command. I'll add that back in when I get the sudo command - * working to the point where it won't hang when the given password is - * wrong. - */ public SystemCommandExecutor(final List cmdOptions) { + if (log.isDebugEnabled()) + log.debug("cmd options {}", cmdOptions.toString()); + + if (cmdOptions.contains("sudo")) { + log.error("", new IllegalStateException("'sudo' commands are prohibited.")); + exit(1); + } + if (cmdOptions == null) throw new IllegalStateException("No command params specified."); this.cmdOptions = cmdOptions; - this.sudoPassword = null; } // Execute a system command and return its status code (0 or 1). @@ -85,23 +86,18 @@ public int exec() throws IOException, InterruptedException { public int exec(boolean waitOnErrStream) throws IOException, InterruptedException { Process process = new ProcessBuilder(cmdOptions).start(); - // you need this if you're going to write something to the command's input stream - // (such as when invoking the 'sudo' command, and it prompts you for a password). - OutputStream stdOutput = process.getOutputStream(); - - // i'm currently doing these on a separate line here in case i need to set them to null + // I'm currently doing these on a separate line here in case i need to set them to null // to get the threads to stop. // see http://java.sun.com/j2se/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html InputStream inputStream = process.getInputStream(); InputStream errorStream = process.getErrorStream(); - // these need to run as java threads to get the standard output and error from the command. + // These need to run as java threads to get the standard output and error from the command. // the inputstream handler gets a reference to our stdOutput in case we need to write - // something to it, such as with the sudo command - inputStreamHandler = new ThreadedStreamHandler(inputStream, stdOutput, sudoPassword); + // something to it. + inputStreamHandler = new ThreadedStreamHandler(inputStream); errorStreamHandler = new ThreadedStreamHandler(errorStream); - // TODO the inputStreamHandler has a nasty side-effect of hanging if the given password is wrong; fix it. inputStreamHandler.start(); errorStreamHandler.start(); @@ -117,7 +113,6 @@ public int exec(boolean waitOnErrStream) throws IOException, InterruptedExceptio return exitStatus; } - // Get the standard error from an executed system command. public StringBuilder getStandardErrorFromCommand() { return errorStreamHandler.getOutputBuffer(); diff --git a/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java index 7390b8807e4..88ae539b0d0 100644 --- a/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java +++ b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java @@ -20,8 +20,6 @@ import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PrintWriter; import lombok.extern.slf4j.Slf4j; @@ -60,49 +58,13 @@ @Slf4j class ThreadedStreamHandler extends Thread { final InputStream inputStream; - String adminPassword; - @SuppressWarnings("unused") - OutputStream outputStream; - PrintWriter printWriter; final StringBuilder outputBuffer = new StringBuilder(); - private boolean sudoIsRequested = false; - /** - * A simple constructor for when the sudo command is not necessary. - * This constructor will just run the command you provide, without - * running sudo before the command, and without expecting a password. - * - * @param inputStream InputStream - */ ThreadedStreamHandler(InputStream inputStream) { this.inputStream = inputStream; } - /** - * Use this constructor when you want to invoke the 'sudo' command. - * The outputStream must not be null. If it is, you'll regret it. :) - * - * TODO this currently hangs if the admin password given for the sudo command is wrong. - * - * @param inputStream InputStream - * @param outputStream OutputStream - * @param adminPassword String - */ - ThreadedStreamHandler(InputStream inputStream, OutputStream outputStream, String adminPassword) { - this.inputStream = inputStream; - this.outputStream = outputStream; - this.printWriter = new PrintWriter(outputStream); - this.adminPassword = adminPassword; - this.sudoIsRequested = true; - } - public void run() { - // On mac os x 10.5.x, the admin password needs to be written immediately. - if (sudoIsRequested) { - printWriter.println(adminPassword); - printWriter.flush(); - } - try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { String line; while ((line = bufferedReader.readLine()) != null) From 7d664d9cfdea965aecdeb567ee0225f7c4552d49 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 16 Jul 2020 17:05:32 -0300 Subject: [PATCH 55/88] Do not "killall bitcoind" processes Just kill the one we started. --- apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java index f4594a10ba4..cfa4cbbbf8f 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java @@ -88,7 +88,7 @@ public void shutdown() throws IOException, InterruptedException { if (!isAlive(pid)) throw new IllegalStateException("bitcoind already shut down"); - if (new BashCommand("killall bitcoind").run().getExitStatus() != 0) + if (new BashCommand("kill -15 " + pid).run().getExitStatus() != 0) throw new IllegalStateException("Could not shut down bitcoind; probably already stopped."); MILLISECONDS.sleep(2000); // allow it time to shutdown From 6edab1a2cd7bd1468e4cb6c1f886180bffc2439c Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 16 Jul 2020 19:22:51 -0300 Subject: [PATCH 56/88] Create convenient way to call bitcoin-cli from tests A BitcoinCliHelper class was added. GetBalanceTest's @BeforeClass was simplified, and it no longer starts up the arbitration node. --- .../main/java/bisq/apitest/ApiTestCase.java | 3 + .../bisq/apitest/method/BitcoinCliHelper.java | 64 +++++++++++++++++++ .../bisq/apitest/method/GetBalanceTest.java | 18 ++---- 3 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java diff --git a/apitest/src/main/java/bisq/apitest/ApiTestCase.java b/apitest/src/main/java/bisq/apitest/ApiTestCase.java index 841d5e60bae..38be47d9749 100644 --- a/apitest/src/main/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/main/java/bisq/apitest/ApiTestCase.java @@ -26,6 +26,7 @@ import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.method.BitcoinCliHelper; public class ApiTestCase { @@ -34,6 +35,7 @@ public class ApiTestCase { protected static Scaffold scaffold; protected static ApiTestConfig config; + protected static BitcoinCliHelper bitcoinCli; @Rule public ExpectedException exceptionRule = ExpectedException.none(); @@ -43,6 +45,7 @@ public static void setUpScaffold(String supportingApps) { // names, e.g. "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon" scaffold = new Scaffold(supportingApps).setUp(); config = scaffold.config; + bitcoinCli = new BitcoinCliHelper((config)); grpcStubs = new GrpcStubs(alicedaemon, config).init(); } diff --git a/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java b/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java new file mode 100644 index 00000000000..cee1d8f9e8a --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java @@ -0,0 +1,64 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method; + +import java.io.IOException; + +import static java.lang.String.format; +import static org.junit.Assert.fail; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.linux.BitcoinCli; + +public final class BitcoinCliHelper { + + private final ApiTestConfig config; + + public BitcoinCliHelper(ApiTestConfig config) { + this.config = config; + } + + // Convenience methods for making bitcoin-cli calls. + + public String getNewBtcAddress() { + try { + return new BitcoinCli(config, "getnewaddress").run().getOutput(); + } catch (IOException | InterruptedException ex) { + fail(ex.getMessage()); + return null; + } + } + + public String[] generateToAddress(int blocks, String address) { + try { + String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address); + BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); + // Return an array of transaction ids. + return generateToAddress.getOutputValueAsStringArray(); + } catch (IOException | InterruptedException ex) { + fail(ex.getMessage()); + return null; + } + } + + public void generateBlocks(int blocks) { + generateToAddress(blocks, getNewBtcAddress()); + } +} diff --git a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java index 2ae85820b23..41fc15fa161 100644 --- a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java +++ b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java @@ -19,8 +19,6 @@ import bisq.proto.grpc.GetBalanceRequest; -import java.io.IOException; - import lombok.extern.slf4j.Slf4j; import org.junit.AfterClass; @@ -28,7 +26,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import static java.lang.String.format; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -37,7 +34,6 @@ import bisq.apitest.OrderedRunner; import bisq.apitest.annotation.Order; -import bisq.apitest.linux.BitcoinCli; @Slf4j @RunWith(OrderedRunner.class) @@ -46,16 +42,14 @@ public class GetBalanceTest extends MethodTest { @BeforeClass public static void setUp() { try { - setUpScaffold("bitcoind,seednode,arbdaemon,alicedaemon"); - - String newAddress = new BitcoinCli(config, "getnewaddress").run().getOutput(); - String generateToAddressCmd = format("generatetoaddress %d \"%s\"", 1, newAddress); + setUpScaffold("bitcoind,seednode,alicedaemon"); - BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); - log.info("{}\n{}", generateToAddress.getCommandWithOptions(), generateToAddress.getOutputValueAsStringArray()); - MILLISECONDS.sleep(1500); // give bisq app time to parse block + // Have to generate 1 regtest block for alice's wallet to show 10 BTC balance. + bitcoinCli.generateBlocks(1); - } catch (IOException | InterruptedException ex) { + // Give the alicedaemon time to parse the new block. + MILLISECONDS.sleep(1500); + } catch (InterruptedException ex) { fail(ex.getMessage()); } } From 687bcf1d8f65cc9a3878e2ad64746894d9e377ac Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 16 Jul 2020 20:53:11 -0300 Subject: [PATCH 57/88] Add FundWalletScenarioTest Some refactoring was done to reduce some of the boilerplate. --- .../bisq/apitest/method/BitcoinCliHelper.java | 28 ++++++- .../java/bisq/apitest/method/MethodTest.java | 18 +++++ .../scenario/FundWalletScenarioTest.java | 78 +++++++++++++++++++ .../bisq/apitest/scenario/ScenarioTest.java | 22 ++++++ 4 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java diff --git a/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java b/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java index cee1d8f9e8a..2d6947b054a 100644 --- a/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java +++ b/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java @@ -20,6 +20,7 @@ import java.io.IOException; import static java.lang.String.format; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; @@ -39,7 +40,9 @@ public BitcoinCliHelper(ApiTestConfig config) { public String getNewBtcAddress() { try { - return new BitcoinCli(config, "getnewaddress").run().getOutput(); + String newAddress = new BitcoinCli(config, "getnewaddress").run().getOutput(); + assertNotNull(newAddress); + return newAddress; } catch (IOException | InterruptedException ex) { fail(ex.getMessage()); return null; @@ -50,8 +53,9 @@ public String[] generateToAddress(int blocks, String address) { try { String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address); BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); - // Return an array of transaction ids. - return generateToAddress.getOutputValueAsStringArray(); + String[] txids = generateToAddress.getOutputValueAsStringArray(); + assertNotNull(txids); + return txids; } catch (IOException | InterruptedException ex) { fail(ex.getMessage()); return null; @@ -61,4 +65,22 @@ public String[] generateToAddress(int blocks, String address) { public void generateBlocks(int blocks) { generateToAddress(blocks, getNewBtcAddress()); } + + public String sendToAddress(String address, double amount) { + // sendtoaddress "address" amount \ + // ( "comment" "comment_to" subtractfeefromamount \ + // replaceable conf_target "estimate_mode" ) + // returns a transaction id + try { + String sendToAddressCmd = format("sendtoaddress \"%s\" %s \"\" \"\" true", + address, amount); + BitcoinCli sendToAddress = new BitcoinCli(config, sendToAddressCmd).run(); + String txid = sendToAddress.getOutput(); + assertNotNull(txid); + return txid; + } catch (IOException | InterruptedException ex) { + fail(ex.getMessage()); + return null; + } + } } diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTest.java b/apitest/src/main/java/bisq/apitest/method/MethodTest.java index 5c8e7a64949..37dea63cdb9 100644 --- a/apitest/src/main/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/main/java/bisq/apitest/method/MethodTest.java @@ -18,6 +18,7 @@ package bisq.apitest.method; import bisq.proto.grpc.GetBalanceRequest; +import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest; @@ -55,6 +56,10 @@ protected final LockWalletRequest createLockWalletRequest() { return LockWalletRequest.newBuilder().build(); } + protected final GetFundingAddressesRequest createGetFundingAddressesRequest() { + return GetFundingAddressesRequest.newBuilder().build(); + } + // Convenience methods for calling frequently used & thoroughly tested gRPC services. protected final long getBalance() { @@ -62,10 +67,23 @@ protected final long getBalance() { } protected final void unlockWallet(String password, long timeout) { + //noinspection ResultOfMethodCallIgnored grpcStubs.walletsService.unlockWallet(createUnlockWalletRequest(password, timeout)); } protected final void lockWallet() { + //noinspection ResultOfMethodCallIgnored grpcStubs.walletsService.lockWallet(createLockWalletRequest()); } + + protected final String getUnusedBtcAddress() { + return grpcStubs.walletsService.getFundingAddresses(createGetFundingAddressesRequest()) + .getAddressBalanceInfoList() + .stream() + .filter(a -> a.getBalance() == 0 && a.getNumConfirmations() == 0) + .findFirst() + .get() + .getAddress(); + + } } diff --git a/apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java b/apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java new file mode 100644 index 00000000000..44a9c058015 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java @@ -0,0 +1,78 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static java.lang.Double.parseDouble; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + + + +import bisq.apitest.OrderedRunner; +import bisq.apitest.annotation.Order; + +@Slf4j +@RunWith(OrderedRunner.class) +public class FundWalletScenarioTest extends ScenarioTest { + + @BeforeClass + public static void setUp() { + try { + setUpScaffold("bitcoind,seednode,alicedaemon"); + bitcoinCli.generateBlocks(1); + MILLISECONDS.sleep(1500); + } catch (InterruptedException ex) { + fail(ex.getMessage()); + } + } + + @Test + @Order(1) + public void testFundWallet() { + long balance = getBalance(); // bisq wallet was initialized with 10 btc + assertEquals(1000000000, balance); + + String unusedAddress = getUnusedBtcAddress(); + + // Given the default tx fee rate, we want to send 2.5 + the fee, + // so the new wallet balance will be exactly 12.5 btc. + // We should calculate the fee based on the fee rate and tx size + // instead of hard coding the fee amount. + double btc = parseDouble("2.5") + parseDouble("0.0000336"); + bitcoinCli.sendToAddress(unusedAddress, btc); + + bitcoinCli.generateBlocks(1); + sleep(1500); + + balance = getBalance(); + assertEquals(1250000000L, balance); // new balance is 12.5 btc + } + + @AfterClass + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java b/apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java index 782b237c3e9..9750b2ed9d6 100644 --- a/apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java +++ b/apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java @@ -1,6 +1,28 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + package bisq.apitest.scenario; +import lombok.extern.slf4j.Slf4j; + + + import bisq.apitest.method.MethodTest; +@Slf4j public class ScenarioTest extends MethodTest { } From 2852e3da8b002e08d80c799723190516ea17e8a1 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 16 Jul 2020 20:54:20 -0300 Subject: [PATCH 58/88] Add JUnitHelper to run tests from JUnitCore --- .../main/java/bisq/apitest/JUnitHelper.java | 51 +++++++++++++++++++ .../bisq/apitest/method/MethodTestMain.java | 46 ++--------------- .../apitest/scenario/ScenarioTestMain.java | 23 +++++++++ 3 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 apitest/src/main/java/bisq/apitest/JUnitHelper.java create mode 100644 apitest/src/main/java/bisq/apitest/scenario/ScenarioTestMain.java diff --git a/apitest/src/main/java/bisq/apitest/JUnitHelper.java b/apitest/src/main/java/bisq/apitest/JUnitHelper.java new file mode 100644 index 00000000000..affb127b13f --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/JUnitHelper.java @@ -0,0 +1,51 @@ +package bisq.apitest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.runner.Description; +import org.junit.runner.JUnitCore; +import org.junit.runner.Result; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; + +import static java.lang.String.format; + +@Slf4j +public class JUnitHelper { + + public static void runTests(Class... testClasses) { + JUnitCore jUnitCore = new JUnitCore(); + jUnitCore.addListener(new RunListener() { + public void testStarted(Description description) { + log.info("{}", description); + } + + public void testIgnored(Description description) { + log.info("Ignored {}", description); + } + + public void testFailure(Failure failure) { + log.error("Failed {}", failure.getTrace()); + } + }); + Result result = jUnitCore.run(testClasses); + printTestResults(result); + } + + public static void printTestResults(Result result) { + log.info("Total tests: {}, Failed: {}, Ignored: {}", + result.getRunCount(), + result.getFailureCount(), + result.getIgnoreCount()); + + if (result.wasSuccessful()) { + log.info("All {} tests passed", result.getRunCount()); + } else if (result.getFailureCount() > 0) { + log.error("{} test(s) failed", result.getFailureCount()); + result.getFailures().iterator().forEachRemaining(f -> log.error(format("%s.%s()%n\t%s", + f.getDescription().getTestClass().getName(), + f.getDescription().getMethodName(), + f.getTrace()))); + } + } +} diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java b/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java index 504520b6337..3840e1c9399 100644 --- a/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java +++ b/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java @@ -2,19 +2,13 @@ import lombok.extern.slf4j.Slf4j; -import org.junit.runner.Description; -import org.junit.runner.JUnitCore; -import org.junit.runner.Result; -import org.junit.runner.notification.Failure; -import org.junit.runner.notification.RunListener; - -import static java.lang.String.format; +import static bisq.apitest.JUnitHelper.runTests; /** * Driver for running API method tests. * * This may not seem necessary, but test cases are contained in the apitest sub - * project's main sources, not its test sources. An IDE will not automatically configure + * project's main sources, not its test sources. An IDE may not automatically configure * JUnit test launchers, and a gradle build will not automatically run the test cases. * * However, it is easy to manually configure an IDE launcher to run all, some or one @@ -24,40 +18,6 @@ public class MethodTestMain { public static void main(String[] args) { - JUnitCore jUnitCore = new JUnitCore(); - jUnitCore.addListener(new RunListener() { - public void testStarted(Description description) { - log.info("{}", description); - } - - public void testIgnored(Description description) { - log.info("Ignored {}", description); - } - - public void testFailure(Failure failure) { - log.error("Failed {}", failure.getTrace()); - } - }); - Result result = jUnitCore.run(GetVersionTest.class, GetBalanceTest.class, WalletProtectionTest.class); - ResultUtil.printResult(result); - } - - private static class ResultUtil { - public static void printResult(Result result) { - log.info("Total tests: {}, Failed: {}, Ignored: {}", - result.getRunCount(), - result.getFailureCount(), - result.getIgnoreCount()); - - if (result.wasSuccessful()) { - log.info("All tests passed"); - } else if (result.getFailureCount() > 0) { - log.error("{} test(s) failed", result.getFailureCount()); - result.getFailures().iterator().forEachRemaining(f -> log.error(format("%s.%s()%n\t%s", - f.getDescription().getTestClass().getName(), - f.getDescription().getMethodName(), - f.getTrace()))); - } - } + runTests(GetVersionTest.class, GetBalanceTest.class, WalletProtectionTest.class); } } diff --git a/apitest/src/main/java/bisq/apitest/scenario/ScenarioTestMain.java b/apitest/src/main/java/bisq/apitest/scenario/ScenarioTestMain.java new file mode 100644 index 00000000000..0fb115939a9 --- /dev/null +++ b/apitest/src/main/java/bisq/apitest/scenario/ScenarioTestMain.java @@ -0,0 +1,23 @@ +package bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.apitest.JUnitHelper.runTests; + +/** + * Driver for running API scenario tests. + * + * This may not seem necessary, but test cases are contained in the apitest sub + * project's main sources, not its test sources. An IDE may not automatically configure + * JUnit test launchers, and a gradle build will not automatically run the test cases. + * + * However, it is easy to manually configure an IDE launcher to run all, some or one + * JUnit test, and new gradle tasks should be provided to run all, some, or one test. + */ +@Slf4j +public class ScenarioTestMain { + + public static void main(String[] args) { + runTests(FundWalletScenarioTest.class); + } +} From 5d7133a9b4fd178495daa1d2dbcea433715d3e9f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 17 Jul 2020 12:31:03 -0300 Subject: [PATCH 59/88] Do not subtract fee from 'bitcoin-cli sendtoaddress' Test cases need to be as simple as possible for now. --- .../main/java/bisq/apitest/method/BitcoinCliHelper.java | 4 ++-- .../bisq/apitest/scenario/FundWalletScenarioTest.java | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java b/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java index 2d6947b054a..bed3477801a 100644 --- a/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java +++ b/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java @@ -66,13 +66,13 @@ public void generateBlocks(int blocks) { generateToAddress(blocks, getNewBtcAddress()); } - public String sendToAddress(String address, double amount) { + public String sendToAddress(String address, String amount) { // sendtoaddress "address" amount \ // ( "comment" "comment_to" subtractfeefromamount \ // replaceable conf_target "estimate_mode" ) // returns a transaction id try { - String sendToAddressCmd = format("sendtoaddress \"%s\" %s \"\" \"\" true", + String sendToAddressCmd = format("sendtoaddress \"%s\" %s \"\" \"\" false", address, amount); BitcoinCli sendToAddress = new BitcoinCli(config, sendToAddressCmd).run(); String txid = sendToAddress.getOutput(); diff --git a/apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java b/apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java index 44a9c058015..95c793482c5 100644 --- a/apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java +++ b/apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java @@ -24,7 +24,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import static java.lang.Double.parseDouble; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -56,13 +55,7 @@ public void testFundWallet() { assertEquals(1000000000, balance); String unusedAddress = getUnusedBtcAddress(); - - // Given the default tx fee rate, we want to send 2.5 + the fee, - // so the new wallet balance will be exactly 12.5 btc. - // We should calculate the fee based on the fee rate and tx size - // instead of hard coding the fee amount. - double btc = parseDouble("2.5") + parseDouble("0.0000336"); - bitcoinCli.sendToAddress(unusedAddress, btc); + bitcoinCli.sendToAddress(unusedAddress, "2.5"); bitcoinCli.generateBlocks(1); sleep(1500); From 8269a0df6d17c69e4ea7551a332e221caf377194 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 17 Jul 2020 17:13:45 -0300 Subject: [PATCH 60/88] Remove final modifier --- cli/src/main/java/bisq/cli/CurrencyFormat.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java index 859dc4e8e8f..527ab8ca612 100644 --- a/cli/src/main/java/bisq/cli/CurrencyFormat.java +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -16,7 +16,7 @@ class CurrencyFormat { static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") - static final String formatSatoshis(long sats) { + static String formatSatoshis(long sats) { return BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR)); } From f7d8c0e5c3957e45ac7e987d2a7368594cc8789c Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 17 Jul 2020 17:20:53 -0300 Subject: [PATCH 61/88] Do not use bitcoin.conf files This change removes code for installing a regtest bitcoin.conf file. It also removes an unused bitcoin.conf file from the main resources directory. Now, the bitcoind startup command passes all configurations on the command line. (See bisq.apitest.linux.BitcoinDaemon.java) --- .../src/main/java/bisq/apitest/Scaffold.java | 61 ++----------------- .../apitest/linux/AbstractLinuxProcess.java | 13 ++-- .../java/bisq/apitest/linux/BitcoinCli.java | 2 +- .../bisq/apitest/linux/BitcoinDaemon.java | 17 +++--- 4 files changed, 19 insertions(+), 74 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index d2d8b077791..dd3f022bb43 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -18,7 +18,6 @@ package bisq.apitest; import bisq.common.config.BisqHelpFormatter; -import bisq.common.storage.FileUtil; import bisq.common.util.Utilities; import java.nio.file.Files; @@ -29,7 +28,6 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.PrintWriter; import java.util.Objects; import java.util.concurrent.CountDownLatch; @@ -212,13 +210,10 @@ public void installDaoSetupDirectories() { log.info("Installed dao-setup files into {}", buildDataDir); - // Write a bitcoin.conf file with the correct path to the blocknotify script, - // and save it to the build resource dir. - installBitcoinConf(); - - // Copy the blocknotify script from the src resources dir to the - // build resources dir. Users may want to edit it sometimes, - // when all default block notifcation ports are being used. + // Copy the blocknotify script from the src resources dir to the build + // resources dir. Users may want to edit comment out some lines when all + // of the default block notifcation ports being will not be used (to avoid + // seeing rpc notifcation warnings in log files). installBitcoinBlocknotify(); } catch (IOException | InterruptedException ex) { @@ -256,26 +251,6 @@ private void cleanDaoSetupDirectories() { } } - private void installBitcoinConf() { - // We write out and install a bitcoin.conf file for regtest/dao mode because - // the path to the blocknotify script is not known until runtime. - String bitcoinConf = "\n" - + "regtest=1\n" - + "[regtest]\n" - + "peerbloomfilters=1\n" - + "rpcport=" + config.bitcoinRpcPort + "\n" - + "server=1\n" - + "txindex=1\n" - + "debug=net\n" - + "deprecatedrpc=generate\n" - + "rpcuser=" + config.bitcoinRpcUser + "\n" - + "rpcpassword=" + config.bitcoinRpcPassword + "\n" - + "blocknotify=" + config.bashPath + " " + config.bitcoinDatadir + "/blocknotify %\n"; - String chmod644Perms = "rw-r--r--"; - saveToFile(bitcoinConf, config.bitcoinDatadir, "bitcoin.conf", chmod644Perms); - log.info("Installed {} with perms {}.", config.bitcoinDatadir + "/bitcoin.conf", chmod644Perms); - } - private void installBitcoinBlocknotify() { // gradle is not working for this try { @@ -290,32 +265,6 @@ private void installBitcoinBlocknotify() { } } - private void saveToFile(String content, - String parentDir, - @SuppressWarnings("SameParameterValue") String relativeFilePath, - String posixFilePermissions) { - File tempFile = null; - File file; - try { - file = Paths.get(parentDir, relativeFilePath).toFile(); - tempFile = File.createTempFile("temp", relativeFilePath, file.getParentFile()); - tempFile.deleteOnExit(); - try (PrintWriter out = new PrintWriter(tempFile)) { - out.println(content); - } - FileUtil.renameFile(tempFile, file); - Files.setPosixFilePermissions(Paths.get(file.toURI()), PosixFilePermissions.fromString(posixFilePermissions)); - } catch (IOException ex) { - throw new IllegalStateException(format("Error saving %s/%s to disk", parentDir, relativeFilePath), ex); - } finally { - if (tempFile != null && tempFile.exists()) { - log.warn("Temp file still exists after failed save; deleting {} now.", tempFile.getAbsolutePath()); - if (!tempFile.delete()) - log.error("Cannot delete temp file."); - } - } - } - private void installShutdownHook() { // Background apps can be left running until the jvm is manually shutdown, // so we add a shutdown hook for that use case. @@ -331,7 +280,7 @@ private void startBackgroundProcesses(ExecutorService executor, if (config.hasSupportingApp("bitcoind")) { BitcoinDaemon bitcoinDaemon = new BitcoinDaemon(config); - bitcoinDaemon.verifyBitcoinConfig(true); + bitcoinDaemon.verifyBitcoinPathsExist(true); bitcoindTask = new SetupTask(bitcoinDaemon, countdownLatch); bitcoindTaskFuture = executor.submit(bitcoindTask); MILLISECONDS.sleep(3500); diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index 3c5cfd4388f..5a9f5565508 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -67,18 +67,17 @@ public List getStartupExceptions() { } @SuppressWarnings("unused") - public void verifyBitcoinConfig() { - verifyBitcoinConfig(false); + public void verifyBitcoinPathsExist() { + verifyBitcoinPathsExist(false); } - public void verifyBitcoinConfig(boolean verbose) { + public void verifyBitcoinPathsExist(boolean verbose) { if (verbose) log.info(format("Checking bitcoind env...%n" - + "\t%-20s%s%n\t%-20s%s%n\t%-20s%s%n\t%-20s%s%n\t%-20s%s", + + "\t%-20s%s%n\t%-20s%s%n\t%-20s%s%n\t%-20s%s", "berkeleyDbLibPath", config.berkeleyDbLibPath, "bitcoinPath", config.bitcoinPath, "bitcoinDatadir", config.bitcoinDatadir, - "bitcoin.conf", config.bitcoinDatadir + "/bitcoin.conf", "blocknotify", config.bitcoinDatadir + "/blocknotify")); if (!config.berkeleyDbLibPath.equals(EMPTY)) { @@ -99,10 +98,6 @@ public void verifyBitcoinConfig(boolean verbose) { if (!bitcoindDatadir.exists() || !bitcoindDatadir.canWrite()) throw new IllegalStateException(bitcoindDatadir + " cannot be found or written to"); - File bitcoinConf = new File(bitcoindDatadir, "bitcoin.conf"); - if (!bitcoinConf.exists() || !bitcoinConf.canRead()) - throw new IllegalStateException(bitcoinConf.getAbsolutePath() + " cannot be found or read"); - File blocknotify = new File(bitcoindDatadir, "blocknotify"); if (!blocknotify.exists() || !blocknotify.canExecute()) throw new IllegalStateException(blocknotify.getAbsolutePath() + " cannot be found or executed"); diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java index 2fd475cce77..169dfd5da1a 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java @@ -140,7 +140,7 @@ public boolean isError() { @Override public void start() throws InterruptedException, IOException { - verifyBitcoinConfig(false); + verifyBitcoinPathsExist(false); verifyBitcoindRunning(); commandWithOptions = config.bitcoinPath + "/bitcoin-cli -regtest " + " -rpcuser=" + config.bitcoinRpcUser diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java index cfa4cbbbf8f..c752d7c8e9b 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java @@ -30,13 +30,6 @@ import bisq.apitest.config.ApiTestConfig; -// Some cmds: -// bitcoin-cli -regtest generatetoaddress 1 "2MyBq4jbtDF6CfKNrrQdp7qkRc8mKuCpKno" -// bitcoin-cli -regtest getbalance -// note: getbalance does not include immature coins (<100 blks deep) -// bitcoin-cli -regtest getbalances -// bitcoin-cli -regtest getrpcinfo - @Slf4j public class BitcoinDaemon extends AbstractLinuxProcess implements LinuxProcess { @@ -56,7 +49,15 @@ public void start() throws InterruptedException, IOException { String bitcoindCmd = berkeleyDbLibPathExport + config.bitcoinPath + "/bitcoind" + " -datadir=" + config.bitcoinDatadir - + " -daemon"; + + " -daemon" + + " -regtest=1" + + " -server=1" + + " -txindex=1" + + " -peerbloomfilters=1" + + " -debug=net" + + " -rpcuser=" + config.bitcoinRpcUser + + " -rpcpassword=" + config.bitcoinRpcPassword + + " -blocknotify=" + config.bitcoinDatadir + "/blocknotify"; BashCommand cmd = new BashCommand(bitcoindCmd).run(); log.info("Starting ...\n$ {}", cmd.getCommand()); From 1847da0110f9a648985d9b7318339d8910e842ed Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 17 Jul 2020 17:29:28 -0300 Subject: [PATCH 62/88] Delete unused bitcoin.conf from resources dir --- apitest/src/main/resources/bitcoin.conf | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 apitest/src/main/resources/bitcoin.conf diff --git a/apitest/src/main/resources/bitcoin.conf b/apitest/src/main/resources/bitcoin.conf deleted file mode 100644 index 7c92bbf0b34..00000000000 --- a/apitest/src/main/resources/bitcoin.conf +++ /dev/null @@ -1,16 +0,0 @@ -# This file is overwritten by ApiTestConfig.java at startup, to set the correct blocknotify script path. -regtest=1 - -[regtest] -peerbloomfilters=1 -rpcport=18443 - -server=1 -txindex=1 - -# TODO migrate to rpcauth -rpcuser=apitest -rpcpassword=apitest - -# The blocknotify path will be defined and at runtime. -# blocknotify=bash ~/.bitcoin/blocknotify % From 19346bbe78b5f9f17165aa8e86483d7706f80d00 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 19 Jul 2020 13:43:25 -0300 Subject: [PATCH 63/88] Delete all JUnit related class from main sources JUnit tests are being moved to the subproject's test sources directory for the sake of convention -- for developers using this test harness, and for IDE and gradle JUnit integration. --- .../main/java/bisq/apitest/ApiTestCase.java | 68 --------- .../main/java/bisq/apitest/JUnitHelper.java | 51 ------- .../main/java/bisq/apitest/OrderedRunner.java | 54 -------- .../java/bisq/apitest/annotation/Order.java | 29 ---- .../bisq/apitest/method/BitcoinCliHelper.java | 86 ------------ .../bisq/apitest/method/GetBalanceTest.java | 68 --------- .../bisq/apitest/method/GetVersionTest.java | 58 -------- .../java/bisq/apitest/method/MethodTest.java | 89 ------------ .../bisq/apitest/method/MethodTestMain.java | 23 ---- .../apitest/method/WalletProtectionTest.java | 130 ------------------ .../scenario/FundWalletScenarioTest.java | 71 ---------- .../bisq/apitest/scenario/ScenarioTest.java | 28 ---- .../apitest/scenario/ScenarioTestMain.java | 23 ---- 13 files changed, 778 deletions(-) delete mode 100644 apitest/src/main/java/bisq/apitest/ApiTestCase.java delete mode 100644 apitest/src/main/java/bisq/apitest/JUnitHelper.java delete mode 100644 apitest/src/main/java/bisq/apitest/OrderedRunner.java delete mode 100644 apitest/src/main/java/bisq/apitest/annotation/Order.java delete mode 100644 apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java delete mode 100644 apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java delete mode 100644 apitest/src/main/java/bisq/apitest/method/GetVersionTest.java delete mode 100644 apitest/src/main/java/bisq/apitest/method/MethodTest.java delete mode 100644 apitest/src/main/java/bisq/apitest/method/MethodTestMain.java delete mode 100644 apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java delete mode 100644 apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java delete mode 100644 apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java delete mode 100644 apitest/src/main/java/bisq/apitest/scenario/ScenarioTestMain.java diff --git a/apitest/src/main/java/bisq/apitest/ApiTestCase.java b/apitest/src/main/java/bisq/apitest/ApiTestCase.java deleted file mode 100644 index 38be47d9749..00000000000 --- a/apitest/src/main/java/bisq/apitest/ApiTestCase.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest; - -import org.junit.Rule; -import org.junit.rules.ExpectedException; - -import static bisq.apitest.config.BisqAppConfig.alicedaemon; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - - - -import bisq.apitest.config.ApiTestConfig; -import bisq.apitest.method.BitcoinCliHelper; - -public class ApiTestCase { - - // The gRPC service stubs are used by method & scenario tests, but not e2e tests. - protected static GrpcStubs grpcStubs; - - protected static Scaffold scaffold; - protected static ApiTestConfig config; - protected static BitcoinCliHelper bitcoinCli; - - @Rule - public ExpectedException exceptionRule = ExpectedException.none(); - - public static void setUpScaffold(String supportingApps) { - // The supportingApps argument is a comma delimited string of supporting app - // names, e.g. "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon" - scaffold = new Scaffold(supportingApps).setUp(); - config = scaffold.config; - bitcoinCli = new BitcoinCliHelper((config)); - grpcStubs = new GrpcStubs(alicedaemon, config).init(); - } - - public static void setUpScaffold() { - scaffold = new Scaffold(new String[]{}).setUp(); - config = scaffold.config; - grpcStubs = new GrpcStubs(alicedaemon, config).init(); - } - - public static void tearDownScaffold() { - scaffold.tearDown(); - } - - protected void sleep(long ms) { - try { - MILLISECONDS.sleep(ms); - } catch (InterruptedException ignored) { - } - } -} diff --git a/apitest/src/main/java/bisq/apitest/JUnitHelper.java b/apitest/src/main/java/bisq/apitest/JUnitHelper.java deleted file mode 100644 index affb127b13f..00000000000 --- a/apitest/src/main/java/bisq/apitest/JUnitHelper.java +++ /dev/null @@ -1,51 +0,0 @@ -package bisq.apitest; - -import lombok.extern.slf4j.Slf4j; - -import org.junit.runner.Description; -import org.junit.runner.JUnitCore; -import org.junit.runner.Result; -import org.junit.runner.notification.Failure; -import org.junit.runner.notification.RunListener; - -import static java.lang.String.format; - -@Slf4j -public class JUnitHelper { - - public static void runTests(Class... testClasses) { - JUnitCore jUnitCore = new JUnitCore(); - jUnitCore.addListener(new RunListener() { - public void testStarted(Description description) { - log.info("{}", description); - } - - public void testIgnored(Description description) { - log.info("Ignored {}", description); - } - - public void testFailure(Failure failure) { - log.error("Failed {}", failure.getTrace()); - } - }); - Result result = jUnitCore.run(testClasses); - printTestResults(result); - } - - public static void printTestResults(Result result) { - log.info("Total tests: {}, Failed: {}, Ignored: {}", - result.getRunCount(), - result.getFailureCount(), - result.getIgnoreCount()); - - if (result.wasSuccessful()) { - log.info("All {} tests passed", result.getRunCount()); - } else if (result.getFailureCount() > 0) { - log.error("{} test(s) failed", result.getFailureCount()); - result.getFailures().iterator().forEachRemaining(f -> log.error(format("%s.%s()%n\t%s", - f.getDescription().getTestClass().getName(), - f.getDescription().getMethodName(), - f.getTrace()))); - } - } -} diff --git a/apitest/src/main/java/bisq/apitest/OrderedRunner.java b/apitest/src/main/java/bisq/apitest/OrderedRunner.java deleted file mode 100644 index d206987e75e..00000000000 --- a/apitest/src/main/java/bisq/apitest/OrderedRunner.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest; - -import java.util.ArrayList; -import java.util.List; - -import org.junit.runners.BlockJUnit4ClassRunner; -import org.junit.runners.model.FrameworkMethod; -import org.junit.runners.model.InitializationError; - - - -import bisq.apitest.annotation.Order; - -// Credit to Aman Goel for providing an example of ordering JUnit tests at -// https://stackoverflow.com/questions/3693626/how-to-run-test-methods-in-specific-order-in-junit4 - -public class OrderedRunner extends BlockJUnit4ClassRunner { - - public OrderedRunner(Class clazz) throws InitializationError { - super(clazz); - } - - @Override - protected List computeTestMethods() { - List list = super.computeTestMethods(); - List copy = new ArrayList<>(list); - copy.sort((f1, f2) -> { - Order o1 = f1.getAnnotation(Order.class); - Order o2 = f2.getAnnotation(Order.class); - if (o1 == null || o2 == null) - return -1; - - return o1.value() - o2.value(); - }); - return copy; - } -} diff --git a/apitest/src/main/java/bisq/apitest/annotation/Order.java b/apitest/src/main/java/bisq/apitest/annotation/Order.java deleted file mode 100644 index 428f9ac19ac..00000000000 --- a/apitest/src/main/java/bisq/apitest/annotation/Order.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) -public @interface Order { - int value(); -} diff --git a/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java b/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java deleted file mode 100644 index bed3477801a..00000000000 --- a/apitest/src/main/java/bisq/apitest/method/BitcoinCliHelper.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.method; - -import java.io.IOException; - -import static java.lang.String.format; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; - - - -import bisq.apitest.config.ApiTestConfig; -import bisq.apitest.linux.BitcoinCli; - -public final class BitcoinCliHelper { - - private final ApiTestConfig config; - - public BitcoinCliHelper(ApiTestConfig config) { - this.config = config; - } - - // Convenience methods for making bitcoin-cli calls. - - public String getNewBtcAddress() { - try { - String newAddress = new BitcoinCli(config, "getnewaddress").run().getOutput(); - assertNotNull(newAddress); - return newAddress; - } catch (IOException | InterruptedException ex) { - fail(ex.getMessage()); - return null; - } - } - - public String[] generateToAddress(int blocks, String address) { - try { - String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address); - BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); - String[] txids = generateToAddress.getOutputValueAsStringArray(); - assertNotNull(txids); - return txids; - } catch (IOException | InterruptedException ex) { - fail(ex.getMessage()); - return null; - } - } - - public void generateBlocks(int blocks) { - generateToAddress(blocks, getNewBtcAddress()); - } - - public String sendToAddress(String address, String amount) { - // sendtoaddress "address" amount \ - // ( "comment" "comment_to" subtractfeefromamount \ - // replaceable conf_target "estimate_mode" ) - // returns a transaction id - try { - String sendToAddressCmd = format("sendtoaddress \"%s\" %s \"\" \"\" false", - address, amount); - BitcoinCli sendToAddress = new BitcoinCli(config, sendToAddressCmd).run(); - String txid = sendToAddress.getOutput(); - assertNotNull(txid); - return txid; - } catch (IOException | InterruptedException ex) { - fail(ex.getMessage()); - return null; - } - } -} diff --git a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java deleted file mode 100644 index 41fc15fa161..00000000000 --- a/apitest/src/main/java/bisq/apitest/method/GetBalanceTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.method; - -import bisq.proto.grpc.GetBalanceRequest; - -import lombok.extern.slf4j.Slf4j; - -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - - - -import bisq.apitest.OrderedRunner; -import bisq.apitest.annotation.Order; - -@Slf4j -@RunWith(OrderedRunner.class) -public class GetBalanceTest extends MethodTest { - - @BeforeClass - public static void setUp() { - try { - setUpScaffold("bitcoind,seednode,alicedaemon"); - - // Have to generate 1 regtest block for alice's wallet to show 10 BTC balance. - bitcoinCli.generateBlocks(1); - - // Give the alicedaemon time to parse the new block. - MILLISECONDS.sleep(1500); - } catch (InterruptedException ex) { - fail(ex.getMessage()); - } - } - - @Test - @Order(1) - public void testGetBalance() { - var balance = grpcStubs.walletsService.getBalance(GetBalanceRequest.newBuilder().build()).getBalance(); - assertEquals(1000000000, balance); - } - - @AfterClass - public static void tearDown() { - tearDownScaffold(); - } -} diff --git a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java deleted file mode 100644 index ab262db6e3a..00000000000 --- a/apitest/src/main/java/bisq/apitest/method/GetVersionTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.method; - -import bisq.proto.grpc.GetVersionRequest; - -import lombok.extern.slf4j.Slf4j; - -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static bisq.apitest.config.BisqAppConfig.alicedaemon; -import static bisq.common.app.Version.VERSION; -import static org.junit.Assert.assertEquals; - - - -import bisq.apitest.OrderedRunner; -import bisq.apitest.annotation.Order; - -@Slf4j -@RunWith(OrderedRunner.class) -public class GetVersionTest extends MethodTest { - - @BeforeClass - public static void setUp() { - setUpScaffold(alicedaemon.name()); - } - - @Test - @Order(1) - public void testGetVersion() { - var version = grpcStubs.versionService.getVersion(GetVersionRequest.newBuilder().build()).getVersion(); - assertEquals(VERSION, version); - } - - @AfterClass - public static void tearDown() { - tearDownScaffold(); - } -} diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTest.java b/apitest/src/main/java/bisq/apitest/method/MethodTest.java deleted file mode 100644 index 37dea63cdb9..00000000000 --- a/apitest/src/main/java/bisq/apitest/method/MethodTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.method; - -import bisq.proto.grpc.GetBalanceRequest; -import bisq.proto.grpc.GetFundingAddressesRequest; -import bisq.proto.grpc.LockWalletRequest; -import bisq.proto.grpc.RemoveWalletPasswordRequest; -import bisq.proto.grpc.SetWalletPasswordRequest; -import bisq.proto.grpc.UnlockWalletRequest; - - - -import bisq.apitest.ApiTestCase; - -public class MethodTest extends ApiTestCase { - - // Convenience methods for building gRPC request objects - - protected final GetBalanceRequest createBalanceRequest() { - return GetBalanceRequest.newBuilder().build(); - } - - protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) { - return SetWalletPasswordRequest.newBuilder().setPassword(password).build(); - } - - protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String oldPassword, String newPassword) { - return SetWalletPasswordRequest.newBuilder().setPassword(oldPassword).setNewPassword(newPassword).build(); - } - - protected final RemoveWalletPasswordRequest createRemoveWalletPasswordRequest(String password) { - return RemoveWalletPasswordRequest.newBuilder().setPassword(password).build(); - } - - protected final UnlockWalletRequest createUnlockWalletRequest(String password, long timeout) { - return UnlockWalletRequest.newBuilder().setPassword(password).setTimeout(timeout).build(); - } - - protected final LockWalletRequest createLockWalletRequest() { - return LockWalletRequest.newBuilder().build(); - } - - protected final GetFundingAddressesRequest createGetFundingAddressesRequest() { - return GetFundingAddressesRequest.newBuilder().build(); - } - - // Convenience methods for calling frequently used & thoroughly tested gRPC services. - - protected final long getBalance() { - return grpcStubs.walletsService.getBalance(createBalanceRequest()).getBalance(); - } - - protected final void unlockWallet(String password, long timeout) { - //noinspection ResultOfMethodCallIgnored - grpcStubs.walletsService.unlockWallet(createUnlockWalletRequest(password, timeout)); - } - - protected final void lockWallet() { - //noinspection ResultOfMethodCallIgnored - grpcStubs.walletsService.lockWallet(createLockWalletRequest()); - } - - protected final String getUnusedBtcAddress() { - return grpcStubs.walletsService.getFundingAddresses(createGetFundingAddressesRequest()) - .getAddressBalanceInfoList() - .stream() - .filter(a -> a.getBalance() == 0 && a.getNumConfirmations() == 0) - .findFirst() - .get() - .getAddress(); - - } -} diff --git a/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java b/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java deleted file mode 100644 index 3840e1c9399..00000000000 --- a/apitest/src/main/java/bisq/apitest/method/MethodTestMain.java +++ /dev/null @@ -1,23 +0,0 @@ -package bisq.apitest.method; - -import lombok.extern.slf4j.Slf4j; - -import static bisq.apitest.JUnitHelper.runTests; - -/** - * Driver for running API method tests. - * - * This may not seem necessary, but test cases are contained in the apitest sub - * project's main sources, not its test sources. An IDE may not automatically configure - * JUnit test launchers, and a gradle build will not automatically run the test cases. - * - * However, it is easy to manually configure an IDE launcher to run all, some or one - * JUnit test, and new gradle tasks should be provided to run all, some, or one test. - */ -@Slf4j -public class MethodTestMain { - - public static void main(String[] args) { - runTests(GetVersionTest.class, GetBalanceTest.class, WalletProtectionTest.class); - } -} diff --git a/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java deleted file mode 100644 index 4183cb6d98c..00000000000 --- a/apitest/src/main/java/bisq/apitest/method/WalletProtectionTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package bisq.apitest.method; - -import io.grpc.StatusRuntimeException; - -import lombok.extern.slf4j.Slf4j; - -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static bisq.apitest.config.BisqAppConfig.alicedaemon; - - - -import bisq.apitest.OrderedRunner; -import bisq.apitest.annotation.Order; - -@Slf4j -@RunWith(OrderedRunner.class) -public class WalletProtectionTest extends MethodTest { - - @BeforeClass - public static void setUp() { - setUpScaffold(alicedaemon.name()); - } - - @Test - @Order(1) - public void testSetWalletPassword() { - var request = createSetWalletPasswordRequest("first-password"); - grpcStubs.walletsService.setWalletPassword(request); - } - - @Test - @Order(2) - public void testGetBalanceOnEncryptedWalletShouldThrowException() { - exceptionRule.expect(StatusRuntimeException.class); - exceptionRule.expectMessage("UNKNOWN: wallet is locked"); - getBalance(); - } - - @Test - @Order(3) - public void testUnlockWalletFor4Seconds() { - var request = createUnlockWalletRequest("first-password", 4); - grpcStubs.walletsService.unlockWallet(request); - getBalance(); // should not throw 'wallet locked' exception - - sleep(4500); // let unlock timeout expire - exceptionRule.expect(StatusRuntimeException.class); - exceptionRule.expectMessage("UNKNOWN: wallet is locked"); - getBalance(); - } - - @Test - @Order(4) - public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() { - var request = createUnlockWalletRequest("first-password", 3); - grpcStubs.walletsService.unlockWallet(request); - sleep(4000); // let unlock timeout expire - exceptionRule.expect(StatusRuntimeException.class); - exceptionRule.expectMessage("UNKNOWN: wallet is locked"); - getBalance(); - } - - @Test - @Order(5) - public void testLockWalletBeforeUnlockTimeoutExpiry() { - unlockWallet("first-password", 60); - var request = createLockWalletRequest(); - grpcStubs.walletsService.lockWallet(request); - - exceptionRule.expect(StatusRuntimeException.class); - exceptionRule.expectMessage("UNKNOWN: wallet is locked"); - getBalance(); - } - - @Test - @Order(6) - public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() { - exceptionRule.expect(StatusRuntimeException.class); - exceptionRule.expectMessage("UNKNOWN: wallet is already locked"); - var request = createLockWalletRequest(); - grpcStubs.walletsService.lockWallet(request); - } - - @Test - @Order(7) - public void testUnlockWalletTimeoutOverride() { - unlockWallet("first-password", 2); - sleep(500); // override unlock timeout after 0.5s - unlockWallet("first-password", 6); - sleep(5000); - getBalance(); // getbalance 5s after resetting unlock timeout to 6s - } - - @Test - @Order(8) - public void testSetNewWalletPassword() { - var request = createSetWalletPasswordRequest("first-password", "second-password"); - grpcStubs.walletsService.setWalletPassword(request); - - unlockWallet("second-password", 2); - getBalance(); - sleep(2500); // allow time for wallet save - } - - @Test - @Order(9) - public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() { - exceptionRule.expect(StatusRuntimeException.class); - exceptionRule.expectMessage("UNKNOWN: incorrect old password"); - var request = createSetWalletPasswordRequest("bad old password", "irrelevant"); - grpcStubs.walletsService.setWalletPassword(request); - } - - @Test - @Order(10) - public void testRemoveNewWalletPassword() { - var request = createRemoveWalletPasswordRequest("second-password"); - grpcStubs.walletsService.removeWalletPassword(request); - getBalance(); // should not throw 'wallet locked' exception - } - - @AfterClass - public static void tearDown() { - tearDownScaffold(); - } -} diff --git a/apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java b/apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java deleted file mode 100644 index 95c793482c5..00000000000 --- a/apitest/src/main/java/bisq/apitest/scenario/FundWalletScenarioTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.scenario; - -import lombok.extern.slf4j.Slf4j; - -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - - - -import bisq.apitest.OrderedRunner; -import bisq.apitest.annotation.Order; - -@Slf4j -@RunWith(OrderedRunner.class) -public class FundWalletScenarioTest extends ScenarioTest { - - @BeforeClass - public static void setUp() { - try { - setUpScaffold("bitcoind,seednode,alicedaemon"); - bitcoinCli.generateBlocks(1); - MILLISECONDS.sleep(1500); - } catch (InterruptedException ex) { - fail(ex.getMessage()); - } - } - - @Test - @Order(1) - public void testFundWallet() { - long balance = getBalance(); // bisq wallet was initialized with 10 btc - assertEquals(1000000000, balance); - - String unusedAddress = getUnusedBtcAddress(); - bitcoinCli.sendToAddress(unusedAddress, "2.5"); - - bitcoinCli.generateBlocks(1); - sleep(1500); - - balance = getBalance(); - assertEquals(1250000000L, balance); // new balance is 12.5 btc - } - - @AfterClass - public static void tearDown() { - tearDownScaffold(); - } -} diff --git a/apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java b/apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java deleted file mode 100644 index 9750b2ed9d6..00000000000 --- a/apitest/src/main/java/bisq/apitest/scenario/ScenarioTest.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest.scenario; - -import lombok.extern.slf4j.Slf4j; - - - -import bisq.apitest.method.MethodTest; - -@Slf4j -public class ScenarioTest extends MethodTest { -} diff --git a/apitest/src/main/java/bisq/apitest/scenario/ScenarioTestMain.java b/apitest/src/main/java/bisq/apitest/scenario/ScenarioTestMain.java deleted file mode 100644 index 0fb115939a9..00000000000 --- a/apitest/src/main/java/bisq/apitest/scenario/ScenarioTestMain.java +++ /dev/null @@ -1,23 +0,0 @@ -package bisq.apitest.scenario; - -import lombok.extern.slf4j.Slf4j; - -import static bisq.apitest.JUnitHelper.runTests; - -/** - * Driver for running API scenario tests. - * - * This may not seem necessary, but test cases are contained in the apitest sub - * project's main sources, not its test sources. An IDE may not automatically configure - * JUnit test launchers, and a gradle build will not automatically run the test cases. - * - * However, it is easy to manually configure an IDE launcher to run all, some or one - * JUnit test, and new gradle tasks should be provided to run all, some, or one test. - */ -@Slf4j -public class ScenarioTestMain { - - public static void main(String[] args) { - runTests(FundWalletScenarioTest.class); - } -} From 7c974b22ac96e3bf88d9896ca283c4c7d2ac595e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 19 Jul 2020 14:18:34 -0300 Subject: [PATCH 64/88] Moving GrpcStubs to test sources --- .../src/main/java/bisq/apitest/GrpcStubs.java | 109 ------------------ 1 file changed, 109 deletions(-) delete mode 100644 apitest/src/main/java/bisq/apitest/GrpcStubs.java diff --git a/apitest/src/main/java/bisq/apitest/GrpcStubs.java b/apitest/src/main/java/bisq/apitest/GrpcStubs.java deleted file mode 100644 index 6279c61489f..00000000000 --- a/apitest/src/main/java/bisq/apitest/GrpcStubs.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.apitest; - -import bisq.proto.grpc.GetVersionGrpc; -import bisq.proto.grpc.OffersGrpc; -import bisq.proto.grpc.PaymentAccountsGrpc; -import bisq.proto.grpc.WalletsGrpc; - -import io.grpc.CallCredentials; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Metadata; - -import java.util.concurrent.Executor; - -import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; -import static io.grpc.Status.UNAUTHENTICATED; -import static java.lang.String.format; -import static java.util.concurrent.TimeUnit.SECONDS; - - - -import bisq.apitest.config.ApiTestConfig; -import bisq.apitest.config.BisqAppConfig; - -public class GrpcStubs { - - public final CallCredentials credentials; - public final String host; - public final int port; - - public GetVersionGrpc.GetVersionBlockingStub versionService; - public OffersGrpc.OffersBlockingStub offersService; - public PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService; - public WalletsGrpc.WalletsBlockingStub walletsService; - - public GrpcStubs(BisqAppConfig bisqAppConfig, ApiTestConfig config) { - this.credentials = new PasswordCallCredentials(config.apiPassword); - this.host = "localhost"; - this.port = bisqAppConfig.apiPort; - } - - public GrpcStubs init() { - var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { - channel.shutdown().awaitTermination(1, SECONDS); - } catch (InterruptedException ex) { - throw new IllegalStateException(ex); - } - })); - - this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); - this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); - this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); - this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); - - return this; - } - - static class PasswordCallCredentials extends CallCredentials { - - public static final String PASSWORD_KEY = "password"; - private final String passwordValue; - - public PasswordCallCredentials(String passwordValue) { - if (passwordValue == null) - throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY)); - this.passwordValue = passwordValue; - } - - @Override - public void applyRequestMetadata(RequestInfo requestInfo, - Executor appExecutor, - MetadataApplier metadataApplier) { - appExecutor.execute(() -> { - try { - var headers = new Metadata(); - var passwordKey = Metadata.Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER); - headers.put(passwordKey, passwordValue); - metadataApplier.apply(headers); - } catch (Throwable ex) { - metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); - } - }); - } - - @Override - public void thisUsesUnstableApi() { - // An experimental api. A noop but never called; tries to make it clearer to - // implementors that they may break in the future. - } - } -} From bf584c218f43f4a2de65f90fcadf903e79a7fd55 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 19 Jul 2020 16:31:46 -0300 Subject: [PATCH 65/88] Move test cases into subproject test sources This change reorganizes the ':apitest' subproject to conform to a typical gradle project, where all JUnit test cases are located in the subproject's test sources folder. This makes running tests from an IDE or gradle command line interface work as expected. It will also help keep Travis CI configuration simple. To avoid interfering in normal builds, the gradle ':apitest test' task is disable by default; API tests will only run when a '-Dforce-true' system property is passed to gradle. To run API tests, run a normal build and install dao-setup files: ./gradlew clean build :apitest:installDaoSetup Then run the tests: ./gradlew :apitest:test -Dforce=true Try to avoid adding the '-Dforce=true' property to any other gradle tasks, because this enables the ':apitest test' task, and would kick off API tests before a normal build completed. The build.gradle file was modified to support this code reorg, and the 'org.junit.jupiter' dependendency was upgraded to v5.6.2 -- only in the ':apitest:test' dependency definiitions, not anywhere else in the bisq dependency definitions. The upgrade is necessary for running ordered tests. Since the scaffolding may be set up from either test cases (under the test src folder), or a class under the main src folder, some changes were made to ensure configuration paths are correct for either use case. For example, when the 'bisq-apitest' script is run from the root project directory, the current working directory is the root project directory. When gradle or an IDE is used to run @Test cases, the current working directory is :apitest subproject directory. The main source's ApiTestMain class has been stripped down, and exists only to keep the gradle build happy -- it needs a 'mainClassName' property. But this main driver does have uses. See the class comments. The other changes in this commit were made to fix style and syntax problems. --- .../main/java/bisq/apitest/ApiTestMain.java | 14 +- .../src/main/java/bisq/apitest/Scaffold.java | 20 +-- .../bisq/apitest/config/ApiTestConfig.java | 31 ++-- .../main/java/bisq/apitest/linux/BisqApp.java | 14 +- .../apitest/linux/SystemCommandExecutor.java | 6 +- .../test/java/bisq/apitest/ApiTestCase.java | 62 ++++++++ .../src/test/java/bisq/apitest/GrpcStubs.java | 109 ++++++++++++++ .../test/java/bisq/apitest/JUnitHelper.java | 58 ++++++++ .../bisq/apitest/method/BitcoinCliHelper.java | 86 +++++++++++ .../bisq/apitest/method/GetBalanceTest.java | 66 +++++++++ .../bisq/apitest/method/GetVersionTest.java | 56 +++++++ .../java/bisq/apitest/method/MethodTest.java | 89 ++++++++++++ .../apitest/method/WalletProtectionTest.java | 137 ++++++++++++++++++ .../scenario/FundWalletScenarioTest.java | 69 +++++++++ .../bisq/apitest/scenario/ScenarioTest.java | 28 ++++ build.gradle | 24 ++- 16 files changed, 838 insertions(+), 31 deletions(-) create mode 100644 apitest/src/test/java/bisq/apitest/ApiTestCase.java create mode 100644 apitest/src/test/java/bisq/apitest/GrpcStubs.java create mode 100644 apitest/src/test/java/bisq/apitest/JUnitHelper.java create mode 100644 apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java create mode 100644 apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java create mode 100644 apitest/src/test/java/bisq/apitest/method/GetVersionTest.java create mode 100644 apitest/src/test/java/bisq/apitest/method/MethodTest.java create mode 100644 apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java create mode 100644 apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java create mode 100644 apitest/src/test/java/bisq/apitest/scenario/ScenarioTest.java diff --git a/apitest/src/main/java/bisq/apitest/ApiTestMain.java b/apitest/src/main/java/bisq/apitest/ApiTestMain.java index ea58c33e837..b79fe6cc3be 100644 --- a/apitest/src/main/java/bisq/apitest/ApiTestMain.java +++ b/apitest/src/main/java/bisq/apitest/ApiTestMain.java @@ -29,9 +29,19 @@ import bisq.apitest.config.ApiTestConfig; /** - * ApiTest Application + * ApiTestMain is a placeholder for the gradle build file, which requires a valid + * 'mainClassName' property in the :apitest subproject configuration. * - * Runs all method tests, scenario tests, and end to end tests (e2e). + * It does has some uses: + * + * It can be used to print test scaffolding options: bisq-apitest --help. + * + * It can be used to smoke test your bitcoind environment: bisq-apitest. + * + * It can be used to run the regtest/dao environment for release testing: + * bisq-test --shutdownAfterTests=false + * + * All method, scenario and end to end tests are found in the test sources folder. * * Requires bitcoind v0.19.x */ diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index dd3f022bb43..fe2b9a3e04e 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -96,7 +96,7 @@ public class Scaffold { * @param supportingApps String */ public Scaffold(String supportingApps) { - this(new ApiTestConfig(new String[]{"--supportingApps", supportingApps})); + this(new ApiTestConfig("--supportingApps", supportingApps)); } /** @@ -181,29 +181,29 @@ public void tearDown() { public void installDaoSetupDirectories() { cleanDaoSetupDirectories(); - String srcResourcesDir = Paths.get("apitest", "src", "main", "resources", "dao-setup").toFile().getAbsolutePath(); + String daoSetupDir = Paths.get(config.baseSrcResourcesDir, "dao-setup").toFile().getAbsolutePath(); String buildDataDir = config.rootAppDataDir.getAbsolutePath(); try { - if (!new File(srcResourcesDir).exists()) + if (!new File(daoSetupDir).exists()) throw new FileNotFoundException( format("Dao setup dir '%s' not found. Run gradle :apitest:installDaoSetup" + " to download dao-setup.zip and extract contents to resources folder", - srcResourcesDir)); + daoSetupDir)); BashCommand copyBitcoinRegtestDir = new BashCommand( - "cp -rf " + srcResourcesDir + "/Bitcoin-regtest/regtest" + "cp -rf " + daoSetupDir + "/Bitcoin-regtest/regtest" + " " + config.bitcoinDatadir); if (copyBitcoinRegtestDir.run().getExitStatus() != 0) throw new IllegalStateException("Could not install bitcoin regtest dir"); BashCommand copyAliceDataDir = new BashCommand( - "cp -rf " + srcResourcesDir + "/" + alicedaemon.appName + "cp -rf " + daoSetupDir + "/" + alicedaemon.appName + " " + config.rootAppDataDir); if (copyAliceDataDir.run().getExitStatus() != 0) throw new IllegalStateException("Could not install alice data dir"); BashCommand copyBobDataDir = new BashCommand( - "cp -rf " + srcResourcesDir + "/" + bobdaemon.appName + "cp -rf " + daoSetupDir + "/" + bobdaemon.appName + " " + config.rootAppDataDir); if (copyBobDataDir.run().getExitStatus() != 0) throw new IllegalStateException("Could not install bob data dir"); @@ -217,7 +217,7 @@ public void installDaoSetupDirectories() { installBitcoinBlocknotify(); } catch (IOException | InterruptedException ex) { - throw new IllegalStateException("Could not install dao-setup files from " + srcResourcesDir, ex); + throw new IllegalStateException("Could not install dao-setup files from " + daoSetupDir, ex); } } @@ -254,7 +254,7 @@ private void cleanDaoSetupDirectories() { private void installBitcoinBlocknotify() { // gradle is not working for this try { - Path srcPath = Paths.get("apitest", "src", "main", "resources", "blocknotify"); + Path srcPath = Paths.get(config.baseSrcResourcesDir, "blocknotify"); Path destPath = Paths.get(config.bitcoinDatadir, "blocknotify"); Files.copy(srcPath, destPath, REPLACE_EXISTING); String chmod700Perms = "rwx------"; @@ -283,7 +283,7 @@ private void startBackgroundProcesses(ExecutorService executor, bitcoinDaemon.verifyBitcoinPathsExist(true); bitcoindTask = new SetupTask(bitcoinDaemon, countdownLatch); bitcoindTaskFuture = executor.submit(bitcoindTask); - MILLISECONDS.sleep(3500); + MILLISECONDS.sleep(3500); // todo make configurable bitcoinDaemon.verifyBitcoindRunning(); } diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index edddfbe2fb6..22ae67fccee 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -104,20 +104,32 @@ public class ApiTestConfig { public final boolean shutdownAfterTests; public final List supportingApps; - // Immutable system configurations. + // Immutable system configurations set in the constructor. public final String bitcoinDatadir; public final String userDir; + public final boolean isRunningTest; + public final String rootProjectDir; + public final String baseBuildResourcesDir; + public final String baseSrcResourcesDir; + // The parser that will be used to parse both cmd line and config file options private final OptionParser parser = new OptionParser(); public ApiTestConfig(String... args) { - this.defaultConfigFile = absoluteConfigFile( - Paths.get("apitest", "build", "resources", "main").toFile().getAbsolutePath(), - DEFAULT_CONFIG_FILE_NAME); - this.bitcoinDatadir = Paths.get("apitest", "build", "resources", "main", "Bitcoin-regtest") - .toFile().getAbsolutePath(); this.userDir = getProperty("user.dir"); + // If running a @Test, the current working directory is the :apitest subproject + // folder. If running ApiTestMain, the current working directory is the + // bisq root project folder. + this.isRunningTest = Paths.get(userDir).getFileName().toString().equals("apitest"); + this.rootProjectDir = isRunningTest + ? Paths.get(userDir).getParent().toFile().getAbsolutePath() + : Paths.get(userDir).toFile().getAbsolutePath(); + this.baseBuildResourcesDir = Paths.get(rootProjectDir, "apitest", "build", "resources", "main").toFile().getAbsolutePath(); + this.baseSrcResourcesDir = Paths.get(rootProjectDir, "apitest", "src", "main", "resources").toFile().getAbsolutePath(); + + this.defaultConfigFile = absoluteConfigFile(baseBuildResourcesDir, DEFAULT_CONFIG_FILE_NAME); + this.bitcoinDatadir = Paths.get(baseBuildResourcesDir, "Bitcoin-regtest").toFile().getAbsolutePath(); AbstractOptionSpec helpOpt = parser.accepts(HELP, "Print this help text") @@ -134,8 +146,7 @@ public ApiTestConfig(String... args) { parser.accepts(ROOT_APP_DATA_DIR, "Application data directory") .withRequiredArg() .ofType(File.class) - .defaultsTo(Paths.get("apitest", "build", "resources", "main") - .toFile().getAbsoluteFile()); + .defaultsTo(new File(baseBuildResourcesDir)); ArgumentAcceptingOptionSpec bashPathOpt = parser.accepts(BASH_PATH, "Bash path") @@ -193,7 +204,7 @@ public ApiTestConfig(String... args) { "Run Arbitration node as desktop") .withRequiredArg() .ofType(Boolean.class) - .defaultsTo(false); // TODO how do I register arbitrator? + .defaultsTo(false); // TODO how do I register mediator? ArgumentAcceptingOptionSpec runAliceNodeAsDesktopOpt = parser.accepts(RUN_ALICE_NODE_AS_DESKTOP, @@ -309,7 +320,7 @@ public ApiTestConfig(String... args) { } public boolean hasSupportingApp(String... supportingApp) { - return stream(supportingApp).anyMatch(a -> this.supportingApps.contains(a)); + return stream(supportingApp).anyMatch(this.supportingApps::contains); } public void printHelp(OutputStream sink, HelpFormatter formatter) { diff --git a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java index f5c4fb48a0f..a5d20a43a72 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java +++ b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java @@ -65,11 +65,12 @@ public BisqApp(BisqAppConfig bisqAppConfig, ApiTestConfig config) { this.fullDaoNode = true; this.useLocalhostForP2P = true; this.useDevPrivilegeKeys = true; - this.findBisqPidScript = config.userDir + "/apitest/scripts/get-bisq-pid.sh"; + this.findBisqPidScript = (config.isRunningTest ? "." : "./apitest") + + "/scripts/get-bisq-pid.sh"; } @Override - public void start() throws InterruptedException, IOException { + public void start() { try { if (config.runSubprojectJars) runJar(); // run subproject/build/lib/*.jar (not full build) @@ -86,7 +87,7 @@ public long getPid() { } @Override - public void shutdown() throws IOException, InterruptedException { + public void shutdown() { try { log.info("Shutting down {} ...", bisqAppConfig.appName); if (!isAlive(pid)) @@ -156,8 +157,10 @@ private void runJar() throws IOException, InterruptedException { // It runs a bisq-* startup script, and depends on a full build. Bisq jars // are loaded from the root project's lib directory. private void runStartupScript() throws IOException, InterruptedException { + String startupScriptPath = config.rootProjectDir + + "/" + bisqAppConfig.startupScript; String bisqCmd = getJavaOptsSpec() - + " " + config.userDir + "/" + bisqAppConfig.startupScript + + " " + startupScriptPath + " " + String.join(" ", getOptsList()) + " &"; // run in background without nohup runBashCommand(bisqCmd); @@ -172,7 +175,8 @@ private void runBashCommand(String bisqCmd) throws IOException, InterruptedExcep bashCommand.runInBackground(); if (bashCommand.getExitStatus() != 0) - throw new IllegalStateException(format("Error starting BisqApp\n%s\nError: %s", + throw new IllegalStateException(format("Error starting BisqApp%n%s%nError: %s", + bisqAppConfig.appName, bashCommand.getError())); // Sometimes it takes a little extra time to find the linux process id. diff --git a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java index f0960ba77aa..25c0cf8fe0d 100644 --- a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java +++ b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java @@ -62,14 +62,14 @@ public SystemCommandExecutor(final List cmdOptions) { if (log.isDebugEnabled()) log.debug("cmd options {}", cmdOptions.toString()); + if (cmdOptions.isEmpty()) + throw new IllegalStateException("No command params specified."); + if (cmdOptions.contains("sudo")) { log.error("", new IllegalStateException("'sudo' commands are prohibited.")); exit(1); } - if (cmdOptions == null) - throw new IllegalStateException("No command params specified."); - this.cmdOptions = cmdOptions; } diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java new file mode 100644 index 00000000000..d3626301436 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -0,0 +1,62 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest; + +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.method.BitcoinCliHelper; + +public class ApiTestCase { + + // The gRPC service stubs are used by method & scenario tests, but not e2e tests. + protected static GrpcStubs grpcStubs; + + protected static Scaffold scaffold; + protected static ApiTestConfig config; + protected static BitcoinCliHelper bitcoinCli; + + public static void setUpScaffold(String supportingApps) { + // The supportingApps argument is a comma delimited string of supporting app + // names, e.g. "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon" + scaffold = new Scaffold(supportingApps).setUp(); + config = scaffold.config; + bitcoinCli = new BitcoinCliHelper((config)); + grpcStubs = new GrpcStubs(alicedaemon, config).init(); + } + + public static void setUpScaffold() { + scaffold = new Scaffold(new String[]{}).setUp(); + config = scaffold.config; + grpcStubs = new GrpcStubs(alicedaemon, config).init(); + } + + public static void tearDownScaffold() { + scaffold.tearDown(); + } + + protected void sleep(long ms) { + try { + MILLISECONDS.sleep(ms); + } catch (InterruptedException ignored) { + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/GrpcStubs.java b/apitest/src/test/java/bisq/apitest/GrpcStubs.java new file mode 100644 index 00000000000..6279c61489f --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/GrpcStubs.java @@ -0,0 +1,109 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest; + +import bisq.proto.grpc.GetVersionGrpc; +import bisq.proto.grpc.OffersGrpc; +import bisq.proto.grpc.PaymentAccountsGrpc; +import bisq.proto.grpc.WalletsGrpc; + +import io.grpc.CallCredentials; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; + +import java.util.concurrent.Executor; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static io.grpc.Status.UNAUTHENTICATED; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.SECONDS; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.config.BisqAppConfig; + +public class GrpcStubs { + + public final CallCredentials credentials; + public final String host; + public final int port; + + public GetVersionGrpc.GetVersionBlockingStub versionService; + public OffersGrpc.OffersBlockingStub offersService; + public PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService; + public WalletsGrpc.WalletsBlockingStub walletsService; + + public GrpcStubs(BisqAppConfig bisqAppConfig, ApiTestConfig config) { + this.credentials = new PasswordCallCredentials(config.apiPassword); + this.host = "localhost"; + this.port = bisqAppConfig.apiPort; + } + + public GrpcStubs init() { + var channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + channel.shutdown().awaitTermination(1, SECONDS); + } catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + })); + + this.versionService = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + + return this; + } + + static class PasswordCallCredentials extends CallCredentials { + + public static final String PASSWORD_KEY = "password"; + private final String passwordValue; + + public PasswordCallCredentials(String passwordValue) { + if (passwordValue == null) + throw new IllegalArgumentException(format("'%s' value must not be null", PASSWORD_KEY)); + this.passwordValue = passwordValue; + } + + @Override + public void applyRequestMetadata(RequestInfo requestInfo, + Executor appExecutor, + MetadataApplier metadataApplier) { + appExecutor.execute(() -> { + try { + var headers = new Metadata(); + var passwordKey = Metadata.Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER); + headers.put(passwordKey, passwordValue); + metadataApplier.apply(headers); + } catch (Throwable ex) { + metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); + } + }); + } + + @Override + public void thisUsesUnstableApi() { + // An experimental api. A noop but never called; tries to make it clearer to + // implementors that they may break in the future. + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/JUnitHelper.java b/apitest/src/test/java/bisq/apitest/JUnitHelper.java new file mode 100644 index 00000000000..03f16ee6b11 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/JUnitHelper.java @@ -0,0 +1,58 @@ +package bisq.apitest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.runner.Description; +import org.junit.runner.JUnitCore; +import org.junit.runner.Result; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; + +import static java.lang.String.format; + +@Slf4j +public class JUnitHelper { + + private static boolean allPass; + + public static void runTests(Class... testClasses) { + JUnitCore jUnitCore = new JUnitCore(); + jUnitCore.addListener(new RunListener() { + public void testStarted(Description description) { + log.info("{}", description); + } + + public void testIgnored(Description description) { + log.info("Ignored {}", description); + } + + public void testFailure(Failure failure) { + log.error("Failed {}", failure.getTrace()); + } + }); + Result result = jUnitCore.run(testClasses); + printTestResults(result); + } + + public static void printTestResults(Result result) { + log.info("Total tests: {}, Failed: {}, Ignored: {}", + result.getRunCount(), + result.getFailureCount(), + result.getIgnoreCount()); + + if (result.wasSuccessful()) { + log.info("All {} tests passed", result.getRunCount()); + allPass = true; + } else if (result.getFailureCount() > 0) { + log.error("{} test(s) failed", result.getFailureCount()); + result.getFailures().iterator().forEachRemaining(f -> log.error(format("%s.%s()%n\t%s", + f.getDescription().getTestClass().getName(), + f.getDescription().getMethodName(), + f.getTrace()))); + } + } + + public static boolean allTestsPassed() { + return allPass; + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java new file mode 100644 index 00000000000..bed3477801a --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java @@ -0,0 +1,86 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method; + +import java.io.IOException; + +import static java.lang.String.format; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + + + +import bisq.apitest.config.ApiTestConfig; +import bisq.apitest.linux.BitcoinCli; + +public final class BitcoinCliHelper { + + private final ApiTestConfig config; + + public BitcoinCliHelper(ApiTestConfig config) { + this.config = config; + } + + // Convenience methods for making bitcoin-cli calls. + + public String getNewBtcAddress() { + try { + String newAddress = new BitcoinCli(config, "getnewaddress").run().getOutput(); + assertNotNull(newAddress); + return newAddress; + } catch (IOException | InterruptedException ex) { + fail(ex.getMessage()); + return null; + } + } + + public String[] generateToAddress(int blocks, String address) { + try { + String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address); + BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); + String[] txids = generateToAddress.getOutputValueAsStringArray(); + assertNotNull(txids); + return txids; + } catch (IOException | InterruptedException ex) { + fail(ex.getMessage()); + return null; + } + } + + public void generateBlocks(int blocks) { + generateToAddress(blocks, getNewBtcAddress()); + } + + public String sendToAddress(String address, String amount) { + // sendtoaddress "address" amount \ + // ( "comment" "comment_to" subtractfeefromamount \ + // replaceable conf_target "estimate_mode" ) + // returns a transaction id + try { + String sendToAddressCmd = format("sendtoaddress \"%s\" %s \"\" \"\" false", + address, amount); + BitcoinCli sendToAddress = new BitcoinCli(config, sendToAddressCmd).run(); + String txid = sendToAddress.getOutput(); + assertNotNull(txid); + return txid; + } catch (IOException | InterruptedException ex) { + fail(ex.getMessage()); + return null; + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java new file mode 100644 index 00000000000..5dac0ca7d74 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java @@ -0,0 +1,66 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method; + +import bisq.proto.grpc.GetBalanceRequest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class GetBalanceTest extends MethodTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold("bitcoind,seednode,alicedaemon"); + + // Have to generate 1 regtest block for alice's wallet to show 10 BTC balance. + bitcoinCli.generateBlocks(1); + + // Give the alicedaemon time to parse the new block. + MILLISECONDS.sleep(1500); + } catch (InterruptedException ex) { + fail(ex.getMessage()); + } + } + + @Test + @Order(1) + public void testGetBalance() { + var balance = grpcStubs.walletsService.getBalance(GetBalanceRequest.newBuilder().build()).getBalance(); + assertEquals(1000000000, balance); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java new file mode 100644 index 00000000000..91305de8ba5 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java @@ -0,0 +1,56 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method; + +import bisq.proto.grpc.GetVersionRequest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.common.app.Version.VERSION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + + +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class GetVersionTest extends MethodTest { + + @BeforeAll + public static void setUp() { + setUpScaffold(alicedaemon.name()); + } + + @Test + @Order(1) + public void testGetVersion() { + var version = grpcStubs.versionService.getVersion(GetVersionRequest.newBuilder().build()).getVersion(); + assertEquals(VERSION, version); + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java new file mode 100644 index 00000000000..37dea63cdb9 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -0,0 +1,89 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.method; + +import bisq.proto.grpc.GetBalanceRequest; +import bisq.proto.grpc.GetFundingAddressesRequest; +import bisq.proto.grpc.LockWalletRequest; +import bisq.proto.grpc.RemoveWalletPasswordRequest; +import bisq.proto.grpc.SetWalletPasswordRequest; +import bisq.proto.grpc.UnlockWalletRequest; + + + +import bisq.apitest.ApiTestCase; + +public class MethodTest extends ApiTestCase { + + // Convenience methods for building gRPC request objects + + protected final GetBalanceRequest createBalanceRequest() { + return GetBalanceRequest.newBuilder().build(); + } + + protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) { + return SetWalletPasswordRequest.newBuilder().setPassword(password).build(); + } + + protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String oldPassword, String newPassword) { + return SetWalletPasswordRequest.newBuilder().setPassword(oldPassword).setNewPassword(newPassword).build(); + } + + protected final RemoveWalletPasswordRequest createRemoveWalletPasswordRequest(String password) { + return RemoveWalletPasswordRequest.newBuilder().setPassword(password).build(); + } + + protected final UnlockWalletRequest createUnlockWalletRequest(String password, long timeout) { + return UnlockWalletRequest.newBuilder().setPassword(password).setTimeout(timeout).build(); + } + + protected final LockWalletRequest createLockWalletRequest() { + return LockWalletRequest.newBuilder().build(); + } + + protected final GetFundingAddressesRequest createGetFundingAddressesRequest() { + return GetFundingAddressesRequest.newBuilder().build(); + } + + // Convenience methods for calling frequently used & thoroughly tested gRPC services. + + protected final long getBalance() { + return grpcStubs.walletsService.getBalance(createBalanceRequest()).getBalance(); + } + + protected final void unlockWallet(String password, long timeout) { + //noinspection ResultOfMethodCallIgnored + grpcStubs.walletsService.unlockWallet(createUnlockWalletRequest(password, timeout)); + } + + protected final void lockWallet() { + //noinspection ResultOfMethodCallIgnored + grpcStubs.walletsService.lockWallet(createLockWalletRequest()); + } + + protected final String getUnusedBtcAddress() { + return grpcStubs.walletsService.getFundingAddresses(createGetFundingAddressesRequest()) + .getAddressBalanceInfoList() + .stream() + .filter(a -> a.getBalance() == 0 && a.getNumConfirmations() == 0) + .findFirst() + .get() + .getAddress(); + + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java new file mode 100644 index 00000000000..388c667f084 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java @@ -0,0 +1,137 @@ +package bisq.apitest.method; + +import io.grpc.StatusRuntimeException; + +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + +@SuppressWarnings("ResultOfMethodCallIgnored") +@Slf4j +@TestMethodOrder(OrderAnnotation.class) +public class WalletProtectionTest extends MethodTest { + + @BeforeAll + public static void setUp() { + setUpScaffold(alicedaemon.name()); + try { + TimeUnit.MILLISECONDS.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Test + @Order(1) + public void testSetWalletPassword() { + var request = createSetWalletPasswordRequest("first-password"); + grpcStubs.walletsService.setWalletPassword(request); + } + + @Test + @Order(2) + public void testGetBalanceOnEncryptedWalletShouldThrowException() { + Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance); + assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); + } + + @Test + @Order(3) + public void testUnlockWalletFor4Seconds() { + var request = createUnlockWalletRequest("first-password", 4); + grpcStubs.walletsService.unlockWallet(request); + getBalance(); // should not throw 'wallet locked' exception + + sleep(4500); // let unlock timeout expire + Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance); + assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); + } + + @Test + @Order(4) + public void testGetBalanceAfterUnlockTimeExpiryShouldThrowException() { + var request = createUnlockWalletRequest("first-password", 3); + grpcStubs.walletsService.unlockWallet(request); + sleep(4000); // let unlock timeout expire + Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance); + assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); + } + + @Test + @Order(5) + public void testLockWalletBeforeUnlockTimeoutExpiry() { + unlockWallet("first-password", 60); + var request = createLockWalletRequest(); + grpcStubs.walletsService.lockWallet(request); + + Throwable exception = assertThrows(StatusRuntimeException.class, this::getBalance); + assertEquals("UNKNOWN: wallet is locked", exception.getMessage()); + } + + @Test + @Order(6) + public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() { + var request = createLockWalletRequest(); + + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + grpcStubs.walletsService.lockWallet(request)); + assertEquals("UNKNOWN: wallet is already locked", exception.getMessage()); + + } + + @Test + @Order(7) + public void testUnlockWalletTimeoutOverride() { + unlockWallet("first-password", 2); + sleep(500); // override unlock timeout after 0.5s + unlockWallet("first-password", 6); + sleep(5000); + getBalance(); // getbalance 5s after resetting unlock timeout to 6s + } + + @Test + @Order(8) + public void testSetNewWalletPassword() { + var request = createSetWalletPasswordRequest("first-password", "second-password"); + grpcStubs.walletsService.setWalletPassword(request); + + unlockWallet("second-password", 2); + getBalance(); + sleep(2500); // allow time for wallet save + } + + @Test + @Order(9) + public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() { + var request = createSetWalletPasswordRequest("bad old password", "irrelevant"); + + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + grpcStubs.walletsService.setWalletPassword(request)); + assertEquals("UNKNOWN: incorrect old password", exception.getMessage()); + } + + @Test + @Order(10) + public void testRemoveNewWalletPassword() { + var request = createRemoveWalletPasswordRequest("second-password"); + grpcStubs.walletsService.removeWalletPassword(request); + getBalance(); // should not throw 'wallet locked' exception + } + + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java b/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java new file mode 100644 index 00000000000..64a01363569 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java @@ -0,0 +1,69 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class FundWalletScenarioTest extends ScenarioTest { + + @BeforeAll + public static void setUp() { + try { + setUpScaffold("bitcoind,seednode,alicedaemon"); + bitcoinCli.generateBlocks(1); + MILLISECONDS.sleep(1500); + } catch (InterruptedException ex) { + fail(ex.getMessage()); + } + } + + @Test + @Order(1) + public void testFundWallet() { + long balance = getBalance(); // bisq wallet was initialized with 10 btc + assertEquals(1000000000, balance); + + String unusedAddress = getUnusedBtcAddress(); + bitcoinCli.sendToAddress(unusedAddress, "2.5"); + + bitcoinCli.generateBlocks(1); + sleep(1500); + + balance = getBalance(); + assertEquals(1250000000L, balance); // new balance is 12.5 btc + } + + @AfterAll + public static void tearDown() { + tearDownScaffold(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/ScenarioTest.java b/apitest/src/test/java/bisq/apitest/scenario/ScenarioTest.java new file mode 100644 index 00000000000..9750b2ed9d6 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/ScenarioTest.java @@ -0,0 +1,28 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.apitest.scenario; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.apitest.method.MethodTest; + +@Slf4j +public class ScenarioTest extends MethodTest { +} diff --git a/build.gradle b/build.gradle index ad902e11739..389f4a8bc17 100644 --- a/build.gradle +++ b/build.gradle @@ -579,6 +579,14 @@ configure(project(':apitest')) { // ./gradlew :apitest:cleanDaoSetup -x test apply from: 'dao-setup.gradle' + // We have to disable the :apitest 'test' task by default because we do not want + // to interfere with normal builds. To run JUnit tests in this subproject: + // Run a normal build and install dao-setup files: + // 'gradle clean build :apitest:installDaoSetup' + // Run apitest tests cases + // 'gradle :apitest:test' -Dforce=true + test.enabled = System.getProperty("force") == "true" + sourceSets { main { resources { @@ -588,6 +596,13 @@ configure(project(':apitest')) { } } + test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } + } + dependencies { compile project(':proto') compile project(':common') @@ -609,10 +624,17 @@ configure(project(':apitest')) { implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation "ch.qos.logback:logback-core:$logbackVersion" implementation "ch.qos.logback:logback-classic:$logbackVersion" - implementation "junit:junit:$junitVersion" + compileOnly "org.projectlombok:lombok:$lombokVersion" compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" annotationProcessor "org.projectlombok:lombok:$lombokVersion" + + testCompile "org.junit.jupiter:junit-jupiter-api:5.6.2" + testCompile "org.junit.jupiter:junit-jupiter-params:5.6.2" + testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" + testCompileOnly "org.projectlombok:lombok:$lombokVersion" + testRuntime "javax.annotation:javax.annotation-api:$javaxAnnotationVersion" + testRuntime("org.junit.jupiter:junit-jupiter-engine:5.6.2") } } From 591c8b295c6fb7968b6af86b579b75c6fd8353e4 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 20 Jul 2020 12:33:47 -0300 Subject: [PATCH 66/88] Change :apitest:test task system property name The property name 'force' is changed 'runApiTests'. To run the :apitest test task: ./gradlew :apitest:test -DrunApiTests=true --- build.gradle | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 389f4a8bc17..406c99e327a 100644 --- a/build.gradle +++ b/build.gradle @@ -571,7 +571,7 @@ configure(project(':apitest')) { // https://github.com/bisq-network/bisq/raw/master/docs/dao-setup.zip // These tasks are not run by the default build, but they can can be run during // full or partial builds, or by themselves. - // To run a full Bisq clean build, test, and install dao-setup files: + // To run the regular clean + build + test (non api), and install dao-setup files: // ./gradlew clean build :apitest:installDaoSetup // To install or re-install dao-setup file only: // ./gradlew :apitest:installDaoSetup -x test @@ -581,11 +581,9 @@ configure(project(':apitest')) { // We have to disable the :apitest 'test' task by default because we do not want // to interfere with normal builds. To run JUnit tests in this subproject: - // Run a normal build and install dao-setup files: - // 'gradle clean build :apitest:installDaoSetup' - // Run apitest tests cases - // 'gradle :apitest:test' -Dforce=true - test.enabled = System.getProperty("force") == "true" + // Run a normal build and install dao-setup files first, then run: + // 'gradle :apitest:test -DrunApiTests=true' + test.enabled = System.getProperty("runApiTests") == "true" sourceSets { main { From b4d3ea7e023c790ac07ad3c53fc8d6b991ddb1a0 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 20 Jul 2020 12:37:35 -0300 Subject: [PATCH 67/88] Add comment about Bisq DAO dev environment Some explanation is needed about why Bob & Alice have non-zero initial BTC and BSQ balances when tests run. The comments also include links to more detailed information about the DAO/ regtest testing environment. --- .../test/java/bisq/apitest/ApiTestCase.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java index d3626301436..2cc74883a9d 100644 --- a/apitest/src/test/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -25,6 +25,31 @@ import bisq.apitest.config.ApiTestConfig; import bisq.apitest.method.BitcoinCliHelper; +/** + * Base class for all test types: 'method', 'scenario' and 'e2e'. + *

+ * During scaffold setup, various combinations of bitcoind and bisq instances + * can be started in the background before test cases are run. Currently, this test + * harness supports only the "Bisq DAO development environment running against a + * local Bitcoin regtest network" as described in + * dev-setup.md + * and dao-setup.md. + *

+ * Those documents contain information about the configurations used by this test harness: + * bitcoin-core's bitcoin.conf and blocknotify values, bisq instance options, the DAO genesis + * transaction id, initial BSQ and BTC balances for Bob & Alice accounts, and default + * PerfectMoney dummy payment accounts (USD) for Bob and Alice. + *

+ * During a build, the + * dao-setup.zip + * file is downloaded and extracted if necessary. In each test case's @BeforeClass + * method, the DAO setup files are re-installed into the run time's data directories + * (each test case runs on a refreshed DAO/regtest environment setup). + *

+ * Initial Alice balances & accounts: 10.0 BTC, 1000000.00 BSQ, USD PerfectMoney dummy + *

+ * Initial Bob balances & accounts: 10.0 BTC, 1500000.00 BSQ, USD PerfectMoney dummy + */ public class ApiTestCase { // The gRPC service stubs are used by method & scenario tests, but not e2e tests. From 999e9ec93b6eea9d131b0255249acdfcec69fbb7 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 20 Jul 2020 14:53:00 -0300 Subject: [PATCH 68/88] Fix @BeforeClass error handling and use jupiter api The Scaffold set up was calling System.exit(1) when it encountered a configuration error, or a problem starting bitcoind & bisq instances. This incorrect error handling was hiding errors from gradle, and preventing tests that would otherwise successfully complete. This change fixes the problem by throwing an IllegalStateException up to the test case's @BeforeClass method -- to be caught and used in a JUnit fail(ex) call. An 'initializationError' triggered in @BeforeClass correctly bubbles up to gradle, and will not block execution of remaining tests. A gradle Test Summary containing any initialization errors is also produced in /apitest/build/reports/tests/test/index.html This change also fixes many import statements and asserts to ensure 'org.junit.jupiter.api.*' is used in place of 'org.junit.*', for proper gradle integration. --- .../src/main/java/bisq/apitest/Scaffold.java | 34 ++++++------------- .../src/main/java/bisq/apitest/SetupTask.java | 14 +++----- .../apitest/linux/AbstractLinuxProcess.java | 9 +++-- .../java/bisq/apitest/linux/LinuxProcess.java | 4 +-- .../apitest/linux/SystemCommandExecutor.java | 8 ++--- .../test/java/bisq/apitest/ApiTestCase.java | 10 ++++-- .../bisq/apitest/method/BitcoinCliHelper.java | 10 +++--- .../bisq/apitest/method/GetBalanceTest.java | 6 ++-- .../bisq/apitest/method/GetVersionTest.java | 7 +++- .../apitest/method/WalletProtectionTest.java | 12 +++---- .../scenario/FundWalletScenarioTest.java | 9 +++-- 11 files changed, 59 insertions(+), 64 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index fe2b9a3e04e..ef905a9d3e8 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -42,7 +42,6 @@ import static bisq.apitest.config.BisqAppConfig.*; import static java.lang.String.format; -import static java.lang.System.err; import static java.lang.System.exit; import static java.lang.System.out; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; @@ -128,27 +127,19 @@ public Scaffold(ApiTestConfig config) { } - public Scaffold setUp() { - try { - installDaoSetupDirectories(); - - // Start each background process from an executor, then add a shutdown hook. - CountDownLatch countdownLatch = new CountDownLatch(config.supportingApps.size()); - startBackgroundProcesses(executor, countdownLatch); - installShutdownHook(); + public Scaffold setUp() throws IOException, InterruptedException, ExecutionException { + installDaoSetupDirectories(); - // Wait for all submitted startup tasks to decrement the count of the latch. - Objects.requireNonNull(countdownLatch).await(); + // Start each background process from an executor, then add a shutdown hook. + CountDownLatch countdownLatch = new CountDownLatch(config.supportingApps.size()); + startBackgroundProcesses(executor, countdownLatch); + installShutdownHook(); - // Verify each startup task's future is done. - verifyStartupCompleted(); + // Wait for all submitted startup tasks to decrement the count of the latch. + Objects.requireNonNull(countdownLatch).await(); - } catch (Throwable ex) { - err.println("Fault: An unexpected error occurred. " + - "Please file a report at https://bisq.network/issues"); - ex.printStackTrace(err); - exit(EXIT_FAILURE); - } + // Verify each startup task's future is done. + verifyStartupCompleted(); return this; } @@ -336,10 +327,7 @@ private void startBisqApp(BisqAppConfig bisqAppConfig, log.info("Giving {} ms for {} to initialize ...", config.bisqAppInitTime, bisqAppConfig.appName); MILLISECONDS.sleep(config.bisqAppInitTime); if (bisqApp.hasStartupExceptions()) { - for (Throwable t : bisqApp.getStartupExceptions()) { - log.error("", t); - } - exit(EXIT_FAILURE); + throw bisqApp.startupIllegalStateException(log); } } diff --git a/apitest/src/main/java/bisq/apitest/SetupTask.java b/apitest/src/main/java/bisq/apitest/SetupTask.java index 56f3af07140..d2d76cba50c 100644 --- a/apitest/src/main/java/bisq/apitest/SetupTask.java +++ b/apitest/src/main/java/bisq/apitest/SetupTask.java @@ -25,9 +25,7 @@ import lombok.extern.slf4j.Slf4j; -import static bisq.apitest.Scaffold.EXIT_FAILURE; import static java.lang.String.format; -import static java.lang.System.exit; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -50,16 +48,14 @@ public Status call() throws Exception { try { linuxProcess.start(); // always runs in background MILLISECONDS.sleep(1000); // give 1s for bg process to init - if (linuxProcess.hasStartupExceptions()) { - for (Throwable t : linuxProcess.getStartupExceptions()) { - log.error("", t); - } - exit(EXIT_FAILURE); - } - } catch (InterruptedException ex) { throw new IllegalStateException(format("Error starting %s", linuxProcess.getName()), ex); } + + if (linuxProcess.hasStartupExceptions()) { + throw linuxProcess.startupIllegalStateException(log); + } + Objects.requireNonNull(countdownLatch).countDown(); return new Status(linuxProcess.getName(), LocalDateTime.now()); } diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index 5a9f5565508..a9b62c26be4 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -62,8 +62,13 @@ public boolean hasStartupExceptions() { } @Override - public List getStartupExceptions() { - return startupExceptions; + public IllegalStateException startupIllegalStateException(org.slf4j.Logger log) { + StringBuilder errorBuilder = new StringBuilder(); + for (Throwable t : startupExceptions) { + log.error("", t); + errorBuilder.append(t.getMessage()).append("\n"); + } + return new IllegalStateException(errorBuilder.toString().trim(), startupExceptions.get(0)); } @SuppressWarnings("unused") diff --git a/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java index 5c453de0b10..8c19be66f12 100644 --- a/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java @@ -19,8 +19,6 @@ import java.io.IOException; -import java.util.List; - public interface LinuxProcess { void start() throws InterruptedException, IOException; @@ -30,7 +28,7 @@ public interface LinuxProcess { boolean hasStartupExceptions(); - List getStartupExceptions(); + IllegalStateException startupIllegalStateException(org.slf4j.Logger log); void shutdown() throws IOException, InterruptedException; } diff --git a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java index 25c0cf8fe0d..28c0fde235e 100644 --- a/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java +++ b/apitest/src/main/java/bisq/apitest/linux/SystemCommandExecutor.java @@ -24,8 +24,6 @@ import lombok.extern.slf4j.Slf4j; -import static java.lang.System.exit; - /** * This class can be used to execute a system command from a Java application. * See the documentation for the public methods of this class for more @@ -65,10 +63,8 @@ public SystemCommandExecutor(final List cmdOptions) { if (cmdOptions.isEmpty()) throw new IllegalStateException("No command params specified."); - if (cmdOptions.contains("sudo")) { - log.error("", new IllegalStateException("'sudo' commands are prohibited.")); - exit(1); - } + if (cmdOptions.contains("sudo")) + throw new IllegalStateException("'sudo' commands are prohibited."); this.cmdOptions = cmdOptions; } diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java index 2cc74883a9d..e2301346c43 100644 --- a/apitest/src/test/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -17,6 +17,10 @@ package bisq.apitest; +import java.io.IOException; + +import java.util.concurrent.ExecutionException; + import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -59,7 +63,8 @@ public class ApiTestCase { protected static ApiTestConfig config; protected static BitcoinCliHelper bitcoinCli; - public static void setUpScaffold(String supportingApps) { + public static void setUpScaffold(String supportingApps) + throws InterruptedException, ExecutionException, IOException { // The supportingApps argument is a comma delimited string of supporting app // names, e.g. "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon" scaffold = new Scaffold(supportingApps).setUp(); @@ -68,7 +73,8 @@ public static void setUpScaffold(String supportingApps) { grpcStubs = new GrpcStubs(alicedaemon, config).init(); } - public static void setUpScaffold() { + public static void setUpScaffold() + throws InterruptedException, ExecutionException, IOException { scaffold = new Scaffold(new String[]{}).setUp(); config = scaffold.config; grpcStubs = new GrpcStubs(alicedaemon, config).init(); diff --git a/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java index bed3477801a..693da32966b 100644 --- a/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java +++ b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java @@ -20,8 +20,8 @@ import java.io.IOException; import static java.lang.String.format; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; @@ -44,7 +44,7 @@ public String getNewBtcAddress() { assertNotNull(newAddress); return newAddress; } catch (IOException | InterruptedException ex) { - fail(ex.getMessage()); + fail(ex); return null; } } @@ -57,7 +57,7 @@ public String[] generateToAddress(int blocks, String address) { assertNotNull(txids); return txids; } catch (IOException | InterruptedException ex) { - fail(ex.getMessage()); + fail(ex); return null; } } @@ -79,7 +79,7 @@ public String sendToAddress(String address, String amount) { assertNotNull(txid); return txid; } catch (IOException | InterruptedException ex) { - fail(ex.getMessage()); + fail(ex); return null; } } diff --git a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java b/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java index 5dac0ca7d74..2cf4e8ae1cd 100644 --- a/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/GetBalanceTest.java @@ -47,14 +47,16 @@ public static void setUp() { // Give the alicedaemon time to parse the new block. MILLISECONDS.sleep(1500); - } catch (InterruptedException ex) { - fail(ex.getMessage()); + } catch (Exception ex) { + fail(ex); } } @Test @Order(1) public void testGetBalance() { + // All tests depend on the DAO / regtest environment, and Alice's wallet is + // initialized with 10 BTC during the scaffolding setup. var balance = grpcStubs.walletsService.getBalance(GetBalanceRequest.newBuilder().build()).getBalance(); assertEquals(1000000000, balance); } diff --git a/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java index 91305de8ba5..22413cf9d3c 100644 --- a/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java +++ b/apitest/src/test/java/bisq/apitest/method/GetVersionTest.java @@ -30,6 +30,7 @@ import static bisq.apitest.config.BisqAppConfig.alicedaemon; import static bisq.common.app.Version.VERSION; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @@ -39,7 +40,11 @@ public class GetVersionTest extends MethodTest { @BeforeAll public static void setUp() { - setUpScaffold(alicedaemon.name()); + try { + setUpScaffold(alicedaemon.name()); + } catch (Exception ex) { + fail(ex); + } } @Test diff --git a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java index 388c667f084..a00d5333c01 100644 --- a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java +++ b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java @@ -2,8 +2,6 @@ import io.grpc.StatusRuntimeException; -import java.util.concurrent.TimeUnit; - import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; @@ -13,8 +11,10 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation; @SuppressWarnings("ResultOfMethodCallIgnored") @@ -24,11 +24,11 @@ public class WalletProtectionTest extends MethodTest { @BeforeAll public static void setUp() { - setUpScaffold(alicedaemon.name()); try { - TimeUnit.MILLISECONDS.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); + setUpScaffold(alicedaemon.name()); + MILLISECONDS.sleep(2000); + } catch (Exception ex) { + fail(ex); } } diff --git a/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java b/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java index 64a01363569..e95e310eb58 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/FundWalletScenarioTest.java @@ -27,9 +27,8 @@ import org.junit.jupiter.api.TestMethodOrder; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @@ -41,8 +40,8 @@ public static void setUp() { setUpScaffold("bitcoind,seednode,alicedaemon"); bitcoinCli.generateBlocks(1); MILLISECONDS.sleep(1500); - } catch (InterruptedException ex) { - fail(ex.getMessage()); + } catch (Exception ex) { + fail(ex); } } From 13a8396b4533f8d67578ac9db0129baed7f45be6 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 20 Jul 2020 15:05:10 -0300 Subject: [PATCH 69/88] Remove unnecessary curly braces --- apitest/src/main/java/bisq/apitest/Scaffold.java | 3 +-- apitest/src/main/java/bisq/apitest/SetupTask.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index ef905a9d3e8..12bd79b9bd7 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -326,9 +326,8 @@ private void startBisqApp(BisqAppConfig bisqAppConfig, } log.info("Giving {} ms for {} to initialize ...", config.bisqAppInitTime, bisqAppConfig.appName); MILLISECONDS.sleep(config.bisqAppInitTime); - if (bisqApp.hasStartupExceptions()) { + if (bisqApp.hasStartupExceptions()) throw bisqApp.startupIllegalStateException(log); - } } private BisqApp createBisqApp(BisqAppConfig bisqAppConfig) diff --git a/apitest/src/main/java/bisq/apitest/SetupTask.java b/apitest/src/main/java/bisq/apitest/SetupTask.java index d2d76cba50c..8818eee19ac 100644 --- a/apitest/src/main/java/bisq/apitest/SetupTask.java +++ b/apitest/src/main/java/bisq/apitest/SetupTask.java @@ -52,9 +52,8 @@ public Status call() throws Exception { throw new IllegalStateException(format("Error starting %s", linuxProcess.getName()), ex); } - if (linuxProcess.hasStartupExceptions()) { + if (linuxProcess.hasStartupExceptions()) throw linuxProcess.startupIllegalStateException(log); - } Objects.requireNonNull(countdownLatch).countDown(); return new Status(linuxProcess.getName(), LocalDateTime.now()); From cf031e688390332863c183344e1e0441baa2454f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 22 Jul 2020 10:58:20 -0300 Subject: [PATCH 70/88] Change 'missing bitcoind path' error msg Since JUnit tests cannot take program arguments, change the wording to instruct the user to configure the bitcoind path in apitest.properties --- .../src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index a9b62c26be4..bcaf72ece60 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -95,8 +95,7 @@ public void verifyBitcoinPathsExist(boolean verbose) { if (!bitcoindExecutable.exists() || !bitcoindExecutable.canExecute()) throw new IllegalStateException(format("'%s' cannot be found or executed.%n" + "A bitcoin-core v0.19.X installation is required, and" + - " a '--bitcoinPath' option must be passed on the command line" - + " or added to 'apitest.properties'", + " the 'bitcoinPath' must be configured in 'apitest.properties'", bitcoindExecutable.getAbsolutePath())); File bitcoindDatadir = new File(config.bitcoinDatadir); From 27ee4b8974ba364a386e94bcf699b28c0072157a Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 27 Jul 2020 16:35:46 -0300 Subject: [PATCH 71/88] Do not leave orphaned processes after failed teardown The test harness should not fail a test case's @AfterAll (teardown) method on the first background instance shutdown exception. This change makes the shutdown logic similar to the startup's: it caches any exceptions that may have occurred during an instance shutdown, logs them, then proceeds to shut down the next background instance. An IllegalStateException (the 1st one) is passed up to @AfterAll method only after the scaffolding teardown process is complete, to avoid leaving any orphaned java or bitcoind processes running after a java system exit. --- .../src/main/java/bisq/apitest/Scaffold.java | 38 +++++++++++++++---- .../src/main/java/bisq/apitest/SetupTask.java | 4 -- .../apitest/linux/AbstractLinuxProcess.java | 25 ++++++++++-- .../main/java/bisq/apitest/linux/BisqApp.java | 20 ++++++---- .../bisq/apitest/linux/BitcoinDaemon.java | 33 ++++++++++------ .../java/bisq/apitest/linux/LinuxProcess.java | 12 +++++- 6 files changed, 96 insertions(+), 36 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index 12bd79b9bd7..9319226211f 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -56,6 +57,7 @@ import bisq.apitest.linux.BashCommand; import bisq.apitest.linux.BisqApp; import bisq.apitest.linux.BitcoinDaemon; +import bisq.apitest.linux.LinuxProcess; @Slf4j public class Scaffold { @@ -152,17 +154,30 @@ public void tearDown() { SetupTask[] orderedTasks = new SetupTask[]{ bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask}; + final Optional[] firstShutdownException = new Optional[]{Optional.empty()}; stream(orderedTasks).filter(t -> t != null && t.getLinuxProcess() != null) .forEachOrdered(t -> { try { - t.getLinuxProcess().shutdown(); + LinuxProcess p = t.getLinuxProcess(); + p.shutdown(); MILLISECONDS.sleep(1000); - } catch (IOException | InterruptedException ex) { - throw new IllegalStateException(ex); + if (p.hasShutdownExceptions()) { + // We log shutdown exceptions, but do not throw + // one from here until the rest of the background + // instances have been shut down. + p.logExceptions(p.getShutdownExceptions(), log); + firstShutdownException[0] = Optional.of(p.getShutdownExceptions().get(0)); + } + } catch (InterruptedException ignored) { } }); - log.info("Teardown complete"); + if (firstShutdownException[0].isPresent()) + throw new IllegalStateException("There were errors shutting down one or more background instances.", + firstShutdownException[0].get()); + else + log.info("Teardown complete"); + } catch (Exception ex) { throw new IllegalStateException(ex); } @@ -274,7 +289,14 @@ private void startBackgroundProcesses(ExecutorService executor, bitcoinDaemon.verifyBitcoinPathsExist(true); bitcoindTask = new SetupTask(bitcoinDaemon, countdownLatch); bitcoindTaskFuture = executor.submit(bitcoindTask); - MILLISECONDS.sleep(3500); // todo make configurable + MILLISECONDS.sleep(config.bisqAppInitTime); + + LinuxProcess bitcoindProcess = bitcoindTask.getLinuxProcess(); + if (bitcoindProcess.hasStartupExceptions()) { + bitcoindProcess.logExceptions(bitcoindProcess.getStartupExceptions(), log); + throw new IllegalStateException(bitcoindProcess.getStartupExceptions().get(0)); + } + bitcoinDaemon.verifyBitcoindRunning(); } @@ -326,8 +348,10 @@ private void startBisqApp(BisqAppConfig bisqAppConfig, } log.info("Giving {} ms for {} to initialize ...", config.bisqAppInitTime, bisqAppConfig.appName); MILLISECONDS.sleep(config.bisqAppInitTime); - if (bisqApp.hasStartupExceptions()) - throw bisqApp.startupIllegalStateException(log); + if (bisqApp.hasStartupExceptions()) { + bisqApp.logExceptions(bisqApp.getStartupExceptions(), log); + throw new IllegalStateException(bisqApp.getStartupExceptions().get(0)); + } } private BisqApp createBisqApp(BisqAppConfig bisqAppConfig) diff --git a/apitest/src/main/java/bisq/apitest/SetupTask.java b/apitest/src/main/java/bisq/apitest/SetupTask.java index 8818eee19ac..7e519b65cec 100644 --- a/apitest/src/main/java/bisq/apitest/SetupTask.java +++ b/apitest/src/main/java/bisq/apitest/SetupTask.java @@ -51,10 +51,6 @@ public Status call() throws Exception { } catch (InterruptedException ex) { throw new IllegalStateException(format("Error starting %s", linuxProcess.getName()), ex); } - - if (linuxProcess.hasStartupExceptions()) - throw linuxProcess.startupIllegalStateException(log); - Objects.requireNonNull(countdownLatch).countDown(); return new Status(linuxProcess.getName(), LocalDateTime.now()); } diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index bcaf72ece60..4063272c686 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -39,16 +39,18 @@ abstract class AbstractLinuxProcess implements LinuxProcess { protected final String name; + protected final ApiTestConfig config; protected long pid; - protected final ApiTestConfig config; protected final List startupExceptions; + protected final List shutdownExceptions; public AbstractLinuxProcess(String name, ApiTestConfig config) { this.name = name; this.config = config; this.startupExceptions = new ArrayList<>(); + this.shutdownExceptions = new ArrayList<>(); } @Override @@ -62,15 +64,30 @@ public boolean hasStartupExceptions() { } @Override - public IllegalStateException startupIllegalStateException(org.slf4j.Logger log) { + public boolean hasShutdownExceptions() { + return !shutdownExceptions.isEmpty(); + } + + @Override + public void logExceptions(List exceptions, org.slf4j.Logger log) { StringBuilder errorBuilder = new StringBuilder(); - for (Throwable t : startupExceptions) { + for (Throwable t : exceptions) { log.error("", t); errorBuilder.append(t.getMessage()).append("\n"); } - return new IllegalStateException(errorBuilder.toString().trim(), startupExceptions.get(0)); } + @Override + public List getStartupExceptions() { + return startupExceptions; + } + + @Override + public List getShutdownExceptions() { + return shutdownExceptions; + } + + @SuppressWarnings("unused") public void verifyBitcoinPathsExist() { verifyBitcoinPathsExist(false); diff --git a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java index a5d20a43a72..f449d0b98f1 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BisqApp.java +++ b/apitest/src/main/java/bisq/apitest/linux/BisqApp.java @@ -90,12 +90,16 @@ public long getPid() { public void shutdown() { try { log.info("Shutting down {} ...", bisqAppConfig.appName); - if (!isAlive(pid)) - throw new IllegalStateException(format("%s already shut down", bisqAppConfig.appName)); + if (!isAlive(pid)) { + this.shutdownExceptions.add(new IllegalStateException(format("%s already shut down", bisqAppConfig.appName))); + return; + } String killCmd = "kill -15 " + pid; - if (new BashCommand(killCmd).run().getExitStatus() != 0) - throw new IllegalStateException(format("Could not shut down %s", bisqAppConfig.appName)); + if (new BashCommand(killCmd).run().getExitStatus() != 0) { + this.shutdownExceptions.add(new IllegalStateException(format("Could not shut down %s", bisqAppConfig.appName))); + return; + } // Be lenient about the time it takes for a java app to shut down. for (int i = 0; i < 5; i++) { @@ -106,11 +110,13 @@ public void shutdown() { MILLISECONDS.sleep(2500); } - if (isAlive(pid)) - throw new IllegalStateException(format("%s shutdown did not work", bisqAppConfig.appName)); + if (isAlive(pid)) { + this.shutdownExceptions.add(new IllegalStateException(format("%s shutdown did not work", bisqAppConfig.appName))); + return; + } } catch (Exception e) { - throw new IllegalStateException(format("Error shutting down %s", bisqAppConfig.appName), e); + this.shutdownExceptions.add(new IllegalStateException(format("Error shutting down %s", bisqAppConfig.appName), e)); } } diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java index c752d7c8e9b..cdbba297704 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java @@ -83,23 +83,32 @@ public long getPid() { } @Override - public void shutdown() throws IOException, InterruptedException { + public void shutdown() { try { log.info("Shutting down bitcoind daemon..."); - if (!isAlive(pid)) - throw new IllegalStateException("bitcoind already shut down"); - if (new BashCommand("kill -15 " + pid).run().getExitStatus() != 0) - throw new IllegalStateException("Could not shut down bitcoind; probably already stopped."); + if (!isAlive(pid)) { + this.shutdownExceptions.add(new IllegalStateException("Bitcoind already shut down.")); + return; + } + + if (new BashCommand("kill -15 " + pid).run().getExitStatus() != 0) { + this.shutdownExceptions.add(new IllegalStateException("Could not shut down bitcoind; probably already stopped.")); + return; + } + + MILLISECONDS.sleep(2500); // allow it time to shutdown + + if (isAlive(pid)) { + this.shutdownExceptions.add(new IllegalStateException( + format("Could not kill bitcoind process with pid %d.", pid))); + return; + } - MILLISECONDS.sleep(2000); // allow it time to shutdown log.info("Stopped"); - } catch (Exception e) { - throw new IllegalStateException("Error shutting down bitcoind", e); - } finally { - if (isAlive(pid)) - //noinspection ThrowFromFinallyBlock - throw new IllegalStateException("bitcoind shutdown did not work"); + } catch (InterruptedException ignored) { + } catch (IOException e) { + this.shutdownExceptions.add(new IllegalStateException("Error shutting down bitcoind.", e)); } } } diff --git a/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java index 8c19be66f12..fff25c2976c 100644 --- a/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/LinuxProcess.java @@ -19,6 +19,8 @@ import java.io.IOException; +import java.util.List; + public interface LinuxProcess { void start() throws InterruptedException, IOException; @@ -28,7 +30,13 @@ public interface LinuxProcess { boolean hasStartupExceptions(); - IllegalStateException startupIllegalStateException(org.slf4j.Logger log); + boolean hasShutdownExceptions(); + + void logExceptions(List exceptions, org.slf4j.Logger log); + + List getStartupExceptions(); + + List getShutdownExceptions(); - void shutdown() throws IOException, InterruptedException; + void shutdown(); } From e2f00b74172d32710d9721a8360acf8eb8b8e0cc Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 27 Jul 2020 16:43:43 -0300 Subject: [PATCH 72/88] Remove extra whiteline --- .../src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java | 1 - 1 file changed, 1 deletion(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java index 4063272c686..4687477e956 100644 --- a/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java +++ b/apitest/src/main/java/bisq/apitest/linux/AbstractLinuxProcess.java @@ -87,7 +87,6 @@ public List getShutdownExceptions() { return shutdownExceptions; } - @SuppressWarnings("unused") public void verifyBitcoinPathsExist() { verifyBitcoinPathsExist(false); From 8bb7e12f317faa5b44e561cbd87f99fd91e1e820 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 28 Jul 2020 12:33:25 -0300 Subject: [PATCH 73/88] Clarify scaffold tear down error handling The Scaffold#tearDown() method was split into two methods. The original tearDown() now passes the background process/task array to a new shutDownAll() method. This new method loops through the tasks in a more readable way, plainly expressing the intent to log all shutdown exceptions for each process being shut down, but not throwing an exception while processes are being shut down. The new shutDownAll() method returns the first shutdown exception encountered, which in turn is passed up to the test case's @AfterAll method. --- .../src/main/java/bisq/apitest/Scaffold.java | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index 9319226211f..5f9e013c5cc 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -46,7 +46,6 @@ import static java.lang.System.exit; import static java.lang.System.out; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; -import static java.util.Arrays.stream; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; @@ -154,27 +153,12 @@ public void tearDown() { SetupTask[] orderedTasks = new SetupTask[]{ bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask}; - final Optional[] firstShutdownException = new Optional[]{Optional.empty()}; - stream(orderedTasks).filter(t -> t != null && t.getLinuxProcess() != null) - .forEachOrdered(t -> { - try { - LinuxProcess p = t.getLinuxProcess(); - p.shutdown(); - MILLISECONDS.sleep(1000); - if (p.hasShutdownExceptions()) { - // We log shutdown exceptions, but do not throw - // one from here until the rest of the background - // instances have been shut down. - p.logExceptions(p.getShutdownExceptions(), log); - firstShutdownException[0] = Optional.of(p.getShutdownExceptions().get(0)); - } - } catch (InterruptedException ignored) { - } - }); - - if (firstShutdownException[0].isPresent()) - throw new IllegalStateException("There were errors shutting down one or more background instances.", - firstShutdownException[0].get()); + Optional firstException = shutDownAll(orderedTasks); + + if (firstException.isPresent()) + throw new IllegalStateException( + "There were errors shutting down one or more background instances.", + firstException.get()); else log.info("Teardown complete"); @@ -184,6 +168,32 @@ public void tearDown() { } } + private Optional shutDownAll(SetupTask[] orderedTasks) { + Optional firstException = Optional.empty(); + for (SetupTask t : orderedTasks) { + if (t != null && t.getLinuxProcess() != null) { + try { + LinuxProcess p = t.getLinuxProcess(); + p.shutdown(); + MILLISECONDS.sleep(1000); + if (p.hasShutdownExceptions()) { + // We log shutdown exceptions, but do not throw any from here + // because all of the background instances must be shut down. + p.logExceptions(p.getShutdownExceptions(), log); + + // We cache only the 1st shutdown exception and move on to the + // next process to be shutdown. This cached exception will be the + // one thrown to the calling test case (the @AfterAll method). + if (!firstException.isPresent()) + firstException = Optional.of(p.getShutdownExceptions().get(0)); + } + } catch (InterruptedException ignored) { + } + } + } + return firstException; + } + public void installDaoSetupDirectories() { cleanDaoSetupDirectories(); From 685839d3481e0cb0624673865c90f4503e41ab4f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 12 Aug 2020 16:47:12 -0300 Subject: [PATCH 74/88] Add fallbackfee param to bitcoind start cmd This commit adds a -fallbackfee=0.0002 parameter to the start 'bitcoind' command to keep bitcoin-core v0.20.1 working as v0.19.1 does -- with the 'fallbackfee' enabled. Bitcoin v0.20.0 contains a fix for inconsistent behaviour related to this fallbackfee configuration. Prior to v0.20, fallbackfee was disabled (0) by default for the mainnet chain, but enabled (0.0002) for the testnet and regtest chains. A test case with bitcoin-core v0.20.1 was breaking on the bitcoin-cli 'sendtoaddress' command, which was returning an error message instead of a tx-id: error code: -4 error message: Fee estimation failed. Fallbackfee is disabled. \ Wait a few blocks or enable -fallbackfee. Bitcoin-core v0.20.0 release notes contain info about this change: https://bitcoin.org/en/release/v0.20.0#updated-rpcs-1 The Bitcoin-core PR is https://github.com/bitcoin/bitcoin/pull/16524 --- apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java | 1 + 1 file changed, 1 insertion(+) diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java index cdbba297704..24d694a22d8 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java @@ -55,6 +55,7 @@ public void start() throws InterruptedException, IOException { + " -txindex=1" + " -peerbloomfilters=1" + " -debug=net" + + " -fallbackfee=0.0002" + " -rpcuser=" + config.bitcoinRpcUser + " -rpcpassword=" + config.bitcoinRpcPassword + " -blocknotify=" + config.bitcoinDatadir + "/blocknotify"; From 176f0b2ad811462f6104520ca6563d75b3e9ea3c Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 13 Aug 2020 13:32:58 -0300 Subject: [PATCH 75/88] Fix BitcoinCli wrapper error handling This change checks the system call exit status of bitcoin-cli commands, and populates a new error message accessor if the system exist status != 0. --- .../java/bisq/apitest/linux/BitcoinCli.java | 19 ++++++++++++-- .../bisq/apitest/method/BitcoinCliHelper.java | 26 ++++++++++++------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java index 169dfd5da1a..0bf49aa3602 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java @@ -33,11 +33,13 @@ public class BitcoinCli extends AbstractLinuxProcess implements LinuxProcess { private String commandWithOptions; private String output; private boolean error; + private String errorMessage; public BitcoinCli(ApiTestConfig config, String command) { super("bitcoin-cli", config); this.command = command; this.error = false; + this.errorMessage = null; } public BitcoinCli run() throws IOException, InterruptedException { @@ -138,6 +140,10 @@ public boolean isError() { return error; } + public String getErrorMessage() { + return errorMessage; + } + @Override public void start() throws InterruptedException, IOException { verifyBitcoinPathsExist(false); @@ -146,8 +152,17 @@ public void start() throws InterruptedException, IOException { + " -rpcuser=" + config.bitcoinRpcUser + " -rpcpassword=" + config.bitcoinRpcPassword + " " + command; - output = new BashCommand(commandWithOptions).run().getOutput(); - error = output.startsWith("error"); + BashCommand bashCommand = new BashCommand(commandWithOptions).run(); + + error = bashCommand.getExitStatus() != 0; + if (error) { + errorMessage = bashCommand.getError(); + if (errorMessage == null || errorMessage.isEmpty()) + throw new IllegalStateException("bitcoin-cli returned an error without a message"); + + } else { + output = bashCommand.getOutput(); + } } @Override diff --git a/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java index 693da32966b..45edf415794 100644 --- a/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java +++ b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java @@ -20,7 +20,6 @@ import java.io.IOException; import static java.lang.String.format; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; @@ -40,9 +39,12 @@ public BitcoinCliHelper(ApiTestConfig config) { public String getNewBtcAddress() { try { - String newAddress = new BitcoinCli(config, "getnewaddress").run().getOutput(); - assertNotNull(newAddress); - return newAddress; + BitcoinCli newAddress = new BitcoinCli(config, "getnewaddress").run(); + + if (newAddress.isError()) + fail(format("Could generate new bitcoin address:%n%s", newAddress.getErrorMessage())); + + return newAddress.getOutput(); } catch (IOException | InterruptedException ex) { fail(ex); return null; @@ -53,9 +55,11 @@ public String[] generateToAddress(int blocks, String address) { try { String generateToAddressCmd = format("generatetoaddress %d \"%s\"", blocks, address); BitcoinCli generateToAddress = new BitcoinCli(config, generateToAddressCmd).run(); - String[] txids = generateToAddress.getOutputValueAsStringArray(); - assertNotNull(txids); - return txids; + + if (generateToAddress.isError()) + fail(format("Could not generate bitcoin block(s):%n%s", generateToAddress.getErrorMessage())); + + return generateToAddress.getOutputValueAsStringArray(); } catch (IOException | InterruptedException ex) { fail(ex); return null; @@ -75,9 +79,11 @@ public String sendToAddress(String address, String amount) { String sendToAddressCmd = format("sendtoaddress \"%s\" %s \"\" \"\" false", address, amount); BitcoinCli sendToAddress = new BitcoinCli(config, sendToAddressCmd).run(); - String txid = sendToAddress.getOutput(); - assertNotNull(txid); - return txid; + + if (sendToAddress.isError()) + fail(format("Could not send BTC to address:%n%s", sendToAddress.getErrorMessage())); + + return sendToAddress.getOutput(); } catch (IOException | InterruptedException ex) { fail(ex); return null; From 9637cc09437c9aef8c90d3eeea01b296f1f91ddc Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 13 Aug 2020 14:19:02 -0300 Subject: [PATCH 76/88] Fix test fail() msg --- apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java index 45edf415794..6e5b52080f3 100644 --- a/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java +++ b/apitest/src/test/java/bisq/apitest/method/BitcoinCliHelper.java @@ -42,7 +42,7 @@ public String getNewBtcAddress() { BitcoinCli newAddress = new BitcoinCli(config, "getnewaddress").run(); if (newAddress.isError()) - fail(format("Could generate new bitcoin address:%n%s", newAddress.getErrorMessage())); + fail(format("Could not generate new bitcoin address:%n%s", newAddress.getErrorMessage())); return newAddress.getOutput(); } catch (IOException | InterruptedException ex) { From 72ff4dca2b241a55be17a1c9c15946abb20818ca Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 15 Aug 2020 12:20:25 -0300 Subject: [PATCH 77/88] Use non-default regtest bitcoind -rpcport The default bitcoind / bitcoin-cli rpcport option has been changed from 18443 to 19443, to help avoid rpcport conflicts between apitest's bitcoind instances and other bitcoind and/or bitcion-qt instances which are probably using the bitcoin-core default (regtest) rpcport 18443. However, this commit cannot include other changes for avoiding bind address:port conflicts between apitest bitcoind instances and other regtest bitcoin-core instances because bitcoinj's bind port is hardcoded in RegTestParams.java as 18444. In order to avoid bitcoin-core regtest mode bind address conflicts, you must start or restart your bitcoind or bitcoin-qt instance with a non-default bind port argument, e.g. bitcoin-qt -regtest -port=20444 --- apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java | 4 ++-- apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java | 1 + apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index 22ae67fccee..027d046b265 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -173,9 +173,9 @@ public ApiTestConfig(String... args) { .ofType(String.class).defaultsTo("localhost"); ArgumentAcceptingOptionSpec bitcoinRpcPortOpt = - parser.accepts(BITCOIN_RPC_PORT, "Bitcoin Core rpc port") + parser.accepts(BITCOIN_RPC_PORT, "Bitcoin Core rpc port (non-default)") .withRequiredArg() - .ofType(Integer.class).defaultsTo(18443); + .ofType(Integer.class).defaultsTo(19443); ArgumentAcceptingOptionSpec bitcoinRpcUserOpt = parser.accepts(BITCOIN_RPC_USER, "Bitcoin rpc user") diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java index 0bf49aa3602..2367443b0c4 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinCli.java @@ -149,6 +149,7 @@ public void start() throws InterruptedException, IOException { verifyBitcoinPathsExist(false); verifyBitcoindRunning(); commandWithOptions = config.bitcoinPath + "/bitcoin-cli -regtest " + + " -rpcport=" + config.bitcoinRpcPort + " -rpcuser=" + config.bitcoinRpcUser + " -rpcpassword=" + config.bitcoinRpcPassword + " " + command; diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java index 24d694a22d8..e61f1317438 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java @@ -56,6 +56,7 @@ public void start() throws InterruptedException, IOException { + " -peerbloomfilters=1" + " -debug=net" + " -fallbackfee=0.0002" + + " -rpcport=" + config.bitcoinRpcPort + " -rpcuser=" + config.bitcoinRpcUser + " -rpcpassword=" + config.bitcoinRpcPassword + " -blocknotify=" + config.bitcoinDatadir + "/blocknotify"; From e88bdb9f8ac2cc003ccbd3374c865c6997a36244 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 15 Aug 2020 12:46:38 -0300 Subject: [PATCH 78/88] Add regtest-port-conflicts.md doc --- apitest/docs/regtest-port-conflicts.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 apitest/docs/regtest-port-conflicts.md diff --git a/apitest/docs/regtest-port-conflicts.md b/apitest/docs/regtest-port-conflicts.md new file mode 100644 index 00000000000..601871218e2 --- /dev/null +++ b/apitest/docs/regtest-port-conflicts.md @@ -0,0 +1,12 @@ +# Avoiding bitcoin-core regtest port conflicts + +Some developers may already be running a `bitcoind` or `bitcoin-qt` instance in regtest mode when they try to run API +test cases. If a `bitcoin-qt` instance is bound to the default regtest port 18443, `apitest` will not be able to start +its own bitcoind instances. + +Though it would be preferable for `apitest` to change the bind port for Bisq's `bitcoinj` module at runtime, this is +not currently possible because `bitcoinj` hardcodes the default regtest mode bind port in `RegTestParams`. + +To avoid the bind address:port conflict, pass a port option to your bitcoin-core instance: + + bitcoin-qt -regtest -port=20444 From 12d52e869cd2f88470a056f3fd4a07c2a737d33d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 15 Aug 2020 13:22:32 -0300 Subject: [PATCH 79/88] Fix port number typo --- apitest/docs/regtest-port-conflicts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apitest/docs/regtest-port-conflicts.md b/apitest/docs/regtest-port-conflicts.md index 601871218e2..7ec3e6bf45a 100644 --- a/apitest/docs/regtest-port-conflicts.md +++ b/apitest/docs/regtest-port-conflicts.md @@ -1,7 +1,7 @@ # Avoiding bitcoin-core regtest port conflicts Some developers may already be running a `bitcoind` or `bitcoin-qt` instance in regtest mode when they try to run API -test cases. If a `bitcoin-qt` instance is bound to the default regtest port 18443, `apitest` will not be able to start +test cases. If a `bitcoin-qt` instance is bound to the default regtest port 18444, `apitest` will not be able to start its own bitcoind instances. Though it would be preferable for `apitest` to change the bind port for Bisq's `bitcoinj` module at runtime, this is From fc541257aed194feb1831b9e4feed1cb75a69376 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 17 Aug 2020 15:57:54 -0300 Subject: [PATCH 80/88] Add build / run / test categories docs --- apitest/docs/README.md | 5 +++ apitest/docs/build-run.md | 61 +++++++++++++++++++++++++++++++++ apitest/docs/test-categories.md | 35 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 apitest/docs/README.md create mode 100644 apitest/docs/build-run.md create mode 100644 apitest/docs/test-categories.md diff --git a/apitest/docs/README.md b/apitest/docs/README.md new file mode 100644 index 00000000000..4430b6f84e3 --- /dev/null +++ b/apitest/docs/README.md @@ -0,0 +1,5 @@ +# Bisq apitest docs + + - [build-run.md](build-run.md): Build and run API tests at the command line and from Intellij + - [test-categories.md](test-categories.md): Learn about the Bisq testing process and how you can contribute. + - [regtest-port-conflicts.md](regtest-port-conflicts.md): (deprecated) Set up a complete Bisq DAO development environment diff --git a/apitest/docs/build-run.md b/apitest/docs/build-run.md new file mode 100644 index 00000000000..73bd0f96a54 --- /dev/null +++ b/apitest/docs/build-run.md @@ -0,0 +1,61 @@ +# Build and Run API Test Harness + +## Linux & OSX + +The API test harness uses the GNU Bourne-Again SHell `bash`, and is not supported on Windows. + +## Predefined DAO / Regtest Setup + +The API test harness depends on the contents of https://github.com/bisq-network/bisq/raw/master/docs/dao-setup.zip. +The files contained in dao-setup.zip include a bitcoin-core wallet, a regtest genesis tx and chain of 111 blocks, plus +data directories for Bob and Alice Bisq instances. Bob & Alice wallets are pre-configured with 10 BTC each, and the +equivalent of 2.5 BTC in BSQ distributed among Bob & Alice's BSQ wallets. + +See https://github.com/bisq-network/bisq/blob/master/docs/dao-setup.md for details. + +## Install DAO / Regtest Setup Files + +Bisq's gradle build file defines a task for downloading dao-setup.zip and extracting its contents to the +`apitest/src/main/resources` folder, and the test harness will install a fresh set of data files to the +`apitest/build/resources/main` folder during a test case's scaffold setup phase -- normally a static `@BeforeAll` method. + +The dao-setup files can be downloaded during a normal build: + + $ ./gradlew clean build :apitest:installDaoSetup + +Or by running a single task: + + $ ./gradlew :apitest:installDaoSetup + +The `:apitest:installDaoSetup` task does not need to be run again until after the next time you run the gradle `clean` task. + +## Run API Tests + +The API test harness supports narrow & broad functional and full end to end test cases requiring +long setup and teardown times -- for example, to start a bitcoind instance, seednode, arbnode, plus Bob & Alice +Bisq instances, then shut everything down in proper order. For this reason, API test cases do not run during a normal +gradle build. + +To run API test cases, pass system property`-DrunApiTests=true`. + +To run all existing test cases: + + $ ./gradlew :apitest:test -DrunApiTests=true + +To run all test cases in a package: + + $ ./gradlew :apitest:test --tests "bisq.apitest.method.*" -DrunApiTests=true + +To run a single test case: + + $ ./gradlew :apitest:test --tests "bisq.apitest.method.GetBalanceTest" -DrunApiTests=true + +## Gradle Test Reports + +To see detailed test results, logs, and full stack traces for test failures, open +`apitest/build/reports/tests/test/index.html` in a browser. + +## See also + + - [test-categories.md](test-categories.md) + diff --git a/apitest/docs/test-categories.md b/apitest/docs/test-categories.md new file mode 100644 index 00000000000..ba1c095dc04 --- /dev/null +++ b/apitest/docs/test-categories.md @@ -0,0 +1,35 @@ +# API Test Categories + +This guide describes the categorization of tests. + +## Method Tests + +A `method` test is the `apitest` analog of a unit test. It tests a single API method such as `getbalance`, but is not +considered a unit test because the code execution path traverses so many layers: from `gRPC` client -> `gRPC` server +side service -> one or more Bisq `core` services, and back to the client. + +Method tests have direct access to `gRPC` client stubs, and test asserts are made directly on `gRPC` return values -- +Java Objects. + +All `method` tests are part of the `bisq.apitest.method` package. + +## Scenario Tests + +A `scenario` test is a narrow or broad functional test case covering a simple use case such as funding a wallet to a +complex series of trades. Generally, a scenario test case requires multiple `gRPC` method calls. + +Scenario tests have direct access to `gRPC` client stubs, and test asserts are made directly on `gRPC` return values -- +Java Objects. + +All `scenario` tests are part of the `bisq.apitest.scenario` package. + +## End to End Tests + +An end to end (`e2e`) test can cover a narrow or broad use case, and all client calls go through the `CLI` shell script +`bisq-cli`. End to end tests do not have access to `gRPC` client stubs, and test asserts are made on what the end +user sees on the console -- what`gRPC CLI` prints to `STDOUT`. + +As test coverage grows, stable scenario test cases should be migrated to `e2e` test cases. + +All `e2e` tests are part of the `bisq.apitest.e2e` package. + From f85ae2bb4d1bc4605d13864967710daa4580909a Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 17 Aug 2020 16:11:31 -0300 Subject: [PATCH 81/88] Explain how to run test cases from Intellij --- apitest/docs/build-run.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apitest/docs/build-run.md b/apitest/docs/build-run.md index 73bd0f96a54..99cee6e328e 100644 --- a/apitest/docs/build-run.md +++ b/apitest/docs/build-run.md @@ -50,6 +50,13 @@ To run a single test case: $ ./gradlew :apitest:test --tests "bisq.apitest.method.GetBalanceTest" -DrunApiTests=true +To run test cases from an Intellij, add two JVM arguments to your JUnit launchers: + + -DrunApiTests=true -Dlogback.configurationFile=apitest/build/resources/main/logback.xml + +The `-Dlogback.configurationFile` property will prevent `logback` from printing warnings about multiple `logback.xml` +files it will find in Bisq jars `cli.jar`, `daemon.jar`, and `seednode.jar`. + ## Gradle Test Reports To see detailed test results, logs, and full stack traces for test failures, open From af7252ec47987da7b7cf3a5546ba5e217e8220b5 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 17 Aug 2020 16:14:37 -0300 Subject: [PATCH 82/88] Fix typo --- apitest/docs/build-run.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apitest/docs/build-run.md b/apitest/docs/build-run.md index 99cee6e328e..308fe02cf66 100644 --- a/apitest/docs/build-run.md +++ b/apitest/docs/build-run.md @@ -50,7 +50,7 @@ To run a single test case: $ ./gradlew :apitest:test --tests "bisq.apitest.method.GetBalanceTest" -DrunApiTests=true -To run test cases from an Intellij, add two JVM arguments to your JUnit launchers: +To run test cases from Intellij, add two JVM arguments to your JUnit launchers: -DrunApiTests=true -Dlogback.configurationFile=apitest/build/resources/main/logback.xml From 8b081ad5f257aa644309ffa1c6625935943c5892 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 17 Aug 2020 16:18:08 -0300 Subject: [PATCH 83/88] Update README --- apitest/docs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apitest/docs/README.md b/apitest/docs/README.md index 4430b6f84e3..2072899d468 100644 --- a/apitest/docs/README.md +++ b/apitest/docs/README.md @@ -1,5 +1,5 @@ # Bisq apitest docs - [build-run.md](build-run.md): Build and run API tests at the command line and from Intellij - - [test-categories.md](test-categories.md): Learn about the Bisq testing process and how you can contribute. - - [regtest-port-conflicts.md](regtest-port-conflicts.md): (deprecated) Set up a complete Bisq DAO development environment + - [test-categories.md](test-categories.md): Learn about method, scenario and end to end tests + - [regtest-port-conflicts.md](regtest-port-conflicts.md): Avoid port conflicts when running multiple bitcoin-core apps in regtest mode From c3abd4e533b9f673ed805efa9ac6e92a81e843f1 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 18 Aug 2020 10:38:26 -0300 Subject: [PATCH 84/88] Remove white lines --- apitest/src/test/java/bisq/apitest/method/MethodTest.java | 1 - .../src/test/java/bisq/apitest/method/WalletProtectionTest.java | 2 -- 2 files changed, 3 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index 37dea63cdb9..694aa6806e3 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -84,6 +84,5 @@ protected final String getUnusedBtcAddress() { .findFirst() .get() .getAddress(); - } } diff --git a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java index a00d5333c01..450fb58e010 100644 --- a/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java +++ b/apitest/src/test/java/bisq/apitest/method/WalletProtectionTest.java @@ -87,7 +87,6 @@ public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() { Throwable exception = assertThrows(StatusRuntimeException.class, () -> grpcStubs.walletsService.lockWallet(request)); assertEquals("UNKNOWN: wallet is already locked", exception.getMessage()); - } @Test @@ -129,7 +128,6 @@ public void testRemoveNewWalletPassword() { getBalance(); // should not throw 'wallet locked' exception } - @AfterAll public static void tearDown() { tearDownScaffold(); From fa11dab28be39625946313b6c9e5ea9a4cd3e1c3 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 19 Aug 2020 11:04:21 -0300 Subject: [PATCH 85/88] Add punctuation & re-phrase sentence in README A new commit was needed to force a codacy check after changes were made to codacy rules. --- apitest/docs/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apitest/docs/README.md b/apitest/docs/README.md index 2072899d468..625dadddc9f 100644 --- a/apitest/docs/README.md +++ b/apitest/docs/README.md @@ -1,5 +1,5 @@ # Bisq apitest docs - - [build-run.md](build-run.md): Build and run API tests at the command line and from Intellij - - [test-categories.md](test-categories.md): Learn about method, scenario and end to end tests - - [regtest-port-conflicts.md](regtest-port-conflicts.md): Avoid port conflicts when running multiple bitcoin-core apps in regtest mode + - [build-run.md](build-run.md): Build and run API tests at the command line and from Intellij. + - [test-categories.md](test-categories.md): How to categorize a test case as `method`, `scenario` or `e2e`. + - [regtest-port-conflicts.md](regtest-port-conflicts.md): Avoid port conflicts when running multiple bitcoin-core apps in regtest mode. From 2ba0ee9d5d6960a1e81984c3530dc42a8876b303 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 19 Aug 2020 11:14:33 -0300 Subject: [PATCH 86/88] Change access modifer This commit is for forcing a codacy check. The previous change to an .md doc did not force a codacy check. --- apitest/src/test/java/bisq/apitest/JUnitHelper.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/JUnitHelper.java b/apitest/src/test/java/bisq/apitest/JUnitHelper.java index 03f16ee6b11..8ea80ad0d5e 100644 --- a/apitest/src/test/java/bisq/apitest/JUnitHelper.java +++ b/apitest/src/test/java/bisq/apitest/JUnitHelper.java @@ -34,7 +34,11 @@ public void testFailure(Failure failure) { printTestResults(result); } - public static void printTestResults(Result result) { + public static boolean allTestsPassed() { + return allPass; + } + + private static void printTestResults(Result result) { log.info("Total tests: {}, Failed: {}, Ignored: {}", result.getRunCount(), result.getFailureCount(), @@ -51,8 +55,4 @@ public static void printTestResults(Result result) { f.getTrace()))); } } - - public static boolean allTestsPassed() { - return allPass; - } } From ba8b9ccf54c9e87bb00694e4ee667ca33c00258d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 19 Aug 2020 11:49:21 -0300 Subject: [PATCH 87/88] Put 'empty' comments inside ignored catch blocks Follow codacy rule against empty blocks. --- apitest/src/main/java/bisq/apitest/Scaffold.java | 1 + apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java | 1 + .../src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java | 1 + apitest/src/test/java/bisq/apitest/ApiTestCase.java | 1 + 4 files changed, 4 insertions(+) diff --git a/apitest/src/main/java/bisq/apitest/Scaffold.java b/apitest/src/main/java/bisq/apitest/Scaffold.java index 5f9e013c5cc..bf0e4c771dd 100644 --- a/apitest/src/main/java/bisq/apitest/Scaffold.java +++ b/apitest/src/main/java/bisq/apitest/Scaffold.java @@ -188,6 +188,7 @@ private Optional shutDownAll(SetupTask[] orderedTasks) { firstException = Optional.of(p.getShutdownExceptions().get(0)); } } catch (InterruptedException ignored) { + // empty } } } diff --git a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java index e61f1317438..cc2e4952005 100644 --- a/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java +++ b/apitest/src/main/java/bisq/apitest/linux/BitcoinDaemon.java @@ -109,6 +109,7 @@ public void shutdown() { log.info("Stopped"); } catch (InterruptedException ignored) { + // empty } catch (IOException e) { this.shutdownExceptions.add(new IllegalStateException("Error shutting down bitcoind.", e)); } diff --git a/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java index 88ae539b0d0..540b1361009 100644 --- a/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java +++ b/apitest/src/main/java/bisq/apitest/linux/ThreadedStreamHandler.java @@ -80,6 +80,7 @@ private void doSleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException ignored) { + // empty } } diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java index e2301346c43..286e7f8c206 100644 --- a/apitest/src/test/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -88,6 +88,7 @@ protected void sleep(long ms) { try { MILLISECONDS.sleep(ms); } catch (InterruptedException ignored) { + // empty } } } From 1de6239527dfcc2c84a9415161fa37ec3d705511 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 24 Aug 2020 11:03:26 -0300 Subject: [PATCH 88/88] Shorten line length < 120 chars --- .../src/main/java/bisq/apitest/config/ApiTestConfig.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java index 027d046b265..15193feae88 100644 --- a/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java +++ b/apitest/src/main/java/bisq/apitest/config/ApiTestConfig.java @@ -112,7 +112,6 @@ public class ApiTestConfig { public final String baseBuildResourcesDir; public final String baseSrcResourcesDir; - // The parser that will be used to parse both cmd line and config file options private final OptionParser parser = new OptionParser(); @@ -125,8 +124,10 @@ public ApiTestConfig(String... args) { this.rootProjectDir = isRunningTest ? Paths.get(userDir).getParent().toFile().getAbsolutePath() : Paths.get(userDir).toFile().getAbsolutePath(); - this.baseBuildResourcesDir = Paths.get(rootProjectDir, "apitest", "build", "resources", "main").toFile().getAbsolutePath(); - this.baseSrcResourcesDir = Paths.get(rootProjectDir, "apitest", "src", "main", "resources").toFile().getAbsolutePath(); + this.baseBuildResourcesDir = Paths.get(rootProjectDir, "apitest", "build", "resources", "main") + .toFile().getAbsolutePath(); + this.baseSrcResourcesDir = Paths.get(rootProjectDir, "apitest", "src", "main", "resources") + .toFile().getAbsolutePath(); this.defaultConfigFile = absoluteConfigFile(baseBuildResourcesDir, DEFAULT_CONFIG_FILE_NAME); this.bitcoinDatadir = Paths.get(baseBuildResourcesDir, "Bitcoin-regtest").toFile().getAbsolutePath();