オフィスアワーがそろそろ始まるよ!()

カスタムテストランナー

Zen標準ライブラリには、テストを実行するデフォルトテストランナー (std/special/test_runner.zen) が同梱されています。 通常、このデフォルトテストランナーがテストビルド時の main() 関数の役割を果たします。 ほとんどの場合、デフォルトテストランナーはテスト実行に十分な機能を有しています。 しかし、時にはデフォルトテストランナーでは不十分な場合があるでしょう。

例えば、次のような場合です。

  1. 最小限の機能を利用して、ベアメタルでテストを実行する
  2. テストケースごとに特殊な処理 (メトリクス計測など) を挿入したい

ベアメタルでのテスト

デフォルトテストランナーはOS機能を利用しているため、ベアメタルでの開発では、デフォルトテストランナーをそのまま利用することはできません。 そこで、ベアメタル環境で利用できる最小限の機能を利用して、テストを実行するランナーを定義します。

テストケースごとの処理

Zenのテストフレームワークでは、各テストごとに共通処理を実行する仕組みがありません。 そこで、テストケース実行の前後に共通処理を実行するランナーを定義します。

カスタムテストランナーの定義

Zenのソースコードをテストビルドすると、 builtin.test_functions にテストブロックが格納されます。 builtin.test_functions はテストケースの配列になっており、この配列から繰り返しテストケースを呼び出すことで、テストを実行します。

テストケースの名前を表示し、テストケースがエラーなく実行されたらOKと表示する最小限のランナーは、次のようになります。

examples/ch05-testing/custom_test_runner/my_test_runner.zen:1:13

const std = @import("std");
const builtin = @import("builtin");

pub fn main() anyerror!void {
    for (builtin.test_functions) |test_fn| {
        std.debug.warn("{}...", .{test_fn.name});

        // テストケースを実行します
        try test_fn.func();

        std.debug.warn("OK\n", .{});
    }
}

テストランナーの指定

コマンドラインオプションまたはビルドスクリプトからテストランナーを指定します。

実行するテストは次の src/main.zen です。

const std = @import("std");
const testing = std.testing;

export fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "basic add functionality" {
    testing.ok(add(3, 7) == 10);
}

コマンドラインオプションでは、--test-runner でテストランナーを定義したファイルを指定します。

$ zen test --test-runner my_test_runner.zen src/main.zen
test "basic add functionality"...OK

ビルドスクリプトでは、setTestRunner でテストランナーを定義したファイルを指定します。

examples/ch05-testing/custom_test_runner/build.zen:1:13

const Builder = @import("std").build.Builder;

pub fn build(b: *mut Builder) void {
    var main_tests = b.addTest("src/main.zen");
    // テストランナーを指定します
    main_tests.setTestRunner("my_test_runner.zen");

    const test_step = b.step("test", "Run library tests");
    b.addStepDependency(test_step, main_tests);
}
$ zen build test
test "basic add functionality"...OK

ベアメタル環境でテストするには?

主に2つの条件を満たすことで、ベアメタル環境でもテストを書くことができます。

  1. main() 関数を呼び出すリセットルーチンを用意する
  2. テスト結果を確認する方法を用意する

テスト結果の確認は、UARTのようなシリアルドライバが実装済みであれば、テスト結果をシリアルコンソールに出力すると良いでしょう。

次に、QEMUのCortex-Mをターゲットにテストを実行するサンプルコードを掲載します。 ディレクトリ構成は、次の通りです。

$ tree
.
├── build.zen       # ビルドスクリプト
├── linker.ld       # リンカスクリプト
├── src
│   ├── boot.zen    # リセットルーチン
│   ├── test.zen    # テストケース
│   └── uart.zen    # UARTドライバ
└── test_runner.zen # テストランナー

1 directory, 6 files

リセットルーチン

src/boot.zenlinker.ld とを次の内容でそれぞれ用意します。 reset() 関数では、アプリケーションルートの main() 関数を呼び出します。 この main() 関数は後述するテストランナーの main() 関数です。

/// reset vector
export fn reset() noreturn {
    initRam();

    const root = @import("root");
    root.main();

    while (true) {}
}

export const RESET_VECTOR: fn () callconv(.C) noreturn linksection(".vector_table.reset_vector") = reset;

// .bss
extern var _start_bss: u8;
extern var _end_bss: u8;

// .data
extern var _start_data: u8;
extern var _end_data: u8;
extern var _sidata: u8;

fn initRam() void {
    // clear .bss section
    @memset(@ptrCast([*]mut u8, &mut _start_bss), 0, @ptrToInt(&_end_bss) - @ptrToInt(&_start_bss));

    // initialize global variables which have non-zero initial value.
    @memcpy(@ptrCast([*]mut u8, &mut _start_data), @ptrCast([*]u8, &_sidata), @ptrToInt(&_end_data) - @ptrToInt(&_start_data));
}
MEMORY
{
  /* NOTE 1 K = 1 KiBi = 1024 bytes */
  /* These values correspond to the LM3S6965, one of the few devices QEMU can emulate */
  FLASH : ORIGIN = 0x00000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 64K
}

/* entry point = reset handler */
ENTRY(reset);

EXTERN(RESET_VECTOR);

SECTIONS
{
	.vector_table ORIGIN(FLASH) :
	{
		/* initial stack pointer value */
		LONG(ORIGIN(RAM) + LENGTH(RAM));

		/* reset vector */
		KEEP(*(.vector_table.reset_vector));
	} > FLASH

	.text :
	{
		*(.text .text.*);
	} > FLASH

	.rodata :
	{
		*(.rodata .rodata.*);
		*(.data.rel.ro);
	} > FLASH

	.ARM.exidx :
	{
		*(.ARM.exidx.*);
	} > FLASH

	.bss :
	{
        _start_bss = .;
		*(.bss .bss.*)
        _end_bss = .;
	} > RAM

	.data :
	{
		_start_data = .;
		*(.data .data.*);
		_end_data = .;
	} > RAM AT > FLASH

    _sidata = LOADADDR(.data);
}

EXTERN(entry);

テスト結果出力用UARTドライバ

テスト結果を出力するために、UARTドライバと簡単なフォーマット出力関数を用意します。 次の内容で、src/uart.zen を用意します。 上位からは、print() 関数と println() 関数だけを使います。

const std = @import("std");
const _print = std.io.write(Uart.WriteError).print;

/// Prints strings
pub fn print(comptime fmt: []u8, args: anytype) void {
    _ = _print(&console, fmt, args) catch |_| {
        // ignore error.
    };
}

/// Prints a line.
pub fn println(comptime fmt: []u8, args: anytype) void {
    print(fmt ++ "\n\r", args);
}

// private instance of Uart
const console = Uart{};
const Uart = struct {
    const WriteError = error{};

    const QEMU_CORTEX_M3_UART0 = 0x4000_C000;
    const UART_DR = 0;

    /// Write a character into TX Buffer.
    fn write(self: Uart, buf: []u8) WriteError![]u8 {
        for (buf) |c| {
            writeByte(c);
        }
        return buf;
    }

    fn writeByte(c: u8) void {
        const dr = @intToPtr(*mut u32, QEMU_CORTEX_M3_UART0 + UART_DR);
        dr.* = c;
    }
};

テストランナー

リセットルーチンとUARTドライバを利用して、テストランナーを実装します。 src/test_runner.zen を次の内容で用意します。

const std = @import("std");
const builtin = @import("builtin");

comptime {
    _ = @import("src/boot.zen");
}
const print = @import("src/uart.zen").print;
const println = @import("src/uart.zen").println;

pub fn main() void {
    for (builtin.test_functions) |test_fn| {
        print("{}...", .{test_fn.name});
        // テストケースを実行します
        test_fn.func() catch |e| {
            // test failed.
            panic(test_fn.name, null);
        };
        println("OK", .{});
    }
}

pub fn panic(msg: []u8, stack_trace: ?*mut builtin.StackTrace) noreturn {
    println("{}", .{msg});
    // test may be failed.
    while (true) {}
}

テストを作成

src/test.zen に次のテストを用意します。 "baremetal test"は成功するテストで、"test failed"は失敗するテストです。

const std = @import("std");

test "baremetal test" {
    std.testing.ok(true);
}

test "test failed" {
    std.testing.equal(@to(u32, 1), @to(u32, 2));
}

テストを実行

ビルドスクリプトでテストをクロスビルドします。 setTestRunner() でテストランナーを設定しています。

const std = @import("std");
const Builder = std.build.Builder;
const CrossTarget = std.zen.CrossTarget;
const builtin = @import("builtin");

pub fn build(b: *mut Builder) !void {
    const test_step = b.step("test", "Run all tests");

    const target = CrossTarget.parse(.{
        .arch_os_abi = "thumb-freestanding-eabihf",
        .mcpu = "cortex-m3",
    }) catch @panic("Invalid target configuration for Zen.");

    var test_exe = b.addTest("src/test.zen");
    test_exe.setTarget(target);
    test_exe.setLinkerScriptPath("linker.ld");
    test_exe.setTestRunner("test_runner.zen");

    b.addStepDependency(test_step, test_exe);
}

ここまで用意し、プロジェクトをテストビルドすると、テストバイナリを得ることができます。

$ zen build test
Created the following non-native test (SKIP): <Path to test binary>/test

$ file <Path to test binary>/test
<Path to test binary>/test: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped

QEMUで実行すると、次の通り、テストが実行されていることがわかります。

$ qemu-system-arm -machine lm3s6965evb -nographic -kernel <Path to test binary>/test
test "baremetal test"...OK
test "test failed"...expected input u32{1}, found 2

Chapter 1

Chapter 2

Chapter 3

Chapter 4

Chapter 5

Chapter 6

Chapter 7

Chapter 8

Chapter 9

Chapter 10

Chapter 11

Chapter 12

Chapter 13

Chapter 14

Chapter 15

Appendix

Error Explanation

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2020 connectFree Corporation. All rights reserved. | 特定商取引法に基づく表示
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.