Skip to content

Plugins

Vladislav Alekseev edited this page Mar 17, 2022 · 13 revisions

Emcee runs plugins on its workers. Plugins do not alter any logic of Emcee, but they can listen to the events and react to them accordingly.

There are many use cases for creating your plugins. Some examples are:

  • Integrate your tests with your test management system: for example, update test statuses in your TMS when test finishes.

  • Record videos of tests: e.g. you may develop a logic to purge video if test succeeds, and upload video to your internal storage if test fails, for further investigation. Use SimulatorVideoRecorder module for that.

Test Working Directory

Tests can leave various artifacts in a place where plugin can pick them up later. Emcee creates a special folder and passes it into tests using EMCEE_TESTS_WORKING_DIRECTORY environment variable.

Plugin can use TestsWorkingDirectoryDeterminer class from TestsWorkingDirectorySupport module to determine that path, and then pick up the artifacts left by tests.

It is up to you to determine the protocol between tests and the plugin. As an example, tests can create their reports and store them as JSON files, and plugin can then read these files and operate on them as needed. Tests and plugin can share underlying models to be in sync.

Implementing plugin

It is convenient to develop Emcee plugins as Swift packages.

Plugin module

This is a base module for implementing Emcee plugin. In your Package.swift, import the library that has all required APIs to implement a plugin:

dependencies: [
    // NOTE: it is better to use a fixed version instead of master branch
    .package(url: "https://github.com/avito-tech/Emcee", .branch("master")),
]

In your plugin target add EmceePlugin dependency:

targets: [
    .target(
        name: "TestPlugin",
        dependencies: [
            "EmceePlugin"
        ]
    )
]

Creating your listener

Plugin will listen to Emcee event bus via EventStream instance. Emcee provides DefaultBusListener open class for your convenience (available is EventBus module). Subclass it and override its methods to receive corresponding events.

Example implementation of capturing commonly used events related to test execution flow.

import EmceeLogging
import EventBus
import Models
import Plugin

class MyPluginListener: DefaultBusListener {
    private let logger: ContextualLogger
    init(logger: ContextualLogger) {
        // You can use emcee logging system
        self.logger = logger
        super.init()
    }

    override func runnerEvent(_ event: RunnerEvent) {
        logger.trace("runnerEvent \(runnerEvent)")

        switch event {
        case .willRun(let testEntries, let testContext):
            // Will be called when Emcee is preparing to run a set of tests 
            // from a single test bundle (bucket).
            break
        case .testStarted(let testEntry, let testContext):
            // will be called when a single test has started
            break
        case .testFinished(let testEntry, let succeeded, let testContext):
            // will be called when a previously started test has finished
            break
        case .didRun(let testEntryResults, let testContext):
            // Will be called when Emcee finish running a set of tests 
            // and already has test results.
            // This is a good place to process runner output (e.g. result bundle) if you need to.
            break
        }
    }

    override func tearDown() {
        // will be called before Emcee will terminate your plugin
        // close your pipes here, free up resources, store data
        logger.trace("Plugin tear down")
    }
}

Plugin can take advantage from TestContext object passed into most events - it describes the test environment.

Wiring up the plugin

Since any plugin is a regular executable, you will need to provide main.swift file - your executable entry point. It may contain the following code:

import EventBus
import Plugin

// Create an event bus that will get the events from the Emcee
let eventBus = EventBus()

// Create a plugin
let plugin = try Plugin(eventBus: eventBus)

// You can use Emcee logging by asking a logger from your new Plugin instance
// Messages will be logger to ~/Library/Logs/ru.avito.emcee.logs/.
// Note: calls to `print()` or other logging systems aren't not captured.
let logger = plugin.logger
logger.info("Started plugin")

// Subscribe to the event bus by providing your instance of EventStream
eventBus.add(
    stream: MyPluginListener(
        logger: logger
    )
)

plugin.streamPluginEvents()

// Wait for plugin to finish
plugin.join()

Preparing plugin bundle

First, build your plugin using swift build command.

Emcee expects all plugins to have the following bundle structure:

YourPlugin.emceeplugin/
                       Plugin   <-- your plugin executable

You can use some basic Bash skills to prepare bundle and ZIP it:

#!/bin/bash

set -e

pluginName="YourPlugin.emceeplugin"
pluginPath=".build/debug/$pluginName/"
rm -rf "$pluginPath"
mkdir -p "$pluginPath"
cp .build/debug/Plugin "$pluginPath"

cd "$pluginPath/../"
rm -rf "$pluginName".zip
zip -r "$pluginName".zip "$pluginName"

Using your plugin

You should upload your YourPlugin.zip to your web server. Then you can pass URL to this file in test arg file.