native test framework
This commit is contained in:
@@ -227,6 +227,7 @@ Function :: struct {
|
||||
c_abi: bool,
|
||||
imported: bool,
|
||||
generated: bool,
|
||||
test: bool,
|
||||
file_hidden: bool,
|
||||
has_body: bool,
|
||||
variadic: bool,
|
||||
@@ -276,6 +277,7 @@ Import :: struct {
|
||||
target: Package_Id,
|
||||
valid: bool,
|
||||
used: bool,
|
||||
test_only: bool,
|
||||
diagnostic: source.Diagnostic_Id,
|
||||
}
|
||||
|
||||
@@ -310,9 +312,15 @@ Package :: struct {
|
||||
path: string,
|
||||
name: symbol.Id,
|
||||
available: bool,
|
||||
test: bool,
|
||||
kind: Package_Kind,
|
||||
}
|
||||
|
||||
Compile_Mode :: enum u8 {
|
||||
Executable,
|
||||
Test,
|
||||
}
|
||||
|
||||
Package_Kind :: enum u8 {
|
||||
Native,
|
||||
C_Header,
|
||||
|
||||
+69
-24
@@ -43,26 +43,7 @@ destroy_build_config :: proc(cfg: ^BuildConfig) {
|
||||
delete(cfg.c_options.defines)
|
||||
}
|
||||
|
||||
// run_build implements `brolang build [root]`: it loads and type-checks
|
||||
// `root/build.bro`, reads its `config` constant, and compiles the program
|
||||
// package the config names. build.bro is only checked (never lowered/emitted),
|
||||
// so the config is read straight from the HIR.
|
||||
run_build :: proc(root: string) -> int {
|
||||
project_root := root
|
||||
owns_project_root := false
|
||||
if len(project_root) == 0 {
|
||||
found_root, found := find_build_root()
|
||||
if !found {
|
||||
fmt.eprintln("brolang build: could not find build.bro in the current directory or any parent")
|
||||
return 2
|
||||
}
|
||||
project_root = found_root
|
||||
owns_project_root = true
|
||||
}
|
||||
defer if owns_project_root {
|
||||
delete(project_root)
|
||||
}
|
||||
|
||||
load_build_config :: proc(project_root: string) -> (BuildConfig, bool) {
|
||||
sources := source.init_store()
|
||||
defer source.destroy_store(&sources)
|
||||
diagnostics := source.init_store_diagnostics(&sources)
|
||||
@@ -73,7 +54,7 @@ run_build :: proc(root: string) -> int {
|
||||
arena: vmem.Arena
|
||||
if err := vmem.arena_init_growing(&arena); err != nil {
|
||||
fmt.eprintln("failed to initialize build arena:", err)
|
||||
return 2
|
||||
return {}, false
|
||||
}
|
||||
defer vmem.arena_destroy(&arena)
|
||||
a := vmem.arena_allocator(&arena)
|
||||
@@ -82,7 +63,7 @@ run_build :: proc(root: string) -> int {
|
||||
if !loaded {
|
||||
source.print_all(&diagnostics)
|
||||
fmt.eprintln("failed to load build root:", project_root)
|
||||
return 2
|
||||
return {}, false
|
||||
}
|
||||
// check needs no `main`: it synthesizes a trap main and emits one benign
|
||||
// "missing main" diagnostic, which is expected for build.bro. Suppress that
|
||||
@@ -90,10 +71,32 @@ run_build :: proc(root: string) -> int {
|
||||
hir_module := checker.check(&ast_module, &diagnostics, &symbols, target.DEFAULT, a)
|
||||
if build_bro_has_errors(&diagnostics) {
|
||||
source.print_all(&diagnostics)
|
||||
return {}, false
|
||||
}
|
||||
return extract_build_config(&hir_module, &symbols)
|
||||
}
|
||||
|
||||
project_root_for_command :: proc(root, command: string) -> (string, bool, bool) {
|
||||
if len(root) > 0 {
|
||||
return root, false, true
|
||||
}
|
||||
project_root, found := find_build_root()
|
||||
if !found {
|
||||
fmt.eprintfln("brolang %s: could not find build.bro in the current directory or any parent", command)
|
||||
return "", false, false
|
||||
}
|
||||
return project_root, true, true
|
||||
}
|
||||
|
||||
// run_build implements `brolang build [root]`: it reads build.bro and compiles
|
||||
// the configured program package.
|
||||
run_build :: proc(root: string) -> int {
|
||||
project_root, owned, found := project_root_for_command(root, "build")
|
||||
if !found {
|
||||
return 2
|
||||
}
|
||||
|
||||
cfg, ok := extract_build_config(&hir_module, &symbols)
|
||||
defer if owned {delete(project_root)}
|
||||
cfg, ok := load_build_config(project_root)
|
||||
if !ok {
|
||||
return 2
|
||||
}
|
||||
@@ -109,6 +112,48 @@ run_build :: proc(root: string) -> int {
|
||||
return compile_package(program, output, cfg.link_arguments, target.DEFAULT, cfg.c_options, project_root)
|
||||
}
|
||||
|
||||
run_tests :: proc(root: string) -> int {
|
||||
project_root, owned, found := project_root_for_command(root, "test")
|
||||
if !found {
|
||||
return 2
|
||||
}
|
||||
defer if owned {delete(project_root)}
|
||||
cfg, ok := load_build_config(project_root)
|
||||
if !ok {
|
||||
return 2
|
||||
}
|
||||
defer destroy_build_config(&cfg)
|
||||
|
||||
program := filepath.join({project_root, cfg.source_dir})
|
||||
defer delete(program)
|
||||
test_name := fmt.aprintf("%s-test", cfg.output_name)
|
||||
defer delete(test_name)
|
||||
output, output_ok := build_output_path(project_root, test_name)
|
||||
if !output_ok {
|
||||
return 2
|
||||
}
|
||||
defer delete(output)
|
||||
status := compile_package(
|
||||
program, output, cfg.link_arguments, target.DEFAULT, cfg.c_options, project_root, .Test,
|
||||
)
|
||||
if status != 0 {
|
||||
return status
|
||||
}
|
||||
state, stdout, stderr, err := os2.process_exec(
|
||||
os2.Process_Desc{command=[]string{output}},
|
||||
context.allocator,
|
||||
)
|
||||
defer delete(stdout)
|
||||
defer delete(stderr)
|
||||
if len(stdout) > 0 {fmt.print(string(stdout))}
|
||||
if len(stderr) > 0 {fmt.eprint(string(stderr))}
|
||||
if err != nil {
|
||||
fmt.eprintln("failed to run test executable:", err)
|
||||
return 2
|
||||
}
|
||||
return 0 if state.exit_code == 0 else 1
|
||||
}
|
||||
|
||||
valid_output_name :: proc(name: string) -> bool {
|
||||
return len(name) > 0 && name != "." && name != ".." &&
|
||||
!strings.contains(name, "/") && !strings.contains(name, "\\")
|
||||
|
||||
@@ -1386,9 +1386,20 @@ build_symbol_indexes :: proc(checker: ^Checker) {
|
||||
}
|
||||
slice.sort_by(checker.global_index, global_index_less)
|
||||
|
||||
checker.import_index = make([]Import_Index_Entry, len(checker.ast_module.imports), checker.allocator)
|
||||
import_count := 0
|
||||
for import_item in checker.ast_module.imports {
|
||||
if !import_item.test_only {
|
||||
import_count += 1
|
||||
}
|
||||
}
|
||||
checker.import_index = make([]Import_Index_Entry, import_count, checker.allocator)
|
||||
import_index := 0
|
||||
for import_item, id in checker.ast_module.imports {
|
||||
checker.import_index[id] = Import_Index_Entry{scope=import_item.file, name=import_item.alias, id=ast.import_id(id)}
|
||||
if import_item.test_only {
|
||||
continue
|
||||
}
|
||||
checker.import_index[import_index] = Import_Index_Entry{scope=import_item.file, name=import_item.alias, id=ast.import_id(id)}
|
||||
import_index += 1
|
||||
}
|
||||
slice.sort_by(checker.import_index, import_index_less)
|
||||
}
|
||||
@@ -12874,7 +12885,7 @@ check :: proc(
|
||||
main_template := find_template(&checker, checker.main_symbol, 0)
|
||||
main_declarations := 0
|
||||
for function in ast_module.functions {
|
||||
if function.pkg == 0 && function.name == checker.main_symbol {
|
||||
if !function.generated && function.pkg == 0 && function.name == checker.main_symbol {
|
||||
main_declarations += 1
|
||||
}
|
||||
}
|
||||
@@ -12917,7 +12928,7 @@ check :: proc(
|
||||
delete(states, allocator)
|
||||
propagate_problems(&checker)
|
||||
for import_item in ast_module.imports {
|
||||
if import_item.valid && !import_item.used {
|
||||
if !import_item.test_only && import_item.valid && !import_item.used {
|
||||
source.addf_warning(diagnostics, import_item.span, "unused import '%s'", symbol_text(&checker, import_item.alias))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package compiler
|
||||
|
||||
import "./ast"
|
||||
import "./backend"
|
||||
import "./cimport"
|
||||
import "./checker"
|
||||
@@ -34,6 +35,7 @@ compile_package :: proc(
|
||||
selected := target.DEFAULT,
|
||||
c_options := cimport.Options{},
|
||||
project_root := "",
|
||||
mode := ast.Compile_Mode.Executable,
|
||||
) -> int {
|
||||
sources := source.init_store()
|
||||
defer source.destroy_store(&sources)
|
||||
@@ -77,12 +79,18 @@ compile_package :: proc(
|
||||
c_options,
|
||||
selected,
|
||||
project_root if len(project_root) > 0 else input_path,
|
||||
mode,
|
||||
)
|
||||
if !loaded {
|
||||
source.print_all(&diagnostics)
|
||||
fmt.eprintln("failed to load root package directory:", input_path)
|
||||
return 2
|
||||
}
|
||||
effective_root := project_root if len(project_root) > 0 else input_path
|
||||
if !prepare_tests(&ast_module, &sources, &diagnostics, &symbols, mode, effective_root) {
|
||||
source.print_all(&diagnostics)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Generated C trampolines (for `static inline` imports) must be compiled and
|
||||
// linked with the program. Write them out and add the source as a link input
|
||||
|
||||
@@ -14,6 +14,7 @@ is_identifier_continue :: proc(value: byte) -> bool {
|
||||
|
||||
keyword_kind :: proc(text: string) -> token.Kind {
|
||||
switch text {
|
||||
case "test": return .Keyword_Test
|
||||
case "func": return .Keyword_Func
|
||||
case "c_func": return .Keyword_C_Func
|
||||
case "struct": return .Keyword_Struct
|
||||
|
||||
+56
-22
@@ -26,6 +26,7 @@ State :: struct {
|
||||
c_options: cimport.Options,
|
||||
selected: target.Target,
|
||||
project_root: string,
|
||||
mode: ast.Compile_Mode,
|
||||
record_identities: [dynamic]string,
|
||||
record_types: [dynamic]types.Type,
|
||||
root_failed: bool,
|
||||
@@ -1130,7 +1131,42 @@ load_header :: proc(state: ^State, path: string, import_span: source.Span) -> as
|
||||
return pkg_id
|
||||
}
|
||||
|
||||
load_package :: proc(state: ^State, path: string, import_span: source.Span, is_root := false) -> ast.Package_Id {
|
||||
resolve_package_imports :: proc(state: ^State, pkg_id: ast.Package_Id) {
|
||||
if int(pkg_id) >= len(state.module.packages) {
|
||||
return
|
||||
}
|
||||
canonical := state.module.packages[pkg_id].path
|
||||
import_count := len(state.module.imports)
|
||||
for import_id in 0..<import_count {
|
||||
import_item := state.module.imports[import_id]
|
||||
if import_item.pkg != pkg_id || import_item.target != ast.INVALID_PACKAGE ||
|
||||
import_item.test_only && (state.mode != .Test || !state.module.packages[pkg_id].test) {
|
||||
continue
|
||||
}
|
||||
if filepath.is_abs(import_item.path) {
|
||||
state.module.imports[import_id].diagnostic = source.add(state.diagnostics, import_item.span, "absolute import paths are invalid")
|
||||
state.module.imports[import_id].valid = false
|
||||
state.module.imports[import_id].target = add_placeholder(state, import_item.path)
|
||||
continue
|
||||
}
|
||||
target_path, target_ok := resolve_import_path(state, canonical, import_item.path)
|
||||
target := load_header(state, target_path, import_item.span) if filepath.ext(import_item.path) == ".h" else
|
||||
load_package(state, target_path, import_item.span, include_tests=import_item.test_only)
|
||||
state.module.imports[import_id].target = target
|
||||
if !target_ok || target == ast.INVALID_PACKAGE || !state.module.packages[target].available {
|
||||
state.module.imports[import_id].valid = false
|
||||
}
|
||||
delete(target_path, state.allocator)
|
||||
}
|
||||
}
|
||||
|
||||
load_package :: proc(
|
||||
state: ^State,
|
||||
path: string,
|
||||
import_span: source.Span,
|
||||
is_root := false,
|
||||
include_tests := false,
|
||||
) -> ast.Package_Id {
|
||||
canonical, ok := filepath.abs(path, state.allocator)
|
||||
if !ok || !os.is_dir(path) {
|
||||
if is_root {
|
||||
@@ -1152,6 +1188,10 @@ load_package :: proc(state: ^State, path: string, import_span: source.Span, is_r
|
||||
return id
|
||||
}
|
||||
if existing := find_package(state, canonical); existing != ast.INVALID_PACKAGE {
|
||||
if include_tests && !state.module.packages[existing].test {
|
||||
state.module.packages[existing].test = true
|
||||
resolve_package_imports(state, existing)
|
||||
}
|
||||
delete(canonical, state.allocator)
|
||||
return existing
|
||||
}
|
||||
@@ -1161,6 +1201,7 @@ load_package :: proc(state: ^State, path: string, import_span: source.Span, is_r
|
||||
path=canonical,
|
||||
name=symbol.intern(state.symbols, filepath.base(canonical)),
|
||||
available=true,
|
||||
test=state.mode == .Test && is_root || include_tests,
|
||||
})
|
||||
files, files_ok := read_package_files(state, canonical)
|
||||
if !files_ok {
|
||||
@@ -1204,26 +1245,7 @@ load_package :: proc(state: ^State, path: string, import_span: source.Span, is_r
|
||||
}
|
||||
os.file_info_slice_delete(files, state.allocator)
|
||||
|
||||
import_count := len(state.module.imports)
|
||||
for import_id in 0..<import_count {
|
||||
import_item := state.module.imports[import_id]
|
||||
if import_item.pkg != pkg_id || import_item.target != ast.INVALID_PACKAGE {
|
||||
continue
|
||||
}
|
||||
if filepath.is_abs(import_item.path) {
|
||||
state.module.imports[import_id].diagnostic = source.add(state.diagnostics, import_item.span, "absolute import paths are invalid")
|
||||
state.module.imports[import_id].valid = false
|
||||
state.module.imports[import_id].target = add_placeholder(state, import_item.path)
|
||||
continue
|
||||
}
|
||||
target_path, target_ok := resolve_import_path(state, canonical, import_item.path)
|
||||
target := load_header(state, target_path, import_item.span) if filepath.ext(import_item.path) == ".h" else load_package(state, target_path, import_item.span)
|
||||
state.module.imports[import_id].target = target
|
||||
if !target_ok || target == ast.INVALID_PACKAGE || !state.module.packages[target].available {
|
||||
state.module.imports[import_id].valid = false
|
||||
}
|
||||
delete(target_path, state.allocator)
|
||||
}
|
||||
resolve_package_imports(state, pkg_id)
|
||||
return pkg_id
|
||||
}
|
||||
|
||||
@@ -1248,6 +1270,9 @@ declaration_conflicts :: proc(module: ^ast.Module, pkg: ast.Package_Id, file: as
|
||||
|
||||
validate_imports :: proc(state: ^State) {
|
||||
for import_item, import_id in state.module.imports {
|
||||
if import_item.test_only {
|
||||
continue
|
||||
}
|
||||
if !symbol.is_valid(import_item.alias) && import_item.target != ast.INVALID_PACKAGE {
|
||||
state.module.imports[import_id].alias = state.module.packages[import_item.target].name
|
||||
}
|
||||
@@ -1287,7 +1312,7 @@ validate_imports :: proc(state: ^State) {
|
||||
|
||||
find_type_import :: proc(module: ^ast.Module, file: ast.File_Id, alias: symbol.Id) -> ast.Import_Id {
|
||||
for import_item, index in module.imports {
|
||||
if import_item.file == file && import_item.alias == alias {
|
||||
if !import_item.test_only && import_item.file == file && import_item.alias == alias {
|
||||
return ast.import_id(index)
|
||||
}
|
||||
}
|
||||
@@ -1879,6 +1904,7 @@ load :: proc(
|
||||
c_options := cimport.Options{},
|
||||
selected := target.DEFAULT,
|
||||
project_root_path := "",
|
||||
mode := ast.Compile_Mode.Executable,
|
||||
) -> (ast.Module, bool) {
|
||||
module := ast.init_module(allocator)
|
||||
project_root_source := project_root_path if len(project_root_path) > 0 else root_path
|
||||
@@ -1896,6 +1922,7 @@ load :: proc(
|
||||
c_options=c_options,
|
||||
selected=selected,
|
||||
project_root=project_root,
|
||||
mode=mode,
|
||||
}
|
||||
state.record_identities.allocator = allocator
|
||||
state.record_types.allocator = allocator
|
||||
@@ -1911,6 +1938,13 @@ load :: proc(
|
||||
if root != ast.Package_Id(0) && root != ast.INVALID_PACKAGE {
|
||||
state.root_failed = true
|
||||
}
|
||||
if mode == .Test {
|
||||
testing_path, testing_error := filepath.join({project_root, "std", "testing"}, allocator)
|
||||
if testing_error == nil {
|
||||
_ = load_package(&state, testing_path, source.Span{})
|
||||
delete(testing_path, allocator)
|
||||
}
|
||||
}
|
||||
validate_imports(&state)
|
||||
validate_declaration_aliases(&state)
|
||||
resolve_enum_values(&state)
|
||||
|
||||
@@ -3002,7 +3002,7 @@ decode_multiline_string :: proc(parser: ^Parser, tok: token.Token) -> string {
|
||||
return fmt.aprintf("%s", strings.to_string(builder), allocator=parser.module.allocator)
|
||||
}
|
||||
|
||||
parse_import :: proc(parser: ^Parser, alias: token.Token, start: token.Token) {
|
||||
parse_import :: proc(parser: ^Parser, alias: token.Token, start: token.Token, test_only := false) {
|
||||
skip_newlines(parser)
|
||||
path_token := current(parser)
|
||||
if path_token.kind != .String {
|
||||
@@ -3018,6 +3018,7 @@ parse_import :: proc(parser: ^Parser, alias: token.Token, start: token.Token) {
|
||||
file=parser.file,
|
||||
target=ast.INVALID_PACKAGE,
|
||||
valid=false,
|
||||
test_only=test_only,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
_ = finish_statement(parser)
|
||||
@@ -3033,13 +3034,45 @@ parse_import :: proc(parser: ^Parser, alias: token.Token, start: token.Token) {
|
||||
file=parser.file,
|
||||
target=ast.INVALID_PACKAGE,
|
||||
valid=true,
|
||||
test_only=test_only,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
_ = finish_statement(parser)
|
||||
}
|
||||
|
||||
parse_test :: proc(parser: ^Parser, name: token.Token) {
|
||||
advance(parser) // consume 'test'
|
||||
skip_newlines(parser)
|
||||
if current(parser).kind != .Left_Brace {
|
||||
source.add(parser.diagnostics, current(parser).span, "expected '{' after test name")
|
||||
_ = finish_statement(parser)
|
||||
return
|
||||
}
|
||||
body := parse_block(parser)
|
||||
append(&parser.module.functions, ast.Function{
|
||||
span=span_from(name.span, previous(parser).span),
|
||||
name=name.symbol,
|
||||
pkg=parser.pkg,
|
||||
file=parser.file,
|
||||
test=true,
|
||||
has_body=true,
|
||||
result=types.VOID,
|
||||
body=body,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
}
|
||||
|
||||
parse_top_level :: proc(parser: ^Parser) {
|
||||
hide_token, file_hidden := allow(parser, .Keyword_Hide)
|
||||
if current(parser).kind == .Keyword_Test && peek(parser).kind == .Keyword_Import {
|
||||
if file_hidden {
|
||||
source.add(parser.diagnostics, hide_token.span, "test imports cannot use 'hide'")
|
||||
}
|
||||
start := advance(parser)
|
||||
advance(parser) // consume 'import'
|
||||
parse_import(parser, token.Token{}, start, test_only=true)
|
||||
return
|
||||
}
|
||||
if current(parser).kind == .Keyword_Import {
|
||||
if file_hidden {
|
||||
source.add(parser.diagnostics, hide_token.span, "imports are already file-local and cannot use 'hide'")
|
||||
@@ -3058,6 +3091,13 @@ parse_top_level :: proc(parser: ^Parser) {
|
||||
return
|
||||
}
|
||||
name := advance(parser)
|
||||
if current(parser).kind == .Keyword_Test {
|
||||
if file_hidden {
|
||||
source.add(parser.diagnostics, hide_token.span, "test declarations cannot use 'hide'")
|
||||
}
|
||||
parse_test(parser, name)
|
||||
return
|
||||
}
|
||||
if current(parser).kind == .Colon_Colon {
|
||||
saved := parser.cursor
|
||||
advance(parser)
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
package compiler
|
||||
|
||||
import "./ast"
|
||||
import "./lexer"
|
||||
import "./parser"
|
||||
import "./source"
|
||||
import "./symbol"
|
||||
import "./types"
|
||||
import "core:fmt"
|
||||
import "core:path/filepath"
|
||||
import "core:slice"
|
||||
import "core:strings"
|
||||
|
||||
Test_Entry :: struct {
|
||||
function: ast.Function_Id,
|
||||
package_path: string,
|
||||
source_path: string,
|
||||
offset: source.Offset,
|
||||
}
|
||||
|
||||
test_entry_less :: proc(a, b: Test_Entry) -> bool {
|
||||
if a.package_path != b.package_path {
|
||||
return a.package_path < b.package_path
|
||||
}
|
||||
if a.source_path != b.source_path {
|
||||
return a.source_path < b.source_path
|
||||
}
|
||||
return a.offset < b.offset
|
||||
}
|
||||
|
||||
testing_package :: proc(module: ^ast.Module, project_root: string) -> ast.Package_Id {
|
||||
path, err := filepath.join({project_root, "std", "testing"}, module.allocator)
|
||||
if err != nil {
|
||||
return ast.INVALID_PACKAGE
|
||||
}
|
||||
defer delete(path, module.allocator)
|
||||
canonical, ok := filepath.abs(path, module.allocator)
|
||||
if !ok {
|
||||
return ast.INVALID_PACKAGE
|
||||
}
|
||||
defer delete(canonical, module.allocator)
|
||||
for pkg, index in module.packages {
|
||||
if pkg.path == canonical {
|
||||
return ast.package_id(index)
|
||||
}
|
||||
}
|
||||
return ast.INVALID_PACKAGE
|
||||
}
|
||||
|
||||
source_file_id :: proc(module: ^ast.Module, id: source.Source_Id) -> ast.File_Id {
|
||||
for file, index in module.files {
|
||||
if file.source == id {
|
||||
return ast.file_id(index)
|
||||
}
|
||||
}
|
||||
return ast.INVALID_FILE
|
||||
}
|
||||
|
||||
append_ast_expr :: proc(module: ^ast.Module, expr: ast.Expr) -> ast.Expr_Id {
|
||||
id := ast.expr_id(len(module.exprs))
|
||||
append(&module.exprs, expr)
|
||||
return id
|
||||
}
|
||||
|
||||
location_expr :: proc(
|
||||
module: ^ast.Module,
|
||||
sources: ^source.Store,
|
||||
symbols: ^symbol.Table,
|
||||
span: source.Span,
|
||||
qualifier: symbol.Id,
|
||||
) -> ast.Expr_Id {
|
||||
line, column := 1, 1
|
||||
path := "<unknown>"
|
||||
if int(span.file) < len(sources.items) {
|
||||
file := &sources.items[span.file]
|
||||
line, column = source.line_and_column(file, span.start)
|
||||
path = file.path
|
||||
}
|
||||
string_index := u64(len(module.strings))
|
||||
append(&module.strings, strings.clone(path, module.allocator))
|
||||
file_expr := append_ast_expr(module, ast.Expr{
|
||||
kind=.String, span=span, integer=string_index,
|
||||
left=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
line_expr := append_ast_expr(module, ast.Expr{
|
||||
kind=.Integer, span=span, integer=u64(line),
|
||||
left=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
column_expr := append_ast_expr(module, ast.Expr{
|
||||
kind=.Integer, span=span, integer=u64(column),
|
||||
left=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
values := []ast.Expr_Id{file_expr, line_expr, column_expr}
|
||||
names := []string{"file", "line", "column"}
|
||||
fields := make([]ast.Expr_Id, 3, module.allocator)
|
||||
for index in 0..<3 {
|
||||
fields[index] = append_ast_expr(module, ast.Expr{
|
||||
kind=.Keyed, span=span,
|
||||
name=symbol.intern(symbols, names[index]),
|
||||
left=values[index], right=ast.INVALID_EXPR,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
}
|
||||
return append_ast_expr(module, ast.Expr{
|
||||
kind=.Struct_Literal, span=span,
|
||||
qualifier=qualifier,
|
||||
name=symbol.intern(symbols, "SourceLocation"),
|
||||
args=fields,
|
||||
left=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
}
|
||||
|
||||
inject_assertion_locations :: proc(
|
||||
module: ^ast.Module,
|
||||
sources: ^source.Store,
|
||||
symbols: ^symbol.Table,
|
||||
testing_pkg: ast.Package_Id,
|
||||
) {
|
||||
expect := symbol.intern(symbols, "expect")
|
||||
expect_equal := symbol.intern(symbols, "expect_equal")
|
||||
original_count := len(module.exprs)
|
||||
for index in 0..<original_count {
|
||||
expr := &module.exprs[index]
|
||||
if expr.kind != .Call || expr.intrinsic || !symbol.is_valid(expr.qualifier) ||
|
||||
(expr.name != expect && expr.name != expect_equal) {
|
||||
continue
|
||||
}
|
||||
file := source_file_id(module, expr.span.file)
|
||||
matched := false
|
||||
for import_item in module.imports {
|
||||
if !import_item.test_only && import_item.file == file &&
|
||||
import_item.alias == expr.qualifier && import_item.target == testing_pkg {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
location := location_expr(module, sources, symbols, expr.span, expr.qualifier)
|
||||
args := make([]ast.Expr_Id, len(expr.args)+1, module.allocator)
|
||||
copy(args, expr.args)
|
||||
args[len(expr.args)] = location
|
||||
delete(expr.args, module.allocator)
|
||||
expr.args = args
|
||||
}
|
||||
}
|
||||
|
||||
write_brolang_string :: proc(builder: ^strings.Builder, value: string) {
|
||||
for byte_value in transmute([]byte)value {
|
||||
if byte_value == '\\' || byte_value == '"' {
|
||||
strings.write_byte(builder, '\\')
|
||||
}
|
||||
strings.write_byte(builder, byte_value)
|
||||
}
|
||||
}
|
||||
|
||||
append_runner :: proc(
|
||||
module: ^ast.Module,
|
||||
sources: ^source.Store,
|
||||
diagnostics: ^source.Diagnostics,
|
||||
symbols: ^symbol.Table,
|
||||
testing_pkg: ast.Package_Id,
|
||||
tests: []Test_Entry,
|
||||
) {
|
||||
builder := strings.builder_make(module.allocator)
|
||||
defer strings.builder_destroy(&builder)
|
||||
strings.write_string(&builder, "main func() i32 {\n\tfailed i32 = 0\n")
|
||||
for entry, index in tests {
|
||||
test_id := entry.function
|
||||
test := module.functions[test_id]
|
||||
pkg := module.packages[test.pkg]
|
||||
alias := fmt.tprintf("__brolang_test_%d", index)
|
||||
name := fmt.tprintf("%s.%s", filepath.base(pkg.path), symbol.resolve(symbols, test.name))
|
||||
strings.write_string(&builder, "\tif (!__brolang_testing.run(\"")
|
||||
write_brolang_string(&builder, name)
|
||||
fmt.sbprintf(&builder, "\", %s.%s)) ", alias, symbol.resolve(symbols, test.name))
|
||||
strings.write_string(&builder, "{\n\t\tfailed += 1\n\t}\n")
|
||||
}
|
||||
fmt.sbprintf(&builder, "\t__brolang_testing.summary(%d - failed, failed)\n", len(tests))
|
||||
strings.write_string(&builder, "\tif (failed != 0) return 1\n\treturn 0\n}\n")
|
||||
|
||||
runner_text := strings.to_string(builder)
|
||||
source_id := source.add_source(sources, "<brolang-test-runner>", runner_text)
|
||||
file_id := ast.file_id(len(module.files))
|
||||
append(&module.files, ast.File{source=source_id, pkg=0})
|
||||
stream := lexer.lex(&sources.items[source_id], diagnostics, symbols, module.allocator)
|
||||
defer delete(stream.items)
|
||||
parser.parse_into(&stream, &sources.items[source_id], diagnostics, module, 0, file_id)
|
||||
|
||||
append(&module.imports, ast.Import{
|
||||
alias=symbol.intern(symbols, "__brolang_testing"),
|
||||
path=strings.clone("@std/testing", module.allocator),
|
||||
pkg=0, file=file_id, target=testing_pkg,
|
||||
valid=true, used=true,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
for entry, index in tests {
|
||||
test_id := entry.function
|
||||
test := module.functions[test_id]
|
||||
append(&module.imports, ast.Import{
|
||||
alias=symbol.intern(symbols, fmt.tprintf("__brolang_test_%d", index)),
|
||||
path=strings.clone(module.packages[test.pkg].path, module.allocator),
|
||||
pkg=0, file=file_id, target=test.pkg,
|
||||
valid=true, used=true,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
prepare_tests :: proc(
|
||||
module: ^ast.Module,
|
||||
sources: ^source.Store,
|
||||
diagnostics: ^source.Diagnostics,
|
||||
symbols: ^symbol.Table,
|
||||
mode: ast.Compile_Mode,
|
||||
project_root: string,
|
||||
) -> bool {
|
||||
if mode == .Executable {
|
||||
for &function in module.functions {
|
||||
if !function.test {
|
||||
continue
|
||||
}
|
||||
function.generated = true
|
||||
for &import_item in module.imports {
|
||||
if !import_item.test_only && import_item.file == function.file {
|
||||
import_item.used = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
testing_pkg := testing_package(module, project_root)
|
||||
if testing_pkg == ast.INVALID_PACKAGE {
|
||||
source.add(diagnostics, source.Span{}, "test builds require @std/testing")
|
||||
return false
|
||||
}
|
||||
error_type := types.find_named(
|
||||
&module.type_store,
|
||||
u32(testing_pkg),
|
||||
u32(symbol.intern(symbols, "Error")),
|
||||
)
|
||||
if !types.is_enum(error_type, &module.type_store) {
|
||||
source.add(diagnostics, source.Span{}, "@std/testing must declare Error as an enum")
|
||||
return false
|
||||
}
|
||||
|
||||
tests: [dynamic]Test_Entry
|
||||
tests.allocator = module.allocator
|
||||
defer delete(tests)
|
||||
main_name := symbol.intern(symbols, "main")
|
||||
for &function, index in module.functions {
|
||||
if function.test {
|
||||
if int(function.pkg) < len(module.packages) && module.packages[function.pkg].test {
|
||||
function.result = types.VOID
|
||||
function.error = error_type
|
||||
file := module.files[function.file]
|
||||
append(&tests, Test_Entry{
|
||||
function=ast.function_id(index),
|
||||
package_path=module.packages[function.pkg].path,
|
||||
source_path=sources.items[file.source].path,
|
||||
offset=function.span.start,
|
||||
})
|
||||
} else {
|
||||
function.generated = true
|
||||
}
|
||||
} else if function.pkg == 0 && function.name == main_name {
|
||||
function.generated = true
|
||||
}
|
||||
}
|
||||
|
||||
slice.sort_by(tests[:], test_entry_less)
|
||||
inject_assertion_locations(module, sources, symbols, testing_pkg)
|
||||
append_runner(module, sources, diagnostics, symbols, testing_pkg, tests[:])
|
||||
return true
|
||||
}
|
||||
@@ -50,6 +50,7 @@ Kind :: enum u8 {
|
||||
Right_Brace,
|
||||
Comma,
|
||||
Pipe,
|
||||
Keyword_Test,
|
||||
Keyword_Func,
|
||||
Keyword_C_Func,
|
||||
Keyword_Struct,
|
||||
@@ -118,7 +119,7 @@ Kind :: enum u8 {
|
||||
}
|
||||
|
||||
is_keyword :: proc(kind: Kind) -> bool {
|
||||
return kind >= .Keyword_Func && kind <= .Keyword_C_Longdouble
|
||||
return kind >= .Keyword_Test && kind <= .Keyword_C_Longdouble
|
||||
}
|
||||
|
||||
Token :: struct {
|
||||
|
||||
Reference in New Issue
Block a user