compiler: Check and initialize test discovery storage

b213f338699a7fbaf36f5b52cdbdd5891ee19ed79cc06a5cc22c0e28e9960c46
Alexis Sellier committed ago 1 parent da9699b9
Makefile +10 -2
29 29
30 30
# Verify the emulator binary exists.
31 31
EMU_PATH := $(shell command -v $(EMU) 2>/dev/null)
32 32
33 33
default: emulator $(RAD_BIN)
34 -
test: emulator seed-test std-test bin-test
34 +
test: emulator seed-test std-test bin-test driver-test
35 35
36 36
seed-test:
37 37
	@seed/test
38 38
39 39
# Emulator command check
79 79
		$(addsuffix .s,$(STD_LIB_TEST)) \
80 80
		$(addsuffix .o,$(STD_LIB_TEST))
81 81
82 82
# Binary Tests
83 83
84 +
# Driver fixtures exercise test discovery through the compiler command line.
85 +
driver-test: $(RAD_BIN)
86 +
	@RAD_EMULATOR="$(EMU)" RAD_BIN="$(RAD_BIN)" test/driver
87 +
88 +
driver-test-image: $(RAD_BIN)
89 +
	@$(RADIANCE) -test $(STD) -mod lib/std/testing.rad $(DRIVER_TEST_INPUTS) $(DRIVER_TEST_FLAGS) \
90 +
		-entry selected -o $(DRIVER_TEST_OUTPUT)
91 +
84 92
BIN_TEST_DIR := test/tests
85 93
# Only tests with `//! returns:` are compiled to binaries and executed.
86 94
BIN_TEST_EXE_SRC := $(shell grep -rl '^//! returns:' $(BIN_TEST_DIR))
87 95
BIN_TEST_RAD_EXE_SRC := $(filter %.rad,$(BIN_TEST_EXE_SRC))
88 96
BIN_TEST_RAS_EXE_SRC := $(filter %.ras,$(BIN_TEST_EXE_SRC))
141 149
142 150
t: test
143 151
c: clean
144 152
145 153
.PHONY: test clean default seed-test std-test bin-test seed \
146 -
	clean-std-test clean-bin-test clean-rad emulator
154 +
	clean-std-test clean-bin-test clean-rad emulator driver-test driver-test-image
147 155
.SUFFIXES:
148 156
.DELETE_ON_ERROR:
149 157
.SILENT:
compiler/radiance.rad +21 -15
657 657
}
658 658
659 659
/// Scan a single module's AST for `@test` functions and append them to `tests`.
660 660
fn collectModuleTests(
661 661
    entry: *module::ModuleEntry,
662 -
    tests: &mut [TestDesc],
662 +
    tests: &mut [?TestDesc],
663 663
    testCount: &mut u32
664 664
) {
665 665
    let modAst = module::astFor(entry) else {
666 666
        return;
667 667
    };
682 682
            }
683 683
        }
684 684
    }
685 685
}
686 686
687 +
/// Collect initialized test descriptors from one package in module order.
688 +
fn collectPackageTests(graph: &module::ModuleGraph, packageId: u16, tests: &mut [?TestDesc]) -> u32 {
689 +
    let mut count: u32 = 0;
690 +
    for modIdx in 0..graph.entriesLen {
691 +
        if let entry = module::get(graph, modIdx as u16) {
692 +
            if entry.packageId == packageId {
693 +
                collectModuleTests(entry, tests, &mut count);
694 +
            }
695 +
        }
696 +
    }
697 +
    return count;
698 +
}
699 +
687 700
/// Synthesize a `testing::test("mod", "name", mod::fn)` call for one test.
688 701
unsafe fn synthTestCall(arena: &mut ast::NodeArena, desc: &TestDesc) -> *ast::Node {
689 702
    let callee = synthScopeAccess(arena, &["testing", "test"]);
690 703
    let modStr = il::formatQualifiedName(
691 704
        &mut arena.arena,
694 707
    );
695 708
    let modArg = ast::synthNode(arena, ast::NodeValue::String(modStr));
696 709
    let nameArg = ast::synthNode(arena, ast::NodeValue::String(desc.fnName));
697 710
698 711
    // Intra-package path: skip the package name prefix.
699 -
    let mut funcPath: [*[u8]; 16] = undefined;
712 +
    let mut funcPath: [*[u8]; 16] = [""; 16];
700 713
    for j in 1..desc.modPath.len {
701 714
        set funcPath[j - 1] = desc.modPath[j];
702 715
    }
703 716
    set funcPath[desc.modPath.len - 1] = desc.fnName;
704 717
    let funcArg = synthScopeAccess(arena, &funcPath[..desc.modPath.len]);
733 746
) throws (Error) {
734 747
    let entryPkg = try getEntryPackage(ctx);
735 748
    let root = try getRootModule(&entryPkg, &ctx.graph);
736 749
737 750
    // Collect test functions from the entry package's modules.
738 -
    let mut tests: [TestDesc; MAX_TESTS] = undefined;
739 -
    let mut testCount: u32 = 0;
740 -
741 -
    for modIdx in 0..ctx.graph.entriesLen {
742 -
        if let entry = module::get(&ctx.graph, modIdx as u16) {
743 -
            if entry.packageId == entryPkg.id {
744 -
                collectModuleTests(entry, &mut tests[..], &mut testCount);
745 -
            }
746 -
        }
747 -
    }
751 +
    let mut tests: [?TestDesc; MAX_TESTS] = [nil; MAX_TESTS];
752 +
    let testCount = collectPackageTests(&ctx.graph, entryPkg.id, &mut tests[..]);
748 753
    if testCount == 0 {
749 754
        throw error(&["fatal:", "no test functions found"]);
750 755
    }
751 -
    let mut countBuf: [u8; 10] = undefined;
756 +
    let mut countBuf: [u8; 10] = [0; 10];
752 757
    let start = fmt::formatU32(testCount, &mut countBuf[..]);
753 758
    io::printError("radiance: ");
754 759
    io::printError(entryPkg.name);
755 760
    io::printError(": found ");
756 761
    io::printError(&countBuf[start..]);
764 769
        throw error(&["failed to set test runner AST"]);
765 770
    };
766 771
}
767 772
768 773
/// Synthesize the test entry point.
769 -
unsafe fn synthTestMainFn(arena: &mut ast::NodeArena, tests: &[TestDesc]) -> *ast::Node {
774 +
unsafe fn synthTestMainFn(arena: &mut ast::NodeArena, tests: &[?TestDesc]) -> *ast::Node {
770 775
    // Build array literal: `[testing::test(...), ...]`.
771 776
    let a = alloc::arenaAllocator(&mut arena.arena);
772 777
    let mut elements = ast::nodeSlice(arena, tests.len as u32);
773 778
    for i in 0..tests.len {
774 -
        elements.append(synthTestCall(arena, &tests[i]), a);
779 +
        let desc = tests[i] else panic "synthTestMainFn: missing active test";
780 +
        elements.append(synthTestCall(arena, &desc), a);
775 781
    }
776 782
    let arrayLit = ast::synthNode(arena, ast::NodeValue::ArrayLit(elements));
777 783
778 784
    // Build: `&[...]`.
779 785
    let testsRef = ast::synthNode(arena, ast::NodeValue::AddressOf(ast::AddressOf {
test/driver added +85 -0
1 +
#!/bin/sh
2 +
# Verify test discovery, package selection, and descriptor capacity.
3 +
set -eu
4 +
ulimit -c 0
5 +
work=$(mktemp -d)
6 +
trap 'rm -rf "$work"' EXIT HUP INT TERM
7 +
mkdir "$work/selected"
8 +
9 +
# The dependency test must not enter the selected package's runner.
10 +
printf '%s\n' '/// Test outside the entry package.' '@test fn excluded() { assert false; }' > "$work/dependency.rad"
11 +
printf '%s\n' 'use std::testing;' '/// Test in a child module.' \
12 +
    '@test fn childTest() throws (testing::TestError) { assert true; }' > "$work/selected/child.rad"
13 +
14 +
# Compile through Make so the driver uses the standard memory settings.
15 +
compile() {
16 +
    timeout 300 "${MAKE:-make}" --no-print-directory driver-test-image \
17 +
        RAD_BIN="${RAD_BIN:-bin/radiance.rv64.dev}" \
18 +
        RAD_EMULATOR="${RAD_EMULATOR:-emulator}" \
19 +
        DRIVER_TEST_INPUTS="-pkg dependency -mod $work/dependency.rad -pkg selected -mod $work/selected.rad $1" \
20 +
        DRIVER_TEST_FLAGS="${2:-}" \
21 +
        DRIVER_TEST_OUTPUT="$work/result.rv64" > "$work/compile.log" 2>&1
22 +
}
23 +
24 +
# Check one successful runner and its exact number of executed tests.
25 +
run() {
26 +
    if ! compile "$1"; then
27 +
        cat "$work/compile.log"
28 +
        exit 1
29 +
    fi
30 +
    if ! timeout 10 "${RAD_EMULATOR:-emulator}" -run "$work/result.rv64" > "$work/run.log" 2>&1; then
31 +
        cat "$work/run.log"
32 +
        exit 1
33 +
    fi
34 +
    grep -Fq "test result: ok. $2 passed; 0 failed" "$work/run.log"
35 +
    test "$(grep -c '^test selected::' "$work/run.log")" -eq "$2"
36 +
}
37 +
38 +
printf '%s\n' 'use std::testing;' 'mod child;' \
39 +
    '/// Test in the package root.' '@test fn rootTest() throws (testing::TestError) { assert true; }' > "$work/selected.rad"
40 +
run "-mod $work/selected/child.rad" 2
41 +
grep -Fq 'test selected::rootTest ... ok' "$work/run.log"
42 +
grep -Fq 'test selected::child::childTest ... ok' "$work/run.log"
43 +
44 +
printf '%s\n' 'use std::testing;' > "$work/selected.rad"
45 +
if compile ''; then
46 +
    echo 'driver: empty test discovery unexpectedly succeeded'
47 +
    exit 1
48 +
fi
49 +
grep -Fq 'no test functions found' "$work/compile.log"
50 +
51 +
# MAX_TESTS is the compiler's supported discovery capacity.
52 +
for count in 1024 1025; do
53 +
    awk -v count="$count" -v directory="$work/selected" 'BEGIN {
54 +
        print "use std::testing;"
55 +
        for (i = 0; i < count; i++) {
56 +
            part = int(i / 256)
57 +
            path = directory "/part" part ".rad"
58 +
            if (i % 256 == 0) {
59 +
                printf "/// Capacity test group.\nmod part%d;\n", part
60 +
                print "use std::testing;" > path
61 +
            }
62 +
            print "/// Generated capacity test." > path
63 +
            printf "@test fn test%d() throws (testing::TestError) { assert true; }\n", i > path
64 +
        }
65 +
    }' > "$work/selected.rad"
66 +
    inputs=''
67 +
    for path in "$work/selected"/part*.rad; do
68 +
        inputs="$inputs -mod $path"
69 +
    done
70 +
    if [ "$count" -eq 1024 ]; then
71 +
        if ! compile "$inputs" '-dump il'; then
72 +
            cat "$work/compile.log"
73 +
            exit 1
74 +
        fi
75 +
        grep -Fq 'selected: found 1024 test(s)' "$work/compile.log"
76 +
        test "$(grep -c 'call.*std::testing::test' "$work/compile.log")" -eq "$count"
77 +
    else
78 +
        if compile "$inputs"; then
79 +
            echo 'driver: test discovery overflow unexpectedly succeeded'
80 +
            exit 1
81 +
        fi
82 +
        grep -Fq 'Runtime error (EBREAK)' "$work/compile.log"
83 +
    fi
84 +
done
85 +
echo 'driver tests: 4 passed'