# Test Package Architecture * [Code Organization](#code-organization) * [Frontend](#frontend) * [Backend](#backend) * [Runner](#runner) * [Lifecycle of a Test Run](#lifecycle-of-a-test-run) * [Loading a Suite on the VM](#loading-a-suite-on-the-vm) * [Loading a Suite in the Browser](#loading-a-suite-in-the-browser) ## Code Organization From a user's perspective, the test package provides two main pieces of functionality: an API for defining tests, and a command-line tool to run those tests. The structure of the package reflects this division. The code is divided into three main sections: the frontend, the backend, and the runner. ### Frontend The [`lib/src/frontend`][frontend] directory contains APIs that are exposed to the user when they import `package:test/test.dart`. This includes core functions such as `expect()` and `expectAsync()`, test-specific matchers such as `throwsA()` and `prints()`, and annotation classes such as `TestOn` and `Timeout`. The functions that define the top-level structure of the test, such as `test()` and `group()`, are defined in `lib/test.dart`, but they can be thought of as frontend functions as well. [frontend]: https://github.com/dart-lang/test/tree/master/lib/src/frontend The frontend communicates with the backend using zone-scoped getters. [`Invoker.current`][Invoker] provides access to the current test case to matchers like [`completion()`][completion], for example to control when it completes. Structural functions use [`Declarer.current`][Declarer] to gradually build up an in-memory representation of a test suite. The runner is in charge of setting up these variables, but the frontend never communicates with the runner directly. [Invoker]: https://github.com/dart-lang/test/blob/master/lib/src/backend/invoker.dart [completion]: https://pub.dev/documentation/matcher/latest/expect/completion.html [Declarer]: https://github.com/dart-lang/test/blob/master/lib/src/backend/declarer.dart ### Backend The [`lib/src/backend`][backend] directory contains classes that represent the in-memory structure of a test suite. A [`Suite`][Suite] represents a single test file, and class contains a tree of [`Group`][Group]s, each of which contains many [`Test`][Test]s. These classes are built using a [`Declarer`][Declarer]. [backend]: https://github.com/dart-lang/test/tree/master/lib/src/backend [Suite]: https://github.com/dart-lang/test/blob/master/lib/src/backend/suite.dart [Group]: https://github.com/dart-lang/test/blob/master/lib/src/backend/group.dart [Test]: https://github.com/dart-lang/test/blob/master/lib/src/backend/test.dart The backend also contains the [`Invoker`][Invoker], which is responsible for actually running an individual test case—including tracking how many outstanding asynchronous callbacks are pending, handling exceptions, and timing out the test if it takes too long. The `Invoker` provides information about the status of a running test as streams and futures on a [`LiveTest`][LiveTest] object. [LiveTest]: https://github.com/dart-lang/test/blob/master/lib/src/backend/live_test.dart The backend provides a bridge between the frontend and the runner. The runner sets up the `Declarer` and starts the `Invoker`, which the frontend functions then communicate with directly. ### Runner The [`lib/src/runner`][runner] directory contains the code that's executed when `dart test` is invoked. It's in charge of locating test files, loading them, executing them, and communicating their results to the user. It's also by far the biggest section. For more information on the runner architecture, see [Lifecycle of a Test Run](#lifecycle-of-a-test-suite) below. [runner]: https://github.com/dart-lang/test/tree/master/lib/src/runner ## Lifecycle of a Test Run To understand generally how the test runner works, let's look at an example run. When the user first invokes `dart test`, the command-line arguments and [configuration files][] are combined into a single [`Configuration`][Configuration] object which is passed into the [`Runner`][Runner] class. The `Runner` is mostly just glue: it starts up the various components necessary for a test run, and connects them to one another. It's also in charge of handling certain `Configuration` flags. [configuration files]: https://github.com/dart-lang/test/blob/master/doc/configuration.md [Configuration]: https://github.com/dart-lang/test/tree/master/lib/src/runner/configuration.dart [Runner]: https://github.com/dart-lang/test/tree/master/lib/src/runner.dart The first thing the runner starts is the [`Engine`][Engine]. The engine iterates through a test suite's tests and invokes them in order. It knows how to handle set-up and tear-down functions, and how to combine the output of multiple test suites running concurrently. It exposes its progress through a collection of getters and streams that provide access to individual [`LiveTest`][LiveTest]s. [Engine]: https://github.com/dart-lang/test/tree/master/lib/src/runner/engine.dart The runner then passes the `Engine` to a [`Reporter`][Reporter], which listens to the `Engine`'s streams and exposes the information there to the user, usually by printing human-readable text. [`CompactReporter`][CompactReporter] is the default on Posix platforms, but others may be selected based on the `Configuration`. Nearly everything the user sees comes through the reporter. [Reporter]: https://github.com/dart-lang/test/tree/master/lib/src/runner/reporter.dart [CompactReporter]: https://github.com/dart-lang/test/tree/master/lib/src/runner/reporter/compact.dart The `Engine` and `Reporter` can't do much of anything, though, without any test suites to run. The next step is to load those suites. The [`Loader`][Loader] is in charge of this part. It takes in file or directory paths and finds all the test files they contain—by default any files matching `*_test.dart`. It then proceeds to load each file on all the platforms specified in the `Configuration` that's also supported by the test suite. [Loader]: https://github.com/dart-lang/test/tree/master/lib/src/runner/loader.dart The specifics of loading suites differs based on whether the platform is a browser or the Dart VM. I'll cover each platform below, but for now let's stick to what they have in common. Every platform will emit a [`LoadSuite`][LoadSuite], which is a synthetic [`Suite`][Suite] containing a single test that, when invoked, produces the actual `Suite` defined in the test file. [LoadSuite]: https://github.com/dart-lang/test/tree/master/lib/src/runner/load_suite.dart Wrapping the loading process in a synthetic `Suite` gives us the very useful invariant that *all test errors occur within a `Suite`*. Loading can fail in all sorts of ways—the code might not compile, the `main()` method might throw, the browser might not be installed, and so on. Locating those errors within a `Suite` means that the `Engine` and `Reporter`, which already know how to deal with test errors, can deal with load errors in exactly the same way. It makes the load process a little more complex, but it makes everything else a lot cleaner. Once a `Suite` has been loaded, the runner does a little post-processing to make sure the `Configuration` is handled properly. It filters out tests whose tags don't match the `--tags` flag, or whose names don't match the `--name` flag. Then it passes the resulting `Suite`s on to the `Engine` and they begin to run. ### Loading a Suite on the VM Let's start with looking at how suites are loaded on the Dart VM, since the process is substantially simpler than loading them on a browser. This loading is handled by the [`VMPlatform`][VMPlatform], which extends the [`PlatformPlugin`][PlatformPlugin] class. [Eventually][issue 49], we plan to support a user-accessible platform plugin API, so we model platforms as plugins to prepare for that. [VMPlatform]: https://github.com/dart-lang/test/tree/master/lib/src/runner/vm/platform.dart [PlatformPlugin]: https://github.com/dart-lang/test/tree/master/lib/src/runner/plugin/platform.dart [issue 49]: https://github.com/dart-lang/test/issues/49 In its simplest form, a `PlatformPlugin`'s responsibility is just to create a [`StreamChannel`][StreamChannel] that connects the test runner to a remote isolate—everything else is handled by helper functions. The `VMPlatform` uses [`Isolate`][Isolate]s to dynamically load its test suites, and then communicates with them using an [`IsolateChannel`][IsolateChannel]. It passes in a `data:` URI containing Dart code that imports the user's code, and runs that code in the context of the [`serializeSuite()`][remote platform helpers] helper, and the `PlatformPlugin` superclass deserializes it on the other side using [`deserializeSuite()`][platform helpers]. [StreamChannel]: https://pub.dev/packages/stream_channel [Isolate]: https://api.dart.dev/stable/dart-isolate/Isolate-class.html [IsolateChannel]: https://pub.dev/documentation/stream_channel/latest/stream_channel/IsolateChannel-class.html [remote platform helpers]: https://github.com/dart-lang/test/tree/master/lib/src/runner/plugin/remote_platform_helpers.dart [platform helpers]: https://github.com/dart-lang/test/tree/master/lib/src/runner/plugin/platform_helpers.dart When a test suite is serialized and deserialized, it's not just converted to and from some static representation like JSON. The [`Engine`][Engine] needs fine-grained control over the remote suite, and the [`Reporter`][Reporter] needs fine-grained access to the [`LiveTest`][LiveTest]s it emits. To make this work, the helper functions use the [`MultiChannel`][MultiChannel] class to tunnel streams for each test through the main `IsolateChannel`. Each test has its own virtual channel that gets a message when the test runner calls [`Test.load()`][Test], and that sends messages back to indicate the progress of the test. Information about these virtual channels, as well as test names and metadata, are bundled up into a JSON object and sent over the `IsolateChannel` to be deserialized. The deserialization process then converts them into [`RunnerTest`][RunnerTest]s within a [`RunnerSuite`][RunnerSuite], which the `Engine` can then run just like normal `Test`s in a normal [`Suite`][Suite]. [MultiChannel]: https://pub.dev/documentation/stream_channel/latest/stream_channel/MultiChannel-class.html [RunnerTest]: https://github.com/dart-lang/test/tree/master/lib/src/runner/runner_test.dart [RunnerSuite]: https://github.com/dart-lang/test/tree/master/lib/src/runner/runner_suite.dart ### Loading a Suite in the Browser The [`BrowserPlatform`][BrowserPlatform] class also extends [`PlatformPlugin`][PlatformPlugin], but rather than just emitting a [`StreamChannel`][StreamChannel] and letting the plugin helpers do the rest, it takes more control over the loading process. It emits its own [`RunnerSuite`][RunnerSuite], which allows it to expose its own [`Environment`][Environment] to enable debugging. [BrowserPlatform]: https://github.com/dart-lang/test/tree/master/lib/src/runner/browser/platform.dart [Environment]: https://github.com/dart-lang/test/tree/master/lib/src/runner/environment.dart Whereas the [`VMPlatform`][VMPlatform] loads each separate suite in isolation, the `BrowserPlatform` shares a substantial amount of resources between suites. All suites load their code from a single HTTP server, which is managed by the platform. This server provides access to compiled JavaScript for other browsers, and to HTML files that bootstrap the tests. In addition to sharing a server, when multiple suites are loaded for the same browser, they all share a tab within that browser. Each separate browser is controlled by its own [`BrowserManager`][BrowserManager], which uses `WebSocket`s to communicate with Dart code running in the main frame—also known as [the host][host]. [BrowserManager]: https://github.com/dart-lang/test/tree/master/lib/src/runner/browser/browser_manager.dart [host]: https://github.com/dart-lang/test/tree/master/lib/src/runner/browser/static/host.dart Each browser is spawned with a tab pointing to `packages/test/src/runner/browser/static/index.html`, the host page. The host's code then opens a `WebSocket` connection to a dynamically-generated URL. This URL tells the `BrowserPlatform` which `BrowserManager` to send the `WebSocket` to. To load a suite for this browser, the `BrowserPlatform` passes the URL for that suite's HTML file to the `BrowserManager`, which in turn sends it down to the host page. The host opens this HTML in an iframe, opens a [`StreamChannel`][StreamChannel] with this iframe using [`Window.postMessage()`][Window.postMessage]. It then tunnels this channel through the `WebSocket` connection, again using [`MultiChannel`][MultiChannel], so that the `BrowserManager` has a direct line to the iframe where the tests are defined. [Window.postMessage]: https://api.dart.dev/stable/dart-html/Window/postMessage.html From this point forward the process is similar to `VMPlatform`. The iframe serializes its test suite using [`serializeSuite()`][remote platform helpers], and the `BrowserManager` deserializes it using [`deserializeSuite()`][platform helpers]. It's then forwarded to the `Loader` via the `BrowserPlatform`.