test/runner.rad 7.0 KiB raw
1
//! IL snapshot test runner.
2
//!
3
//! Given a `.rad` source file, lowers it to IL and compares the output
4
//! against the corresponding `.ril` snapshot file. Called by `test/run`
5
//! for every test that has a `.ril` file.
6
7
use std::io;
8
use std::mem;
9
use std::sys;
10
use std::sys::unix;
11
use std::lang::alloc;
12
use std::lang::ast;
13
use std::lang::il::printer;
14
use std::lang::parser;
15
use std::lang::scanner;
16
use std::lang::resolver;
17
use std::lang::strings;
18
use std::lang::lower;
19
20
/// Buffer size for reading source files (8 KB).
21
const SOURCE_BUF_SIZE: u32 = 8192;
22
/// Buffer size for reading expected IL files (32 KB).
23
const EXPECTED_BUF_SIZE: u32 = 32768;
24
/// Buffer size for generated IL output (32 KB).
25
const OUTPUT_BUF_SIZE: u32 = 32768;
26
/// Arena size for AST/IL allocations (512 KB).
27
const ARENA_SIZE: u32 = 524288;
28
/// Maximum path length for expected IL file path.
29
const MAX_PATH_LEN: u32 = 256;
30
31
/// String pool.
32
static STRING_POOL: strings::Pool = strings::Pool { table: undefined, count: 0 };
33
34
/// Maximum number of AST nodes per test file.
35
const MAX_NODE_DATA: u32 = 4096;
36
/// Maximum number of resolver errors per test file.
37
const MAX_ERRORS: u32 = 16;
38
39
// Static storage for large buffers to avoid stack overflow.
40
// Tests run serially so sharing these is safe.
41
static SOURCE_BUF: [u8; SOURCE_BUF_SIZE] = undefined;
42
static EXPECTED_BUF: [u8; EXPECTED_BUF_SIZE] = undefined;
43
static OUTPUT_BUF: [u8; OUTPUT_BUF_SIZE] = undefined;
44
static AST_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
45
static IL_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
46
static PRINT_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
47
static RESOLVER_ARENA_STORAGE: [u8; ARENA_SIZE] = undefined;
48
static NODE_DATA_STORAGE: [resolver::NodeData; MAX_NODE_DATA] = undefined;
49
static ERROR_STORAGE: [resolver::Error; MAX_ERRORS] = undefined;
50
51
/// Strip comment from a line. Returns sub-slice up to ';', trimmed of trailing whitespace.
52
fn stripLine(line: *[u8]) -> *[u8] {
53
    let mut end: u32 = 0;
54
    let mut i: u32 = 0;
55
56
    while i < line.len and line[i] != ';' {
57
        if line[i] != ' ' and line[i] != '\t' {
58
            end = i + 1;
59
        }
60
        i += 1;
61
    }
62
    return &line[..end];
63
}
64
65
/// Get next line from string at offset. Returns the line and updates offset past newline.
66
fn nextLine(s: *[u8], offset: *mut u32) -> *[u8] {
67
    let start = *offset;
68
    let mut i = start;
69
70
    while i < s.len and s[i] != '\n' {
71
        i += 1;
72
    }
73
    let line = &s[start..i];
74
    if i < s.len {
75
        *offset = i + 1;
76
    } else {
77
        *offset = i;
78
    }
79
    return line;
80
}
81
82
/// Compare two strings ignoring comments, line by line.
83
fn stringsEqual(a: *[u8], b: *[u8]) -> bool {
84
    let mut ai: u32 = 0;
85
    let mut bi: u32 = 0;
86
87
    while ai < a.len or bi < b.len {
88
        let mut aLine = "";
89
        while ai < a.len and aLine.len == 0 {
90
            aLine = stripLine(nextLine(a, &mut ai));
91
        }
92
        let mut bLine = "";
93
        while bi < b.len and bLine.len == 0 {
94
            bLine = stripLine(nextLine(b, &mut bi));
95
        }
96
        if not mem::eq(aLine, bLine) {
97
            return false;
98
        }
99
    }
100
    return true;
101
}
102
103
/// Print diff between expected and actual output.
104
fn printDiff(expected: *[u8], actual: *[u8]) {
105
    io::printLn("\n;; Expected");
106
    io::print(expected);
107
    io::print("\n");
108
109
    io::printLn(";; Actual");
110
    io::print(actual);
111
    io::print("\n");
112
}
113
114
/// Derive the `.ril` path from a `.rad` source path. Returns nil if the path
115
/// does not end in `.rad` or the buffer is too small. The result is
116
/// null-terminated for use with syscalls.
117
fn deriveRilPath(sourcePath: *[u8], buf: *mut [u8]) -> ?*[u8] {
118
    let len = sourcePath.len;
119
    if len < 4 {
120
        return nil;
121
    }
122
    // Check for .rad extension.
123
    if not mem::eq(&sourcePath[len - 4..len], ".rad") {
124
        return nil;
125
    }
126
    if len + 1 > buf.len {
127
        return nil;
128
    }
129
    // Copy full path then overwrite extension and add null terminator.
130
    try mem::copy(buf, sourcePath) catch {
131
        return nil;
132
    };
133
    // TODO: Rewrite this once we fix the compiler bug and add support for
134
    // additions in ranges.
135
    buf[len - 3] = 'r';
136
    buf[len - 2] = 'i';
137
    buf[len - 1] = 'l';
138
    buf[len] = 0;
139
140
    return &buf[..len];
141
}
142
143
/// Run a single IL snapshot test case. Returns `true` on success.
144
fn runTest(sourcePath: *[u8]) -> bool {
145
    // Path buffer.
146
    let mut rilPathBuf: [u8; MAX_PATH_LEN] = undefined;
147
    let mut pkgScope: resolver::Scope = undefined;
148
149
    // Derive .ril path from source path.
150
    let rilPath = deriveRilPath(sourcePath, &mut rilPathBuf[..]) else {
151
        io::print("error: invalid source path (must end in .rad): ");
152
        io::printLn(sourcePath);
153
        return false;
154
    };
155
156
    // Read expected IL.
157
    let expected = unix::readFile(rilPath, &mut EXPECTED_BUF[..]) else {
158
        io::print("error: could not read expected IL: ");
159
        io::printLn(rilPath);
160
        return false;
161
    };
162
163
    // Read source file.
164
    let source = unix::readFile(sourcePath, &mut SOURCE_BUF[..]) else {
165
        io::print("error: could not read source: ");
166
        io::printLn(sourcePath);
167
        return false;
168
    };
169
170
    // Parse source.
171
    let mut astArena = ast::nodeArena(&mut AST_ARENA_STORAGE[..]);
172
    let root = try parser::parse(scanner::SourceLoc::String, source, &mut astArena, &mut STRING_POOL) catch {
173
        io::printLn("error: parsing failed");
174
        return false;
175
    };
176
177
    // Run resolver.
178
    let storage = resolver::ResolverStorage {
179
        arena: alloc::new(&mut RESOLVER_ARENA_STORAGE[..]),
180
        nodeData: &mut NODE_DATA_STORAGE[..],
181
        pkgScope: &mut pkgScope,
182
        errors: &mut ERROR_STORAGE[..],
183
    };
184
    let config = resolver::Config { buildTest: false };
185
    let mut res = resolver::resolver(storage, config);
186
    let diag = try resolver::resolveModuleRoot(&mut res, root) catch {
187
        io::printLn("error: resolver failed");
188
        return false;
189
    };
190
    if not resolver::success(&diag) {
191
        io::printLn("error: resolver failed");
192
        return false;
193
    }
194
195
    // Lower to IL.
196
    let mut ilArena = alloc::new(&mut IL_ARENA_STORAGE[..]);
197
    let program = try lower::lower(&res, root, "test", &mut ilArena) catch err {
198
        io::print("error: lowering failed: ");
199
        lower::printError(err);
200
        io::printLn("");
201
        return false;
202
    };
203
204
    // Print IL to buffer.
205
    let mut printArena = alloc::new(&mut PRINT_ARENA_STORAGE[..]);
206
    let actual = printer::printProgramToBuffer(&program, &mut printArena, &mut OUTPUT_BUF[..]);
207
208
    // Compare ignoring comments.
209
    if not stringsEqual(actual, expected) {
210
        io::printLn("FAILED");
211
        printDiff(expected, actual);
212
        return false;
213
    }
214
    io::printLn("ok");
215
216
    return true;
217
}
218
219
/// Run a single test specified as an argument.
220
@default fn main(env: *sys::Env) -> i32 {
221
    let args = env.args;
222
223
    if args.len != 2 {
224
        io::printError("error: expected test file path as argument");
225
        return 1;
226
    }
227
    let sourcePath = args[1];
228
    io::print("test ");
229
    io::print(sourcePath);
230
    io::print(" ... ");
231
232
    if runTest(sourcePath) {
233
        return 0;
234
    } else {
235
        return 1;
236
    }
237
}