UNPKG

@onflow/flow-js-testing

Version:

This package will expose a set of utility methods, to allow Cadence code testing with libraries like Jest

241 lines (210 loc) 7.27 kB
/* * Flow JS Testing * * Copyright 2020-2021 Dapper Labs, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {send, build, getBlock, decode, config} from "@onflow/fcl" import {Logger, LOGGER_LEVELS} from "./logger" import {getAvailablePorts, getFlowVersion} from "../utils" import {satisfies} from "semver" const {spawn} = require("child_process") const SUPPORTED_FLOW_CLI_VERSIONS = ">=2.0.0" const SUPPORTED_PRE_RELEASE_MATCHER = "cadence-v1.0.0-preview" const DEFAULT_HTTP_PORT = 8080 const DEFAULT_GRPC_PORT = 3569 const print = { log: console.log, service: console.log, info: console.log, error: console.error, warn: console.warn, } /** Class representing emulator */ export class Emulator { /** * Create an emulator. */ constructor() { this.initialized = false this.logging = false this.filters = [] this.logger = new Logger() this.execName = "flow" } /** * Set logging flag. * @param {boolean} logging - whether logs shall be printed */ setLogging(logging) { this.logging = logging } /** * Log message with a specific type. * @param {*} message - message to put into log output * @param {"log"|"error"} type - type of the message to output */ log(message, type = "log") { if (this.logging !== false) { print[type](message) } } /** * Start emulator. * @param {Object} options - Optional parameters to start emulator with * @param {string} [options.flags] - Extra flags to supply to emulator * @param {boolean} [options.logging] - Switch to enable/disable logging by default * @param {number} [options.grpcPort] - Hardcoded GRPC port * @param {number} [options.restPort] - Hardcoded REST/HTTP port * @param {number} [options.adminPort] - Hardcoded admin port * @param {number} [options.debuggerPort] - Hardcoded debug port * @param {string} [options.execName] - Name of executable for flow-cli * @returns Promise<*> */ async start(options = {}) { const {flags, logging = false, signatureCheck = false, execName} = options if (execName) this.execName = execName // Get version of CLI const flowVersion = await getFlowVersion(this.execName) const satisfiesVersion = satisfies( flowVersion.raw, SUPPORTED_FLOW_CLI_VERSIONS, { includePrerelease: true, } ) const satisfiesPreRelease = flowVersion.raw.includes( SUPPORTED_PRE_RELEASE_MATCHER ) if (!satisfiesVersion && !satisfiesPreRelease) { throw new Error( `Unsupported Flow CLI version: ${flowVersion.raw}. Supported versions: ${SUPPORTED_FLOW_CLI_VERSIONS} or pre-releases tagged with ${SUPPORTED_PRE_RELEASE_MATCHER}` ) } // populate emulator ports with available ports const ports = await getAvailablePorts(4) const [grpcPort, restPort, adminPort, debuggerPort] = ports // override ports if specified in options this.grpcPort = options.grpcPort || grpcPort this.restPort = options.restPort || restPort this.adminPort = options.adminPort || adminPort this.debuggerPort = options.debuggerPort || debuggerPort // Support deprecated start call using static port if (arguments.length > 1 || typeof arguments[0] === "number") { console.warn(`Calling emulator.start with the port argument is now deprecated in favour of dynamically selected ports and will be removed in future versions of flow-js-testing. Please refrain from supplying this argument, as using it may cause unintended consequences. More info: https://github.com/onflow/flow-js-testing/blob/master/TRANSITIONS.md#0001-deprecate-emulatorstart-port-argument`) ;[this.adminPort, options = {}] = arguments const offset = this.adminPort - DEFAULT_HTTP_PORT this.grpcPort = DEFAULT_GRPC_PORT + offset } // config access node config().put("accessNode.api", `http://localhost:${this.restPort}`) this.logging = logging this.process = spawn(this.execName, [ "emulator", "--verbose", `--log-format=JSON`, `--rest-port=${this.restPort}`, `--admin-port=${this.adminPort}`, `--port=${this.grpcPort}`, `--debugger-port=${this.debuggerPort}`, `--skip-version-check`, signatureCheck ? "" : "--skip-tx-validation", flags, ]) this.logger.setProcess(this.process) // Listen to logger to display logs if enabled this.logger.on("*", (level, msg) => { if (!this.filters.includes(level)) return this.log(`${level.toUpperCase()}: ${msg}`) if (msg.includes("Starting") && msg.includes(this.adminPort)) { this.log("EMULATOR IS UP! Listening for events!") } }) // Suppress logger warning while waiting for emulator await config().put("logger.level", 0) return new Promise((resolve, reject) => { const cleanup = success => { this.initialized = success this.logger.removeListener(LOGGER_LEVELS.ERROR, listener) clearInterval(internalId) if (success) resolve(true) else reject() } let internalId const checkLiveness = async function () { try { await send(build([getBlock(false)])).then(decode) // Enable logger after emulator has come online await config().put("logger.level", 2) cleanup(true) } catch (err) {} // eslint-disable-line no-unused-vars, no-empty } internalId = setInterval(checkLiveness, 100) const listener = msg => { this.log(`EMULATOR ERROR: ${msg}`, "error") cleanup(false) } this.logger.on(LOGGER_LEVELS.ERROR, listener) this.process.on("close", code => { if (this.filters.includes("service")) { this.log(`EMULATOR: process exited with code ${code}`) } cleanup(false) }) }) } /** * Clear all log filters. * @returns void **/ clearFilters() { this.filters = [] } /** * Remove specific type of log filter. * @param {(debug|info|warning)} type - type of message * @returns void **/ removeFilter(type) { this.filters = this.filters.filter(item => item !== type) } /** * Add log filter. * @param {(debug|info|warning)} type type - type of message * @returns void **/ addFilter(type) { if (!this.filters.includes(type)) { this.filters.push(type) } } /** * Stop emulator. * @returns Promise<*> */ async stop() { // eslint-disable-next-line no-undef return new Promise(resolve => { this.process.kill() setTimeout(() => { this.initialized = false resolve(false) }, 50) }) } } /** Singleton instance */ export default new Emulator()