harness: reusable test env

This commit is contained in:
Florian Dold 2023-08-23 16:04:16 +02:00
parent ef5962cd3c
commit 7fbe28e640
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
4 changed files with 146 additions and 37 deletions

View File

@ -489,6 +489,7 @@ export interface BankConfig {
database: string; database: string;
allowRegistrations: boolean; allowRegistrations: boolean;
maxDebt?: string; maxDebt?: string;
overrideTestDir?: string;
} }
export interface FakeBankConfig { 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. * 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) { if (!proc || proc.proc.exitCode !== null) {
throw Error(`service process ${serviceName} not started, can't ping`); throw Error(`service process ${serviceName} not started, can't ping`);
} }
let nextDelay = backoffStart();
while (true) { while (true) {
try { try {
logger.trace(`pinging ${serviceName} at ${url}`); logger.trace(`pinging ${serviceName} at ${url}`);
@ -554,7 +564,8 @@ export async function pingProc(
} catch (e: any) { } catch (e: any) {
logger.warn(`service ${serviceName} not ready:`, e.toString()); logger.warn(`service ${serviceName} not ready:`, e.toString());
//console.log(e); //console.log(e);
await delayMs(1000); await delayMs(nextDelay);
nextDelay = backoffIncrement(nextDelay);
} }
if (!proc || proc.proc.exitCode != null || proc.proc.signalCode != null) { if (!proc || proc.proc.exitCode != null || proc.proc.signalCode != null) {
throw Error(`service process ${serviceName} stopped unexpectedly`); throw Error(`service process ${serviceName} stopped unexpectedly`);
@ -885,19 +896,38 @@ export class FakebankService
bc: BankConfig, bc: BankConfig,
): Promise<FakebankService> { ): Promise<FakebankService> {
const config = new Configuration(); 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("taler", "currency", bc.currency);
config.setString("bank", "http_port", `${bc.httpPort}`); config.setString("bank", "http_port", `${bc.httpPort}`);
config.setString("bank", "serve", "http"); config.setString("bank", "serve", "http");
config.setString("bank", "max_debt_bank", `${bc.currency}:999999`); config.setString("bank", "max_debt_bank", `${bc.currency}:999999`);
config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`); config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`);
config.setString("bank", "ram_limit", `${1024}`); config.setString("bank", "ram_limit", `${1024}`);
const cfgFilename = gc.testDir + "/bank.conf"; const cfgFilename = testDir + "/bank.conf";
config.write(cfgFilename); config.write(cfgFilename);
return new FakebankService(gc, bc, 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) { setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) {
if (!!this.proc) { if (!!this.proc) {
throw Error("Can't set suggested exchange while bank is running."); throw Error("Can't set suggested exchange while bank is running.");
@ -981,6 +1011,7 @@ export interface ExchangeConfig {
roundUnit?: string; roundUnit?: string;
httpPort: number; httpPort: number;
database: string; database: string;
overrideTestDir?: string;
} }
export interface ExchangeServiceInterface { export interface ExchangeServiceInterface {
@ -991,8 +1022,13 @@ export interface ExchangeServiceInterface {
} }
export class ExchangeService implements ExchangeServiceInterface { export class ExchangeService implements ExchangeServiceInterface {
static fromExistingConfig(gc: GlobalTestState, exchangeName: string) { static fromExistingConfig(
const cfgFilename = gc.testDir + `/exchange-${exchangeName}.conf`; 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 config = Configuration.load(cfgFilename);
const ec: ExchangeConfig = { const ec: ExchangeConfig = {
currency: config.getString("taler", "currency").required(), currency: config.getString("taler", "currency").required(),
@ -1103,7 +1139,9 @@ export class ExchangeService implements ExchangeServiceInterface {
} }
static create(gc: GlobalTestState, e: ExchangeConfig) { static create(gc: GlobalTestState, e: ExchangeConfig) {
const testDir = e.overrideTestDir ?? gc.testDir;
const config = new Configuration(); const config = new Configuration();
setTalerPaths(config, testDir + "/talerhome");
config.setString("taler", "currency", e.currency); config.setString("taler", "currency", e.currency);
// Required by the exchange but not really used yet. // Required by the exchange but not really used yet.
config.setString("exchange", "aml_threshold", `${e.currency}:1000000`); config.setString("exchange", "aml_threshold", `${e.currency}:1000000`);
@ -1112,7 +1150,6 @@ export class ExchangeService implements ExchangeServiceInterface {
"currency_round_unit", "currency_round_unit",
e.roundUnit ?? `${e.currency}:0.01`, e.roundUnit ?? `${e.currency}:0.01`,
); );
setTalerPaths(config, gc.testDir + "/talerhome");
config.setString( config.setString(
"exchange", "exchange",
"revocation_dir", "revocation_dir",
@ -1149,7 +1186,7 @@ export class ExchangeService implements ExchangeServiceInterface {
fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv)); 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); config.write(cfgFilename);
return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey); return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey);
} }
@ -1553,6 +1590,7 @@ export interface MerchantConfig {
currency: string; currency: string;
httpPort: number; httpPort: number;
database: string; database: string;
overrideTestDir?: string;
} }
export interface PrivateOrderStatusQuery { export interface PrivateOrderStatusQuery {
@ -1798,8 +1836,13 @@ export interface CreateMerchantTippingReserveRequest {
} }
export class MerchantService implements MerchantServiceInterface { export class MerchantService implements MerchantServiceInterface {
static fromExistingConfig(gc: GlobalTestState, name: string) { static fromExistingConfig(
const cfgFilename = gc.testDir + `/merchant-${name}.conf`; 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 config = Configuration.load(cfgFilename);
const mc: MerchantConfig = { const mc: MerchantConfig = {
currency: config.getString("taler", "currency").required(), currency: config.getString("taler", "currency").required(),
@ -1894,11 +1937,12 @@ export class MerchantService implements MerchantServiceInterface {
gc: GlobalTestState, gc: GlobalTestState,
mc: MerchantConfig, mc: MerchantConfig,
): Promise<MerchantService> { ): Promise<MerchantService> {
const testDir = mc.overrideTestDir ?? gc.testDir;
const config = new Configuration(); const config = new Configuration();
config.setString("taler", "currency", mc.currency); config.setString("taler", "currency", mc.currency);
const cfgFilename = gc.testDir + `/merchant-${mc.name}.conf`; const cfgFilename = testDir + `/merchant-${mc.name}.conf`;
setTalerPaths(config, gc.testDir + "/talerhome"); setTalerPaths(config, testDir + "/talerhome");
config.setString("merchant", "serve", "tcp"); config.setString("merchant", "serve", "tcp");
config.setString("merchant", "port", `${mc.httpPort}`); config.setString("merchant", "port", `${mc.httpPort}`);
config.setString( config.setString(

View File

@ -32,6 +32,7 @@ import {
NotificationType, NotificationType,
WalletNotification, WalletNotification,
TransactionMajorState, TransactionMajorState,
Logger,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
BankAccessApi, BankAccessApi,
@ -49,6 +50,7 @@ import {
DbInfo, DbInfo,
ExchangeService, ExchangeService,
ExchangeServiceInterface, ExchangeServiceInterface,
FakebankService,
getPayto, getPayto,
GlobalTestState, GlobalTestState,
MerchantPrivateApi, MerchantPrivateApi,
@ -62,6 +64,10 @@ import {
WithAuthorization, WithAuthorization,
} from "./harness.js"; } from "./harness.js";
import * as fs from "fs";
const logger = new Logger("helpers.ts");
/** /**
* @deprecated * @deprecated
*/ */
@ -212,48 +218,103 @@ export async function createSimpleTestkudosEnvironment(
export async function useSharedTestkudosEnvironment(t: GlobalTestState) { export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); 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 db = await setupSharedDb(t);
const bank = await BankService.create(t, { let bank: FakebankService;
allowRegistrations: true,
currency: "TESTKUDOS",
database: db.connStr,
httpPort: 8082,
});
const exchange = ExchangeService.create(t, { const prevSetupDone = fs.existsSync(sharedDir + "/setup-done");
name: "testexchange-1",
currency: "TESTKUDOS",
httpPort: 8081,
database: db.connStr,
});
const merchant = await MerchantService.create(t, { logger.info(`previous setup done: ${prevSetupDone}`);
name: "testmerchant-1",
currency: "TESTKUDOS", if (fs.existsSync(sharedDir + "/bank.conf")) {
httpPort: 8083, logger.info("reusing existing bank");
database: db.connStr, 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( const exchangeBankAccount = await bank.createExchangeAccount(
"myexchange", "myexchange",
"x", "x",
); );
logger.info("creating exchange bank account");
await exchange.addBankAccount("1", exchangeBankAccount); await exchange.addBankAccount("1", exchangeBankAccount);
bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
exchange.addCoinConfigList(coinConfig);
merchant.addExchange(exchange);
logger.info("basic setup done, starting services");
await bank.start(); await bank.start();
await bank.pingUntilAvailable(); await bank.pingUntilAvailable();
exchange.addCoinConfigList(coinConfig);
await exchange.start(); await exchange.start();
await exchange.pingUntilAvailable(); await exchange.pingUntilAvailable();
merchant.addExchange(exchange);
await merchant.start(); await merchant.start();
await merchant.pingUntilAvailable(); await merchant.pingUntilAvailable();
@ -282,6 +343,8 @@ export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
console.log("setup done!"); console.log("setup done!");
fs.writeFileSync(sharedDir + "/setup-done", "OK");
return { return {
commonDb: db, commonDb: db,
exchange, exchange,

View File

@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
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 child_process from "child_process";
import * as fs from "fs"; import * as fs from "fs";
import * as os from "os"; import * as os from "os";
@ -494,6 +494,8 @@ if (runTestInstrStr && process.argv.includes("__TWCLI_TESTWORKER")) {
runTestInstrStr, runTestInstrStr,
) as RunTestChildInstruction; ) as RunTestChildInstruction;
setGlobalLogLevelFromString("TRACE");
process.on("disconnect", () => { process.on("disconnect", () => {
logger.trace("got disconnect from parent"); logger.trace("got disconnect from parent");
process.exit(3); process.exit(3);

View File

@ -32,13 +32,13 @@ export enum LogLevel {
None = "none", None = "none",
} }
export let globalLogLevel = LogLevel.Info; let globalLogLevel = LogLevel.Info;
const byTagLogLevel: Record<string, LogLevel> = {};
export function setGlobalLogLevelFromString(logLevelStr: string): void { export function setGlobalLogLevelFromString(logLevelStr: string): void {
globalLogLevel = getLevelForString(logLevelStr); globalLogLevel = getLevelForString(logLevelStr);
} }
export const byTagLogLevel: Record<string, LogLevel> = {};
export function setLogLevelFromString(tag: string, logLevelStr: string): void { export function setLogLevelFromString(tag: string, logLevelStr: string): void {
byTagLogLevel[tag] = getLevelForString(logLevelStr); byTagLogLevel[tag] = getLevelForString(logLevelStr);
} }