From 7fbe28e640d81f60b815ba123e30e97bc0747220 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 23 Aug 2023 16:04:16 +0200 Subject: harness: reusable test env --- packages/taler-harness/src/harness/harness.ts | 68 ++++++++++--- packages/taler-harness/src/harness/helpers.ts | 107 ++++++++++++++++----- .../src/integrationtests/testrunner.ts | 4 +- packages/taler-util/src/logging.ts | 4 +- 4 files changed, 146 insertions(+), 37 deletions(-) diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 4e2bae8f2..3a12024dc 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -489,6 +489,7 @@ export interface BankConfig { database: string; allowRegistrations: boolean; maxDebt?: string; + overrideTestDir?: string; } export interface FakeBankConfig { @@ -534,6 +535,14 @@ function setCoin(config: Configuration, c: CoinConfig) { } } +function backoffStart(): number { + return 10; +} + +function backoffIncrement(n: number): number { + return Math.max(n * 2, 1000); +} + /** * Send an HTTP request until it succeeds or the process dies. */ @@ -545,6 +554,7 @@ export async function pingProc( if (!proc || proc.proc.exitCode !== null) { throw Error(`service process ${serviceName} not started, can't ping`); } + let nextDelay = backoffStart(); while (true) { try { logger.trace(`pinging ${serviceName} at ${url}`); @@ -554,7 +564,8 @@ export async function pingProc( } catch (e: any) { logger.warn(`service ${serviceName} not ready:`, e.toString()); //console.log(e); - await delayMs(1000); + await delayMs(nextDelay); + nextDelay = backoffIncrement(nextDelay); } if (!proc || proc.proc.exitCode != null || proc.proc.signalCode != null) { throw Error(`service process ${serviceName} stopped unexpectedly`); @@ -875,7 +886,7 @@ export class FakebankService /** * Create a new fakebank service handle. - * + * * First generates the configuration for the fakebank and * then creates a fakebank handle, but doesn't start the fakebank * service yet. @@ -885,19 +896,38 @@ export class FakebankService bc: BankConfig, ): Promise { const config = new Configuration(); - setTalerPaths(config, gc.testDir + "/talerhome"); + const testDir = bc.overrideTestDir ?? gc.testDir; + setTalerPaths(config, testDir + "/talerhome"); config.setString("taler", "currency", bc.currency); config.setString("bank", "http_port", `${bc.httpPort}`); config.setString("bank", "serve", "http"); config.setString("bank", "max_debt_bank", `${bc.currency}:999999`); config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`); config.setString("bank", "ram_limit", `${1024}`); - const cfgFilename = gc.testDir + "/bank.conf"; + const cfgFilename = testDir + "/bank.conf"; config.write(cfgFilename); return new FakebankService(gc, bc, cfgFilename); } + static fromExistingConfig( + gc: GlobalTestState, + opts: { overridePath?: string }, + ): FakebankService { + const testDir = opts.overridePath ?? gc.testDir; + const cfgFilename = testDir + `/bank.conf`; + const config = Configuration.load(cfgFilename); + const bc: BankConfig = { + allowRegistrations: + config.getYesNo("bank", "allow_registrations").orUndefined() ?? true, + currency: config.getString("taler", "currency").required(), + database: "none", + httpPort: config.getNumber("bank", "http_port").required(), + maxDebt: config.getString("bank", "max_debt").required(), + }; + return new FakebankService(gc, bc, cfgFilename); + } + setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) { if (!!this.proc) { throw Error("Can't set suggested exchange while bank is running."); @@ -981,6 +1011,7 @@ export interface ExchangeConfig { roundUnit?: string; httpPort: number; database: string; + overrideTestDir?: string; } export interface ExchangeServiceInterface { @@ -991,8 +1022,13 @@ export interface ExchangeServiceInterface { } export class ExchangeService implements ExchangeServiceInterface { - static fromExistingConfig(gc: GlobalTestState, exchangeName: string) { - const cfgFilename = gc.testDir + `/exchange-${exchangeName}.conf`; + static fromExistingConfig( + gc: GlobalTestState, + exchangeName: string, + opts: { overridePath?: string }, + ) { + const testDir = opts.overridePath ?? gc.testDir; + const cfgFilename = testDir + `/exchange-${exchangeName}.conf`; const config = Configuration.load(cfgFilename); const ec: ExchangeConfig = { currency: config.getString("taler", "currency").required(), @@ -1103,7 +1139,9 @@ export class ExchangeService implements ExchangeServiceInterface { } static create(gc: GlobalTestState, e: ExchangeConfig) { + const testDir = e.overrideTestDir ?? gc.testDir; const config = new Configuration(); + setTalerPaths(config, testDir + "/talerhome"); config.setString("taler", "currency", e.currency); // Required by the exchange but not really used yet. config.setString("exchange", "aml_threshold", `${e.currency}:1000000`); @@ -1112,7 +1150,6 @@ export class ExchangeService implements ExchangeServiceInterface { "currency_round_unit", e.roundUnit ?? `${e.currency}:0.01`, ); - setTalerPaths(config, gc.testDir + "/talerhome"); config.setString( "exchange", "revocation_dir", @@ -1149,7 +1186,7 @@ export class ExchangeService implements ExchangeServiceInterface { fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv)); - const cfgFilename = gc.testDir + `/exchange-${e.name}.conf`; + const cfgFilename = testDir + `/exchange-${e.name}.conf`; config.write(cfgFilename); return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey); } @@ -1553,6 +1590,7 @@ export interface MerchantConfig { currency: string; httpPort: number; database: string; + overrideTestDir?: string; } export interface PrivateOrderStatusQuery { @@ -1798,8 +1836,13 @@ export interface CreateMerchantTippingReserveRequest { } export class MerchantService implements MerchantServiceInterface { - static fromExistingConfig(gc: GlobalTestState, name: string) { - const cfgFilename = gc.testDir + `/merchant-${name}.conf`; + static fromExistingConfig( + gc: GlobalTestState, + name: string, + opts: { overridePath?: string }, + ) { + const testDir = opts.overridePath ?? gc.testDir; + const cfgFilename = testDir + `/merchant-${name}.conf`; const config = Configuration.load(cfgFilename); const mc: MerchantConfig = { currency: config.getString("taler", "currency").required(), @@ -1894,11 +1937,12 @@ export class MerchantService implements MerchantServiceInterface { gc: GlobalTestState, mc: MerchantConfig, ): Promise { + const testDir = mc.overrideTestDir ?? gc.testDir; const config = new Configuration(); config.setString("taler", "currency", mc.currency); - const cfgFilename = gc.testDir + `/merchant-${mc.name}.conf`; - setTalerPaths(config, gc.testDir + "/talerhome"); + const cfgFilename = testDir + `/merchant-${mc.name}.conf`; + setTalerPaths(config, testDir + "/talerhome"); config.setString("merchant", "serve", "tcp"); config.setString("merchant", "port", `${mc.httpPort}`); config.setString( diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts index 9ad46e587..9004d4419 100644 --- a/packages/taler-harness/src/harness/helpers.ts +++ b/packages/taler-harness/src/harness/helpers.ts @@ -32,6 +32,7 @@ import { NotificationType, WalletNotification, TransactionMajorState, + Logger, } from "@gnu-taler/taler-util"; import { BankAccessApi, @@ -49,6 +50,7 @@ import { DbInfo, ExchangeService, ExchangeServiceInterface, + FakebankService, getPayto, GlobalTestState, MerchantPrivateApi, @@ -62,6 +64,10 @@ import { WithAuthorization, } from "./harness.js"; +import * as fs from "fs"; + +const logger = new Logger("helpers.ts"); + /** * @deprecated */ @@ -212,48 +218,103 @@ export async function createSimpleTestkudosEnvironment( export async function useSharedTestkudosEnvironment(t: GlobalTestState) { const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); + // FIXME: We should probably have some file to indicate that + // the previous env setup finished successfully. + + const sharedDir = `/tmp/taler-harness@${process.env.USER}`; + + fs.mkdirSync(sharedDir, { recursive: true }); + const db = await setupSharedDb(t); - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); + let bank: FakebankService; - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); + const prevSetupDone = fs.existsSync(sharedDir + "/setup-done"); - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); + logger.info(`previous setup done: ${prevSetupDone}`); + + if (fs.existsSync(sharedDir + "/bank.conf")) { + logger.info("reusing existing bank"); + bank = BankService.fromExistingConfig(t, { + overridePath: sharedDir, + }); + } else { + logger.info("creating new bank config"); + bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + overrideTestDir: sharedDir, + }); + } + + logger.info("setting up exchange"); + + const exchangeName = "testexchange-1"; + const exchangeConfigFilename = sharedDir + `/exchange-${exchangeName}}`; + + let exchange: ExchangeService; + + if (fs.existsSync(exchangeConfigFilename)) { + exchange = ExchangeService.fromExistingConfig(t, exchangeName, { + overridePath: sharedDir, + }); + } else { + exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + overrideTestDir: sharedDir, + }); + } + + logger.info("setting up exchange"); + + let merchant: MerchantService; + const merchantName = "testmerchant-1"; + const merchantConfigFilename = sharedDir + `/merchant-${merchantName}}`; + + if (fs.existsSync(merchantConfigFilename)) { + merchant = MerchantService.fromExistingConfig(t, merchantName, { + overridePath: sharedDir, + }); + } else { + merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + overrideTestDir: sharedDir, + }); + } + + logger.info("creating bank account for exchange"); const exchangeBankAccount = await bank.createExchangeAccount( "myexchange", "x", ); + + logger.info("creating exchange bank account"); await exchange.addBankAccount("1", exchangeBankAccount); bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + exchange.addCoinConfigList(coinConfig); + + merchant.addExchange(exchange); + + logger.info("basic setup done, starting services"); + await bank.start(); await bank.pingUntilAvailable(); - exchange.addCoinConfigList(coinConfig); - await exchange.start(); await exchange.pingUntilAvailable(); - merchant.addExchange(exchange); - await merchant.start(); await merchant.pingUntilAvailable(); @@ -282,6 +343,8 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) { console.log("setup done!"); + fs.writeFileSync(sharedDir + "/setup-done", "OK"); + return { commonDb: db, exchange, diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index a9b6d1304..c1b06f21e 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see */ -import { CancellationToken, Logger, minimatch } from "@gnu-taler/taler-util"; +import { CancellationToken, Logger, minimatch, setGlobalLogLevelFromString } from "@gnu-taler/taler-util"; import * as child_process from "child_process"; import * as fs from "fs"; import * as os from "os"; @@ -494,6 +494,8 @@ if (runTestInstrStr && process.argv.includes("__TWCLI_TESTWORKER")) { runTestInstrStr, ) as RunTestChildInstruction; + setGlobalLogLevelFromString("TRACE"); + process.on("disconnect", () => { logger.trace("got disconnect from parent"); process.exit(3); diff --git a/packages/taler-util/src/logging.ts b/packages/taler-util/src/logging.ts index c4b2a3da0..b14274560 100644 --- a/packages/taler-util/src/logging.ts +++ b/packages/taler-util/src/logging.ts @@ -32,13 +32,13 @@ export enum LogLevel { None = "none", } -export let globalLogLevel = LogLevel.Info; +let globalLogLevel = LogLevel.Info; +const byTagLogLevel: Record = {}; export function setGlobalLogLevelFromString(logLevelStr: string): void { globalLogLevel = getLevelForString(logLevelStr); } -export const byTagLogLevel: Record = {}; export function setLogLevelFromString(tag: string, logLevelStr: string): void { byTagLogLevel[tag] = getLevelForString(logLevelStr); } -- cgit v1.2.3