@joezilla/8sim
v0.14.0
Published
Intel 8080 / Zilog Z80 CPU simulator with an S-100 bus, pluggable cards, and a machine builder.
Readme
8sim — S-100 Bus Machine Emulator (Intel 8080 / Zilog Z80)
An S-100 bus microcomputer emulator written in TypeScript. 8sim models the machine the way the hardware was actually built: a passive backplane (Bus) into which you plug cards — a CPU board, RAM and EPROM boards, serial and floppy-controller boards. Everything sits behind well-defined interfaces, so every card is independently testable and swappable, and even the processor is just another card — pluggable between the Intel 8080 and a full Zilog Z80.
Assemble a machine imperatively (attach memory and cards by hand) or declaratively (describe it as a MachineSpec and let buildMachine construct it, rejecting bus collisions at build time). Boot period software — stock Altair CP/M boots unmodified on either CPU.
Runs in Node.js, browsers, Deno, and Bun — zero runtime dependencies.
Features
- S-100 machine model: passive
Busbackplane, pluggable cards, 16-bit memory space + 8-bit I/O space + interrupt controller - Card catalog: real period cards modeled as bus devices — serial/parallel boards (MITS 88-2SIO, IMSAI SIO-2, IMSAI MIO, Processor Technology 3P+S) and four floppy/disk controllers (MITS 88-DCDD, IMSAI MDC-DIO, IMSAI FIF, Processor Technology Helios II), plus the component chips they're built from (8251, 6850, 8212, TR1602). Several boot real period operating systems — CP/M, IMDOS, and the SOLOS/CUTER monitors — unmodified.
- Pluggable CPU cards: complete Intel 8080 instruction set, and a full Zilog Z80 core (validated against ZEXDOC + ZEXALL) — same bus, same constructor
- Declarative machine assembly: build a whole machine from a
MachineSpec; overlapping memory and colliding I/O ports are rejected at build time - Card bundles: self-describing packages (config schema + factory) for CPU, RAM, EPROM, and serial/floppy cards
- Memory-mapped I/O, ROM write protection, and a snooping bus for tracing/debug
- Interrupt controller with RST vector dispatch; accurate flags; EI/DI one-instruction delay; HLT with interrupt wake
- Real-time (authentic 2 MHz) and immediate-mode clocks
- Browser-safe: no Node.js globals in the core library
Installation
npm install
npm test
npm run buildQuick Start
import { Cpu8080, Bus, Ram, Rom, InterruptController } from './src/index.js';
// 1. Create the interrupt controller and bus
const pic = new InterruptController();
const bus = new Bus(pic);
// 2. Attach memory
const rom = new Rom('rom', 0x0000, new Uint8Array([/* your program */]));
const ram = new Ram('ram', 0x2000, 0x4000);
bus.attachMemory(rom);
bus.attachMemory(ram);
// 3. Create and run the CPU
const cpu = new Cpu8080(bus, pic);
cpu.registers.pc = 0x0000;
cpu.registers.sp = 0x5fff;
// Step one instruction at a time
const cycles = cpu.step(); // returns T-states consumed
// Or run until HLT
cpu.run();Architecture
┌──────────────────────────────────────────────────┐
│ ICpu (Cpu8080 | CpuZ80) │
│ Registers, Flags, Decoder, step() / run() │
│ polls: IInterruptController │
│ accesses: IBus │
└────────────────────┬─────────────────────────────┘
│ IBus
┌────────────────────▼─────────────────────────────┐
│ Bus (SystemBus) │
│ memoryMap: BusRegion[] (sorted, 16-bit addr) │
│ ioSpace: IoSpace (8-bit port space) │
│ pic: IInterruptController │
└───────┬──────────────┬──────────────┬────────────┘
│ │ │
IMemory IIODevice IInterruptController
(RAM/ROM/ (port or (PIC / IRQ lines)
MMIO adapter) MMIO device)The CPU holds only a reference to IBus. All memory, I/O, and interrupt logic is behind that interface. This lets you:
- Unit-test the CPU with a mock bus
- Swap any peripheral without touching the CPU
- Run the same code in Node or a browser
Memory Space
Bus holds a sorted list of BusRegion entries. On each read/write, it finds the region containing the address and forwards the call as an offset into that region. Unmapped reads return 0xFF; unmapped writes are silently ignored. ROM regions reject writes without error.
I/O Space
IoSpace maps 8-bit port numbers to IIODevice instances. Unregistered ports return 0xFF on read and ignore writes.
Memory-Mapped I/O
MemoryMappedIOAdapter wraps any IIODevice as an IMemory region. Attach it to the bus at any base address and the device becomes accessible from both the memory and I/O address spaces simultaneously — no CPU or bus changes required.
const uart = new MyUart();
bus.attachIODevice(uart); // I/O port access
bus.attachMemory(new MemoryMappedIOAdapter(0xe000, 8, uart)); // memory accessBus Snooping
SnoopBus wraps any IBus and forwards every memory/I/O access to a list of IBusObservers before returning — a transparent tap for tracing, logging, or building a debugger, with no changes to the CPU or the cards.
const snoop = new SnoopBus(bus);
snoop.attach({ onMemWrite: (addr, val) => log(`W ${addr.toString(16)}=${val.toString(16)}`) });
const cpu = new Cpu8080(snoop, pic);Cards
A card is an IS100Card — a module with an attach(bus) method that wires itself onto the backplane (claiming I/O ports, memory regions, or IRQ lines). 8sim ships a catalog of real S-100 cards plus the component chips they're built from. Each is usable directly, or via its card bundle for declarative assembly.
| Card | Maker | What it is |
|------|-------|-----------|
| 88-2SIO (mits-88-2sio) | MITS | Dual serial board (2× 6850 ACIA). The assumed console for nearly all Altair software — attach this to boot Altair CP/M. |
| SIO-2 (imsai-sio2) | IMSAI | Dual 8251 serial card with a board-control port. |
| MIO (imsai-mio) | IMSAI | Multi-I/O board: a TR1602 UART serial port plus two 8212 parallel ports. |
| 3P+S (proctech-3ps) | Processor Technology | "3 Parallel + Serial" multifunction I/O board (4 consecutive ports) — a general-purpose UART plus two parallel ports, with the jumper-defined status-bit map, channel order, and A0-invert all configurable. Runs the CUTER monitor to a > prompt over serial. |
| 88-DCDD (mits-88-dcdd) | MITS | 8-inch floppy disk controller; byte-at-a-time programmed I/O over an FDC channel (e.g. an fdcplus-web server). |
| MDC-DIO (imsai-mdc-dio) | IMSAI | Firmware-driven floppy controller living in a memory window at E000–EFFF (standard 8″ + mini drives, IBM 3740 format). Backed by an in-memory .dsk or fdcplus-web. |
| FIF (imsai-fif) | IMSAI | Intelligent, DMA-capable floppy controller driven by a single command port (0xFD). Boots IMDOS 2.02 to an A> prompt. In-memory or fdcplus-web backend. |
| Helios II (pt-helios) | Processor Technology | Intelligent DMA 8″ disk controller for the SOL-20 (ports F0–F7), firm-sectored with variable-length blocks; driven by the SOLOS BOOTLOAD ROM. In-memory SVH image or fdcplus-web backend. |
The floppy/disk controllers accept a disk over the same fdcplus-web channel used by boot-cpm, or an in-process image for self-contained use. See Booting period software.
Component chips, usable standalone or as building blocks for a card:
| Chip | What it is |
|------|-----------|
| Intel 8251 (intel-8251) | USART — data + control port |
| Motorola 6850 (motorola-6850) | ACIA — status + data port |
| Intel 8212 (intel-8212) | 8-bit parallel I/O port |
| TR1602 (tr1602-uart) | UART — data + status port |
import { Bus, InterruptController, Mits2SioCard } from '@joezilla/8sim';
const bus = new Bus(new InterruptController());
const console = new Mits2SioCard('sio', { basePort: 0x10 }); // Altair console at 0x10/0x11
console.attach(bus); // claims ports 0x10–0x13CPU
Registers
| Register | Width | Notes |
|----------|-------|-------|
| A | 8-bit | Accumulator |
| B, C | 8-bit | BC pair |
| D, E | 8-bit | DE pair |
| H, L | 8-bit | HL pair; M pseudo-register = (HL) |
| SP | 16-bit | Stack pointer |
| PC | 16-bit | Program counter |
Registers exposes bc, de, hl as read/write pair accessors.
Flags
Stored as an 8080 PSW byte: S Z 0 AC 0 P 1 CY (bit 1 is always 1).
| Flag | Bit | Meaning | |------|-----|---------| | S | 7 | Sign (bit 7 of result) | | Z | 6 | Zero | | AC | 4 | Auxiliary carry (half-carry, used by DAA) | | P | 2 | Parity (even popcount) | | CY | 0 | Carry / borrow |
step() Execution Model
1. Halted? → if INTE + pending interrupt: wake and handle; else return idle cycles
2. pendingEI → inte = true (EI's one-instruction delay commits here)
3. INTE + pending? → handleInterrupt (pushes PC, jumps to RST vector)
4. Fetch opcode; HLT / EI / DI handled inline
5. Dispatch through 256-entry Decoder table → returns T-statesInterrupts
// Assert an IRQ line (0–7)
pic.assertIRQ(1);
// Enable interrupts in the CPU
cpu.inte = true;
// The CPU will take the interrupt at the start of the next step()
// after any pending EI delay resolves.InterruptController.acknowledge() returns 0xC7 | (line << 3) — the RST n opcode byte. The CPU jumps to rstByte & 0x38 (vector × 8):
| IRQ line | RST opcode | Vector | |----------|-----------|--------| | 0 | RST 0 | 0x0000 | | 1 | RST 1 | 0x0008 | | 2 | RST 2 | 0x0010 | | … | … | … | | 7 | RST 7 | 0x0038 |
EI/DI behaviour:
EIsetspendingEI=true. Interrupts become enabled at the start of the nextstep()call (before the next instruction is fetched), matching real 8080 behaviour where one instruction executes after EI before interrupts are accepted.DIclearsinteandpendingEIimmediately.
Z80 CPU
CpuZ80 (src/cpu/z80/) is a second, fully-implemented CPU core that runs on the
same bus, memory, I/O, and interrupt infrastructure. It covers the complete
documented and undocumented instruction set — CB/DD/ED/FD/DDCB/FDCB prefixes,
the IXH/IXL/IYH/IYL and SLL opcodes, the DDCB result-copy variants, and the
undocumented X/Y flags (with MEMPTR/WZ modeling). Correctness is validated against
the standard ZEXDOC and ZEXALL exercisers.
import { CpuZ80, Bus, Ram, InterruptController } from './src/index.js';
const pic = new InterruptController();
const bus = new Bus(pic);
bus.attachMemory(new Ram('ram', 0, 0x10000));
const cpu = new CpuZ80(bus, pic); // same (bus, pic) constructor as Cpu8080
cpu.step(); // returns T-statesBoth cores implement the lightweight ICpu interface (step/reset/run/halted/pc)
and share the (bus, pic) constructor, so MachineRunner drives either without
changes. examples/boot-cpm.ts selects the core via the CPU env var
(CPU=z80 npm run boot:cpm); stock Altair CP/M is 8080-compatible and boots on
the Z80 unchanged.
Extras beyond the 8080: shadow registers (EXX, EX AF,AF'), IX/IY index
registers with displacement, block transfer/search/I-O ops (LDIR, CPIR,
OTIR, …), interrupt modes IM 0/1/2 with IFF1/IFF2, and non-maskable
interrupts (cpu.triggerNMI()). The flags byte layout is S Z Y H X PV N C.
Z80 validation ROMs
npm run fixtures:z80 # downloads prelim/zexdoc/zexall into tests/fixtures/prelim.com runs as part of the normal test suite (~1s). The exhaustive
zexdoc/zexall exercisers take minutes and are gated:
Z80_ZEX=1 npx vitest run tests/integration/z80-zex.test.tsClocks & CPU Speed
MachineRunner
The recommended way to run a machine in real time. Drives cpu.step() at a configurable speed — an authentic 2 MHz by default — pacing simulated T-states against wall time. Software written for the 8080 (delay loops, disk timing, serial pacing) behaves correctly at 2 MHz; 'max' runs unthrottled.
import { MachineRunner } from '8sim';
const runner = new MachineRunner(cpu); // 2 MHz (stock 8080)
const runner = new MachineRunner(cpu, { hz: 4_000_000 }); // 4 MHz
const runner = new MachineRunner(cpu, { hz: 'max' }); // unthrottled
runner.start();
runner.setHz('max'); // change speed while running
console.log(runner.effectiveHz); // measured speed, for status displays
runner.stop();Pacing details: the CPU runs in ~1 ms slices of simulated time against absolute wall-time accounting (scheduler jitter self-corrects). When ahead, the runner sleeps the difference; when behind, it catches up in bounded slices (max 20 ms) so host I/O stays responsive; a host stall longer than 250 ms (GC pause, suspended tab) is forfeited rather than replayed at full speed. Browser-portable (performance.now + setTimeout); Node callers can pass a schedule option using setImmediate for higher 'max' throughput.
ImmediateClock
Counts T-states with no wall-time throttling. Suitable for tests and maximum-speed emulation.
const clock = new ImmediateClock();
const cycles = cpu.step();
clock.addCycles(cycles);
console.log(clock.getElapsedCycles()); // bigintSystemClock
The pacing engine used by MachineRunner — a T-state counter with a target frequency and absolute drift accounting (getAheadMs(), setHz(), resync()). Use it directly only if you're writing your own run loop.
Implementing Cards & Peripherals
Custom Card
An IS100Card bundles one or more devices and wires them onto the bus in its attach(bus) method — this is how the built-in cards (88-2SIO, MIO, …) are structured.
import type { IS100Card } from '@joezilla/8sim';
import { Bus, Usart8251 } from '@joezilla/8sim';
class ConsoleCard implements IS100Card {
readonly id = 'console';
private uart = new Usart8251('uart', 0x10, 0x11);
attach(bus: Bus): void { bus.attachIODevice(this.uart); } // claims ports 0x10/0x11
reset(): void { this.uart.reset(); }
}A card can just as well claim a memory region (attach a Ram/Rom/MemoryMappedIOAdapter) or combine both. The devices it attaches implement the interfaces below.
Custom I/O Device
import type { IIODevice } from './src/interfaces/IIODevice.js';
class Timer implements IIODevice {
readonly id = 'timer';
readonly basePorts = [0x40, 0x41, 0x42, 0x43]; // 8253-style ports
ioRead(port: number): number {
// return timer counter byte for this port
return 0;
}
ioWrite(port: number, value: number): void {
// configure timer
}
reset(): void { /* ... */ }
}
bus.attachIODevice(new Timer());Custom Memory
import type { IMemory } from './src/interfaces/IMemory.js';
class BankedRam implements IMemory {
readonly id = 'banked-ram';
readonly baseAddress = 0x4000;
readonly size = 0x4000;
readonly readOnly = false;
private banks: Uint8Array[] = [new Uint8Array(0x4000), new Uint8Array(0x4000)];
private activeBank = 0;
read(offset: number): number { return this.banks[this.activeBank]![offset] ?? 0xff; }
write(offset: number, value: number): void { this.banks[this.activeBank]![offset] = value & 0xff; }
reset(): void { this.banks.forEach(b => b.fill(0)); this.activeBank = 0; }
selectBank(n: number): void { this.activeBank = n; }
}Card Bundles & Machine Assembly
The Quick Start above wires a machine by hand. For declarative assembly, describe
the machine as data (a MachineSpec) and let buildMachine construct it — memory
regions attached in order, then cards, then the CPU. This is the layer host
applications (e.g. a machine-profile builder) target.
import { buildMachine } from '@joezilla/8sim';
const machine = buildMachine({
cpuKind: 'i8080', // or 'z80'
clock: 'max', // or { hz: 2_000_000 }
resetVector: 0xff00, // program counter at power-on
memory: [
{ id: 'ram', base: 0x0000, size: 0xff00, kind: 'ram' },
{ id: 'boot', base: 0xff00, size: rom.length, kind: 'rom', image: rom },
],
cards: [/* CardSpec[] — see bundles below */],
});
machine.cpu.step();Overlapping memory regions and colliding I/O ports are rejected at build time
(MachineSpecError) rather than silently last-writer-wins.
Seed card bundles
An S-100 machine is assembled from cards. A CardBundle packages a card's
manifest (its configurable surface), a uniform cardFactory, and — depending on
the card — one of three resolution outputs:
| Output | Declared by | Cards |
| --- | --- | --- |
| I/O device | claims(config) → ports/IRQ | imsai-sio2, mits-88-2sio, imsai-mio, proctech-3ps, mits-88-dcdd, imsai-mdc-dio, imsai-fif, pt-helios (+ chip-level intel-8251, motorola-6850, intel-8212, tr1602-uart) |
| Memory region | memory(config) → MemoryRegionSpec[] | ram-card, eprom-card |
| CPU | cpu(config) → { kind, resetVector? } | i8080-cpu, z80-cpu |
On real S-100 hardware the processor and memory are boards, so here they're cards too: a RAM/EPROM card resolves to a memory region (hoisted into the machine's memory map), and a CPU card is the bus master that sets the machine's CPU and power-on jump. Memory and CPU cards are no-ops on the bus itself.
import { seedBundles, seedBundleByName, withDefaults } from '@joezilla/8sim';
const cpu = seedBundleByName('z80-cpu')!;
const cfg = withDefaults(cpu.manifest, { resetVector: 0xf800 }); // validates against the schema
const resolved = cpu.cpu!(cfg); // → { kind: 'z80', resetVector: 0xf800 }
const region = seedBundleByName('ram-card')!.memory!(withDefaults(/* ... */)); // → MemoryRegionSpec[]seedBundles is the full registry; each bundle's manifest.configSchema drives
config validation (withDefaults throws CardConfigError on an out-of-range
value), and manifest.kind distinguishes a real S-100 card from a component
chip.
Integration Testing: cpudiag.com
The repository includes a CP/M CPU diagnostic integration test. To run it:
- Obtain
cpudiag.com(part of the public-domaincpm2.asmdiagnostics suite) - Place it at
tests/fixtures/cpudiag.com - Run
npm test
The test loads the binary at 0x0100, stubs the CP/M BDOS entry point at 0x0005 to capture console output, and expects the output to contain CPU IS OPERATIONAL before the CPU halts.
If the fixture is absent the test is silently skipped.
Examples: Interactive CP/M Boot
examples/boot-cpm.ts boots CP/M on the emulated 8080 (88-DSK boot PROM + 8251 console + MITS 88-DCDD floppy controller) against a live fdcplus-web server, bridging the console to your terminal for a real interactive session.
The same session runs on the Z80 core two ways: set CPU=z80 npm run boot:cpm, or use the dedicated examples/boot-cpm-z80.ts via npm run boot:cpm:z80. Stock Altair CP/M is 8080-compatible, so it boots on the Z80 unchanged.
The server location and API token are not hardcoded — they are read from environment variables, loaded from a local .env file:
| Variable | Description | Default |
|---|---|---|
| FDCPLUS_URL | Base URL of the fdcplus-web server | http://localhost:3000 |
| FDCPLUS_TOKEN | API token for the fdcplus-web server | (required) |
| FDCPLUS_CLIENT_ID | Stable client id for persistent disk writes | (unset — writes are ephemeral) |
| CPU_HZ | CPU speed: 2mhz, 4mhz, a raw Hz number, or max | 2mhz |
Note on disk writes: fdcplus-web sessions are copy-on-write — writes go to a per-client "splinter", not the master disk image. Without FDCPLUS_CLIENT_ID, the splinter (and any files you saved) is discarded when the emulator disconnects. With a stable id, your changes persist across sessions; merge them into the master image via the server's POST /api/drives/:id/transient/commit endpoint.
Setup:
- Copy the template:
cp .env.example .env - Edit
.envand setFDCPLUS_TOKEN(andFDCPLUS_URLif the server isn't on localhost) - Start the fdcplus-web server with a bootable disk mounted on drive 0
- Run
npm run boot:cpm
The .env file is gitignored so your token is never committed. The npm script loads it via Node's built-in --env-file-if-exists flag (requires Node ≥ 22), so no dotenv dependency is needed. Type at the A> prompt; press Ctrl-] to quit. Disk writes are flushed back to the mounted image on the server.
The live boot integration test (tests/integration/bootdisk.live.test.ts) uses the same variables; the vitest config loads .env automatically. The test skips itself when FDCPLUS_TOKEN is unset or the server is unreachable.
Booting period software
Beyond Altair CP/M, several cards boot their native period software. Each has an npm run script and works self-contained (bundled image / assembled ROM) or over fdcplus-web.
| Command | What it boots | Card(s) |
|---|---|---|
| npm run boot:cpm | Altair CP/M to an A> prompt (interactive) | 88-DCDD + 8251 |
| npm run boot:fif | IMSAI IMDOS 2.02 to an A> prompt; try DIR | FIF + SIO-2 |
| npm run boot:cuter | Processor Technology CUTER monitor (> prompt; DU dumps memory) over serial | 3P+S |
| npm run boot:helios | Helios II boot: the genuine SOLOS BOOTLOAD ROM DMAs track 0 and runs it | Helios II + 3P+S |
| npm run example:3ps | 3P+S serial echo — type and it echoes back | 3P+S |
Boot ROMs and monitors live under bios/ (e.g. the IMSAI MPU-A ROM for IMDOS, and the SOLOS-family sources under bios/sol20/, assembled by the build-*.sh scripts). boot:fif and boot:helios default to a bundled/synthetic image but switch to the fdcplus-web backend when FDCPLUS_URL is set (see the table above). These boots are also covered by guarded integration tests (they skip when the ROM/disk artifact is absent).
Browser Usage
Build a single-file ESM bundle:
npm run build
# produces dist/8sim.browser.js<script type="module">
import { Cpu8080, Bus, Ram, InterruptController } from './dist/8sim.browser.js';
const pic = new InterruptController();
const bus = new Bus(pic);
const ram = new Ram('ram', 0, 0x10000);
bus.attachMemory(ram);
const response = await fetch('myprog.bin');
const data = new Uint8Array(await response.arrayBuffer());
ram.load(data, 0x0100);
const cpu = new Cpu8080(bus, pic);
cpu.registers.pc = 0x0100;
cpu.registers.sp = 0x0100;
cpu.run(1_000_000);
</script>Project Structure
src/
├── cpu/
│ ├── Cpu8080.ts — Intel 8080 CPU: step(), run(), interrupt handling
│ ├── Registers.ts — A B C D E H L SP PC, pair accessors
│ ├── Flags.ts — S Z AC P CY, PSW byte serialize/deserialize
│ ├── Decoder.ts — 256-entry InstructionHandler dispatch table
│ ├── instructions/ — one file per 8080 instruction group
│ └── z80/ — Zilog Z80 core
│ ├── CpuZ80.ts — step loop, DD/FD prefixes, IM 0/1/2 + NMI
│ ├── RegistersZ80.ts — main + shadow + IX/IY/I/R/WZ
│ ├── FlagsZ80.ts — S Z Y H X PV N C
│ ├── DecoderZ80.ts — main/IX/IY + CB/ED/DDCB tables
│ ├── flagHelpers.ts — shared flag math (X/Y aware)
│ └── instructions/ — one file per Z80 instruction group
├── bus/ — Bus (backplane), BusRegion, SnoopBus (tracing tap)
├── memory/ — Ram, Rom, MemoryMappedIOAdapter
├── io/ — IoSpace
├── interrupt/ — InterruptController
├── clock/ — ImmediateClock, SystemClock
├── machine/ — buildMachine, MachineSpec, MachineRunner
├── cards/ — S-100 card classes: serial/parallel (88-2SIO, SIO-2, MIO, 3P+S) and
│ │ floppy/disk (88-DCDD, MDC-DIO, FIF, Helios II) controllers, plus the
│ │ component chips (8251, 6850, 8212, TR1602)
│ ├── floppy/ — shared IBM-3740 disk backend (in-memory + fdcplus-web) for MDC-DIO / FIF
│ ├── mdcdio/, fif/ — MDC-DIO firmware model / FIF descriptor engine
│ └── helios/ — Helios II firm-sectored disk model (+ SVH image format, fdcplus backend)
├── bundles/ — CardBundle + seed/ registry (CPU, RAM, EPROM, serial, floppy)
├── interfaces/ — ICpu IBus IMemory IIODevice IInterruptController IClock IModule IS100Card IBusObserver
└── util/bits.ts — u8 u16 signBit zeroFlag parityFlag auxCarryAdd toWord hi lo
tests/
├── cpu/ — per-instruction-group unit tests + interrupt tests
│ └── z80/ — Z80 registers, flags, CB/indexed/DDCB, interrupts
├── bus/ — bus routing, ROM protection, MMIO
├── memory/ — RAM, ROM, MemoryMappedIOAdapter
└── integration/ — cpudiag.com + Z80 prelim/zexdoc/zexall end-to-end testsLicense
Apache License 2.0 — see LICENSE for details.
