Commit 11a0644365

Andrew Kelley <superjoe30@gmail.com>
2016-02-04 02:02:01
basic support for building a test target
1 parent afdb47c
src/all_types.hpp
@@ -36,11 +36,6 @@ enum OutType {
     OutTypeObj,
 };
 
-enum CodeGenBuildType {
-    CodeGenBuildTypeDebug,
-    CodeGenBuildTypeRelease,
-};
-
 struct ConstEnumValue {
     uint64_t tag;
     ConstExprValue *payload;
@@ -983,6 +978,7 @@ struct FnTableEntry {
     bool is_inline;
     bool internal_linkage;
     bool is_extern;
+    bool is_test;
     uint32_t ref_count; // if this is 0 we don't have to codegen it
 
     // reminder: hash tables must be initialized before use
@@ -1073,7 +1069,8 @@ struct CodeGen {
     bool link_libc;
     Buf *libc_lib_dir;
     Buf *libc_include_dir;
-    CodeGenBuildType build_type;
+    bool is_release_build;
+    bool is_test_build;
     LLVMTargetMachineRef target_machine;
     LLVMZigDIFile *dummy_di_file;
     bool is_native_target;
@@ -1087,10 +1084,11 @@ struct CodeGen {
     // there will not be a corresponding fn_defs entry.
     ZigList<FnTableEntry *> fn_protos;
     ZigList<VariableTableEntry *> global_vars;
-    ZigList<Expr *> global_const_list;
+    ZigList<AstNode *> global_const_list;
 
     OutType out_type;
     FnTableEntry *cur_fn;
+    FnTableEntry *main_fn;
     LLVMValueRef cur_ret_ptr;
     ZigList<LLVMBasicBlockRef> break_block_stack;
     ZigList<LLVMBasicBlockRef> continue_block_stack;
@@ -1103,6 +1101,7 @@ struct CodeGen {
     ErrColor err_color;
     ImportTableEntry *root_import;
     ImportTableEntry *bootstrap_import;
+    ImportTableEntry *test_runner_import;
     LLVMValueRef memcpy_fn_val;
     LLVMValueRef memset_fn_val;
     LLVMValueRef trap_fn_val;
@@ -1116,6 +1115,8 @@ struct CodeGen {
     const char **clang_argv;
     int clang_argv_len;
     ZigList<const char *> lib_dirs;
+
+    uint32_t test_fn_count;
 };
 
 struct VariableTableEntry {
src/analyze.cpp
@@ -781,6 +781,7 @@ static void resolve_function_proto(CodeGen *g, AstNode *node, FnTableEntry *fn_t
 
     bool is_cold = false;
     bool is_naked = false;
+    bool is_test = false;
 
     if (fn_proto->directives) {
         for (int i = 0; i < fn_proto->directives->length; i += 1) {
@@ -794,6 +795,9 @@ static void resolve_function_proto(CodeGen *g, AstNode *node, FnTableEntry *fn_t
                         is_naked = true;
                     } else if (buf_eql_str(attr_name, "cold")) {
                         is_cold = true;
+                    } else if (buf_eql_str(attr_name, "test")) {
+                        is_test = true;
+                        g->test_fn_count += 1;
                     } else {
                         add_node_error(g, directive_node,
                                 buf_sprintf("invalid function attribute: '%s'", buf_ptr(name)));
@@ -813,6 +817,7 @@ static void resolve_function_proto(CodeGen *g, AstNode *node, FnTableEntry *fn_t
             is_naked, is_cold);
 
     fn_table_entry->type_entry = fn_type;
+    fn_table_entry->is_test = is_test;
 
     if (fn_type->id == TypeTableEntryIdInvalid) {
         fn_proto->skip = true;
@@ -846,7 +851,7 @@ static void resolve_function_proto(CodeGen *g, AstNode *node, FnTableEntry *fn_t
     unsigned scope_line = line_number;
     bool is_definition = fn_table_entry->fn_def_node != nullptr;
     unsigned flags = 0;
-    bool is_optimized = g->build_type == CodeGenBuildTypeRelease;
+    bool is_optimized = g->is_release_build;
     LLVMZigDISubprogram *subprogram = LLVMZigCreateFunction(g->dbuilder,
         import->block_context->di_scope, buf_ptr(&fn_table_entry->symbol_name), "",
         import->di_file, line_number,
@@ -1247,10 +1252,18 @@ static void preview_fn_proto(CodeGen *g, ImportTableEntry *import,
 
     fn_table->put(proto_name, fn_table_entry);
 
-    if (!struct_type &&
-        g->bootstrap_import &&
-        import == g->root_import && buf_eql_str(proto_name, "main"))
-    {
+    bool is_main_fn = !struct_type && (import == g->root_import) && buf_eql_str(proto_name, "main");
+    if (is_main_fn) {
+        g->main_fn = fn_table_entry;
+
+        if (g->bootstrap_import && !g->is_test_build) {
+            g->bootstrap_import->fn_table.put(proto_name, fn_table_entry);
+        }
+    }
+    bool is_test_main_fn = !struct_type && (import == g->test_runner_import) && buf_eql_str(proto_name, "main");
+    if (is_test_main_fn) {
+        assert(g->bootstrap_import);
+        assert(g->is_test_build);
         g->bootstrap_import->fn_table.put(proto_name, fn_table_entry);
     }
 
@@ -1551,13 +1564,14 @@ static bool type_has_codegen_value(TypeTableEntry *type_entry) {
     }
 }
 
-static void add_global_const_expr(CodeGen *g, Expr *expr) {
+static void add_global_const_expr(CodeGen *g, AstNode *expr_node) {
+    Expr *expr = get_resolved_expr(expr_node);
     if (expr->const_val.ok &&
         type_has_codegen_value(expr->type_entry) &&
         !expr->has_global_const &&
         type_has_bits(expr->type_entry))
     {
-        g->global_const_list.append(expr);
+        g->global_const_list.append(expr_node);
         expr->has_global_const = true;
     }
 }
@@ -1924,7 +1938,7 @@ static TypeTableEntry *resolve_peer_type_compatibility(CodeGen *g, ImportTableEn
                 *child_node, expected_type, child_types[i]);
         Expr *expr = get_resolved_expr(*child_node);
         expr->type_entry = resolved_type;
-        add_global_const_expr(g, expr);
+        add_global_const_expr(g, *child_node);
     }
 
     return expected_type;
@@ -4022,7 +4036,9 @@ static TypeTableEntry *analyze_builtin_fn_call_expr(CodeGen *g, ImportTableEntry
                 if (buf_eql_str(&var_name, "is_big_endian")) {
                     return resolve_expr_const_val_as_bool(g, node, g->is_big_endian);
                 } else if (buf_eql_str(&var_name, "is_release")) {
-                    return resolve_expr_const_val_as_bool(g, node, g->build_type == CodeGenBuildTypeRelease);
+                    return resolve_expr_const_val_as_bool(g, node, g->is_release_build);
+                } else if (buf_eql_str(&var_name, "is_test")) {
+                    return resolve_expr_const_val_as_bool(g, node, g->is_test_build);
                 } else {
                     add_node_error(g, *str_node,
                         buf_sprintf("unrecognized compile variable: '%s'", buf_ptr(&var_name)));
@@ -4752,7 +4768,7 @@ static TypeTableEntry *analyze_expression(CodeGen *g, ImportTableEntry *import,
     expr->type_entry = return_type;
     node->block_context = context;
 
-    add_global_const_expr(g, expr);
+    add_global_const_expr(g, node);
 
     return resolved_type;
 }
src/codegen.cpp
@@ -28,7 +28,8 @@ CodeGen *codegen_create(Buf *root_source_dir) {
     g->primitive_type_table.init(32);
     g->unresolved_top_level_decls.init(32);
     g->fn_type_table.init(32);
-    g->build_type = CodeGenBuildTypeDebug;
+    g->is_release_build = false;
+    g->is_test_build = false;
     g->root_source_dir = root_source_dir;
     g->next_error_index = 1;
     g->error_value_count = 1;
@@ -44,8 +45,12 @@ void codegen_set_clang_argv(CodeGen *g, const char **args, int len) {
     g->clang_argv_len = len;
 }
 
-void codegen_set_build_type(CodeGen *g, CodeGenBuildType build_type) {
-    g->build_type = build_type;
+void codegen_set_is_release(CodeGen *g, bool is_release_build) {
+    g->is_release_build = is_release_build;
+}
+
+void codegen_set_is_test(CodeGen *g, bool is_test_build) {
+    g->is_test_build = is_test_build;
 }
 
 void codegen_set_is_static(CodeGen *g, bool is_static) {
@@ -1003,7 +1008,7 @@ static LLVMValueRef gen_prefix_op_expr(CodeGen *g, AstNode *node) {
                 assert(expr_type->id == TypeTableEntryIdErrorUnion);
                 TypeTableEntry *child_type = expr_type->data.error.child_type;
 
-                if (g->build_type != CodeGenBuildTypeRelease) {
+                if (!g->is_release_build) {
                     LLVMValueRef err_val;
                     if (type_has_bits(child_type)) {
                         add_debug_source_node(g, node);
@@ -1044,7 +1049,7 @@ static LLVMValueRef gen_prefix_op_expr(CodeGen *g, AstNode *node) {
                 assert(expr_type->id == TypeTableEntryIdMaybe);
                 TypeTableEntry *child_type = expr_type->data.maybe.child_type;
 
-                if (g->build_type != CodeGenBuildTypeRelease) {
+                if (!g->is_release_build) {
                     add_debug_source_node(g, node);
                     LLVMValueRef cond_val;
                     if (child_type->id == TypeTableEntryIdPointer ||
@@ -1977,7 +1982,7 @@ static LLVMValueRef gen_container_init_expr(CodeGen *g, AstNode *node) {
     } else if (type_entry->id == TypeTableEntryIdUnreachable) {
         assert(node->data.container_init_expr.entries.length == 0);
         add_debug_source_node(g, node);
-        if (g->build_type != CodeGenBuildTypeRelease) {
+        if (!g->is_release_build) {
             LLVMBuildCall(g->builder, g->trap_fn_val, nullptr, 0, "");
         }
         return LLVMBuildUnreachable(g->builder);
@@ -2233,7 +2238,7 @@ static LLVMValueRef gen_var_decl_raw(CodeGen *g, AstNode *source_node, AstNodeVa
                 }
             }
         }
-        if (!ignore_uninit && g->build_type != CodeGenBuildTypeRelease) {
+        if (!ignore_uninit && !g->is_release_build) {
             TypeTableEntry *isize = g->builtin_types.entry_isize;
             uint64_t size_bytes = LLVMStoreSizeOfType(g->target_data_ref, variable->type->type_ref);
             uint64_t align_bytes = get_memcpy_align(g, variable->type);
@@ -2645,7 +2650,8 @@ static LLVMValueRef gen_const_val(CodeGen *g, TypeTableEntry *type_entry, ConstE
 
 static void gen_const_globals(CodeGen *g) {
     for (int i = 0; i < g->global_const_list.length; i += 1) {
-        Expr *expr = g->global_const_list.at(i);
+        AstNode *expr_node = g->global_const_list.at(i);
+        Expr *expr = get_resolved_expr(expr_node);
         ConstExprValue *const_val = &expr->const_val;
         assert(const_val->ok);
         TypeTableEntry *type_entry = expr->type_entry;
@@ -2680,6 +2686,24 @@ static void delete_unused_builtin_fns(CodeGen *g) {
     }
 }
 
+static bool skip_fn_codegen(CodeGen *g, FnTableEntry *fn_entry) {
+    if (g->is_test_build) {
+        if (fn_entry->is_test) {
+            return false;
+        }
+        if (fn_entry == g->main_fn) {
+            return true;
+        }
+        return fn_entry->ref_count == 0;
+    }
+
+    if (fn_entry->is_test) {
+        return true;
+    }
+
+    return fn_entry->ref_count == 0;
+}
+
 static void do_code_gen(CodeGen *g) {
     assert(!g->errors.length);
 
@@ -2731,12 +2755,19 @@ static void do_code_gen(CodeGen *g) {
         var->value_ref = global_value;
     }
 
+    LLVMValueRef *test_fn_vals = nullptr;
+    uint32_t next_test_index = 0;
+    if (g->is_test_build) {
+        test_fn_vals = allocate<LLVMValueRef>(g->test_fn_count);
+    }
+
     // Generate function prototypes
     for (int fn_proto_i = 0; fn_proto_i < g->fn_protos.length; fn_proto_i += 1) {
         FnTableEntry *fn_table_entry = g->fn_protos.at(fn_proto_i);
-        if (fn_table_entry->ref_count == 0) {
+        if (skip_fn_codegen(g, fn_table_entry)) {
             // huge time saver
             LLVMDeleteFunction(fn_table_entry->fn_value);
+            fn_table_entry->fn_value = nullptr;
             continue;
         }
 
@@ -2785,12 +2816,44 @@ static void do_code_gen(CodeGen *g) {
             }
         }
 
+        if (fn_table_entry->is_test) {
+            test_fn_vals[next_test_index] = fn_table_entry->fn_value;
+            next_test_index += 1;
+        }
+    }
+
+    // Generate the list of test function pointers.
+    if (g->is_test_build) {
+        assert(g->test_fn_count > 0);
+        assert(next_test_index == g->test_fn_count);
+
+        {
+            LLVMValueRef test_fn_array_val = LLVMConstArray(LLVMTypeOf(test_fn_vals[0]),
+                    test_fn_vals, g->test_fn_count);
+            LLVMValueRef global_value = LLVMAddGlobal(g->module,
+                    LLVMTypeOf(test_fn_array_val), "zig_test_fn_list");
+            LLVMSetInitializer(global_value, test_fn_array_val);
+            LLVMSetLinkage(global_value, LLVMExternalLinkage);
+            LLVMSetGlobalConstant(global_value, true);
+            LLVMSetUnnamedAddr(global_value, true);
+        }
+
+        {
+            LLVMValueRef test_fn_count_val = LLVMConstInt(g->builtin_types.entry_isize->type_ref,
+                    g->test_fn_count, false);
+            LLVMValueRef global_value = LLVMAddGlobal(g->module,
+                    LLVMTypeOf(test_fn_count_val), "zig_test_fn_count");
+            LLVMSetInitializer(global_value, test_fn_count_val);
+            LLVMSetLinkage(global_value, LLVMExternalLinkage);
+            LLVMSetGlobalConstant(global_value, true);
+            LLVMSetUnnamedAddr(global_value, true);
+        }
     }
 
     // Generate function definitions.
     for (int fn_i = 0; fn_i < g->fn_defs.length; fn_i += 1) {
         FnTableEntry *fn_table_entry = g->fn_defs.at(fn_i);
-        if (fn_table_entry->ref_count == 0) {
+        if (skip_fn_codegen(g, fn_table_entry)) {
             // huge time saver
             continue;
         }
@@ -3297,8 +3360,7 @@ static void init(CodeGen *g, Buf *source_path) {
     char *native_cpu = LLVMZigGetHostCPUName();
     char *native_features = LLVMZigGetNativeFeatures();
 
-    LLVMCodeGenOptLevel opt_level = (g->build_type == CodeGenBuildTypeDebug) ?
-        LLVMCodeGenLevelNone : LLVMCodeGenLevelAggressive;
+    LLVMCodeGenOptLevel opt_level = g->is_release_build ? LLVMCodeGenLevelAggressive : LLVMCodeGenLevelNone;
 
     LLVMRelocMode reloc_mode = g->is_static ? LLVMRelocStatic : LLVMRelocPIC;
 
@@ -3321,7 +3383,7 @@ static void init(CodeGen *g, Buf *source_path) {
 
 
     Buf *producer = buf_sprintf("zig %s", ZIG_VERSION_STRING);
-    bool is_optimized = g->build_type == CodeGenBuildTypeRelease;
+    bool is_optimized = g->is_release_build;
     const char *flags = "";
     unsigned runtime_version = 0;
     g->compile_unit = LLVMZigCreateCompileUnit(g->dbuilder, LLVMZigLang_DW_LANG_C99(),
@@ -3611,14 +3673,10 @@ void codegen_add_root_code(CodeGen *g, Buf *src_dir, Buf *src_basename, Buf *sou
                 buf_sprintf("missing export declaration and export type not provided"));
     }
 
-    if (!g->link_libc) {
+    if (!g->link_libc && !g->is_test_build) {
         if (g->have_exported_main && (g->out_type == OutTypeObj || g->out_type == OutTypeExe)) {
             g->bootstrap_import = add_special_code(g, "bootstrap.zig");
         }
-
-        if (g->out_type == OutTypeExe) {
-            add_special_code(g, "builtin.zig");
-        }
     }
 
     if (g->verbose) {
@@ -3677,6 +3735,8 @@ static void to_c_type(CodeGen *g, AstNode *type_node, Buf *out_buf) {
 }
 
 static void generate_h_file(CodeGen *g) {
+    assert(!g->is_test_build);
+
     Buf *h_file_out_path = buf_sprintf("%s.h", buf_ptr(g->root_out_name));
     FILE *out_h = fopen(buf_ptr(h_file_out_path), "wb");
     if (!out_h)
@@ -3768,8 +3828,38 @@ static const char *get_libc_file(CodeGen *g, const char *file) {
     return buf_ptr(out_buf);
 }
 
+static Buf *build_o(CodeGen *parent_gen, const char *oname) {
+    Buf *source_basename = buf_sprintf("%s.zig", oname);
+    Buf *std_dir_path = buf_create_from_str(ZIG_STD_DIR);
+
+    CodeGen *child_gen = codegen_create(std_dir_path);
+    codegen_set_is_release(child_gen, parent_gen->is_release_build);
+
+    codegen_set_strip(child_gen, parent_gen->strip_debug_symbols);
+    codegen_set_is_static(child_gen, parent_gen->is_static);
+
+    codegen_set_out_type(child_gen, OutTypeObj);
+    codegen_set_out_name(child_gen, buf_create_from_str(oname));
+
+    codegen_set_verbose(child_gen, parent_gen->verbose);
+    codegen_set_errmsg_color(child_gen, parent_gen->err_color);
+
+    Buf *full_path = buf_alloc();
+    os_path_join(std_dir_path, source_basename, full_path);
+    Buf source_code = BUF_INIT;
+    if (os_fetch_file_path(full_path, &source_code)) {
+        zig_panic("unable to fetch file: %s\n", buf_ptr(full_path));
+    }
+
+    codegen_add_root_code(child_gen, std_dir_path, source_basename, &source_code);
+    Buf *o_out = buf_sprintf("%s.o", oname);
+    codegen_link(child_gen, buf_ptr(o_out));
+
+    return o_out;
+}
+
 void codegen_link(CodeGen *g, const char *out_file) {
-    bool is_optimized = (g->build_type == CodeGenBuildTypeRelease);
+    bool is_optimized = g->is_release_build;
     if (is_optimized) {
         if (g->verbose) {
             fprintf(stderr, "\nOptimization:\n");
@@ -3788,6 +3878,7 @@ void codegen_link(CodeGen *g, const char *out_file) {
     }
 
     if (!out_file) {
+        assert(g->root_out_name);
         out_file = buf_ptr(g->root_out_name);
     }
 
@@ -3821,6 +3912,7 @@ void codegen_link(CodeGen *g, const char *out_file) {
         return;
     }
 
+
     // invoke `ld`
     ZigList<const char *> args = {0};
     const char *crt1o;
@@ -3871,6 +3963,16 @@ void codegen_link(CodeGen *g, const char *out_file) {
         args.append(get_libc_file(g, "crtn.o"));
     }
 
+    if (g->is_test_build) {
+        Buf *test_runner_o_path = build_o(g, "test_runner");
+        args.append(buf_ptr(test_runner_o_path));
+    }
+
+    if (!g->link_libc && (g->out_type == OutTypeExe || g->out_type == OutTypeLib)) {
+        Buf *builtin_o_path = build_o(g, "builtin");
+        args.append(buf_ptr(builtin_o_path));
+    }
+
     for (int i = 0; i < g->lib_dirs.length; i += 1) {
         const char *lib_dir = g->lib_dirs.at(i);
         args.append("-L");
src/codegen.hpp
@@ -16,7 +16,9 @@
 CodeGen *codegen_create(Buf *root_source_dir);
 
 void codegen_set_clang_argv(CodeGen *codegen, const char **args, int len);
-void codegen_set_build_type(CodeGen *codegen, CodeGenBuildType build_type);
+void codegen_set_is_release(CodeGen *codegen, bool is_release);
+void codegen_set_is_test(CodeGen *codegen, bool is_test);
+
 void codegen_set_is_static(CodeGen *codegen, bool is_static);
 void codegen_set_strip(CodeGen *codegen, bool strip);
 void codegen_set_verbose(CodeGen *codegen, bool verbose);
src/main.cpp
@@ -17,6 +17,7 @@ static int usage(const char *arg0) {
     fprintf(stderr, "Usage: %s [command] [options]\n"
         "Commands:\n"
         "  build                     create executable, object, or library from target\n"
+        "  test                      create and run a test build\n"
         "  version                   print version number and exit\n"
         "  parseh                    convert a c header file to zig extern declarations\n"
         "Options:\n"
@@ -40,6 +41,7 @@ static int usage(const char *arg0) {
 enum Cmd {
     CmdInvalid,
     CmdBuild,
+    CmdTest,
     CmdVersion,
     CmdParseH,
 };
@@ -49,7 +51,7 @@ int main(int argc, char **argv) {
     Cmd cmd = CmdInvalid;
     const char *in_file = nullptr;
     const char *out_file = nullptr;
-    bool release = false;
+    bool is_release_build = false;
     bool strip = false;
     bool is_static = false;
     OutType out_type = OutTypeUnknown;
@@ -67,7 +69,7 @@ int main(int argc, char **argv) {
 
         if (arg[0] == '-') {
             if (strcmp(arg, "--release") == 0) {
-                release = true;
+                is_release_build = true;
             } else if (strcmp(arg, "--strip") == 0) {
                 strip = true;
             } else if (strcmp(arg, "--static") == 0) {
@@ -127,6 +129,8 @@ int main(int argc, char **argv) {
                 cmd = CmdVersion;
             } else if (strcmp(arg, "parseh") == 0) {
                 cmd = CmdParseH;
+            } else if (strcmp(arg, "test") == 0) {
+                cmd = CmdTest;
             } else {
                 fprintf(stderr, "Unrecognized command: %s\n", arg);
                 return usage(arg0);
@@ -135,6 +139,7 @@ int main(int argc, char **argv) {
             switch (cmd) {
                 case CmdBuild:
                 case CmdParseH:
+                case CmdTest:
                     if (!in_file) {
                         in_file = arg;
                     } else {
@@ -152,6 +157,7 @@ int main(int argc, char **argv) {
     switch (cmd) {
     case CmdBuild:
     case CmdParseH:
+    case CmdTest:
         {
             if (!in_file)
                 return usage(arg0);
@@ -178,14 +184,22 @@ int main(int argc, char **argv) {
             }
 
             CodeGen *g = codegen_create(&root_source_dir);
-            codegen_set_build_type(g, release ? CodeGenBuildTypeRelease : CodeGenBuildTypeDebug);
+            codegen_set_is_release(g, is_release_build);
+            codegen_set_is_test(g, cmd == CmdTest);
+
             codegen_set_clang_argv(g, clang_argv.items, clang_argv.length);
             codegen_set_strip(g, strip);
             codegen_set_is_static(g, is_static);
-            if (out_type != OutTypeUnknown)
+            if (out_type != OutTypeUnknown) {
                 codegen_set_out_type(g, out_type);
-            if (out_name)
+            } else if (cmd == CmdTest) {
+                codegen_set_out_type(g, OutTypeExe);
+            }
+            if (out_name) {
                 codegen_set_out_name(g, buf_create_from_str(out_name));
+            } else if (cmd == CmdTest) {
+                codegen_set_out_name(g, buf_create_from_str("test"));
+            }
             if (libc_lib_dir)
                 codegen_set_libc_lib_dir(g, buf_create_from_str(libc_lib_dir));
             if (libc_include_dir)
@@ -205,6 +219,17 @@ int main(int argc, char **argv) {
                 codegen_parseh(g, &root_source_dir, &root_source_name, &root_source_code);
                 codegen_render_ast(g, stdout, 4);
                 return EXIT_SUCCESS;
+            } else if (cmd == CmdTest) {
+                codegen_add_root_code(g, &root_source_dir, &root_source_name, &root_source_code);
+                codegen_link(g, "./test");
+                ZigList<const char *> args = {0};
+                int return_code;
+                os_spawn_process("./test", args, &return_code);
+                if (return_code != 0) {
+                    fprintf(stderr, "\nTests failed. Use the following command to reproduce the failure:\n");
+                    fprintf(stderr, "./test\n");
+                }
+                return return_code;
             } else {
                 zig_unreachable();
             }
src/os.cpp
@@ -17,25 +17,24 @@
 #include <fcntl.h>
 #include <limits.h>
 
-void os_spawn_process(const char *exe, ZigList<const char *> &args, bool detached) {
+void os_spawn_process(const char *exe, ZigList<const char *> &args, int *return_code) {
     pid_t pid = fork();
     if (pid == -1)
         zig_panic("fork failed");
-    if (pid != 0)
-        return;
-    if (detached) {
-        if (setsid() == -1)
-            zig_panic("process detach failed");
-    }
-
-    const char **argv = allocate<const char *>(args.length + 2);
-    argv[0] = exe;
-    argv[args.length + 1] = nullptr;
-    for (int i = 0; i < args.length; i += 1) {
-        argv[i + 1] = args.at(i);
+    if (pid == 0) {
+        // child
+        const char **argv = allocate<const char *>(args.length + 2);
+        argv[0] = exe;
+        argv[args.length + 1] = nullptr;
+        for (int i = 0; i < args.length; i += 1) {
+            argv[i + 1] = args.at(i);
+        }
+        execvp(exe, const_cast<char * const *>(argv));
+        zig_panic("execvp failed: %s", strerror(errno));
+    } else {
+        // parent
+        waitpid(pid, return_code, 0);
     }
-    execvp(exe, const_cast<char * const *>(argv));
-    zig_panic("execvp failed: %s", strerror(errno));
 }
 
 static int read_all_fd_stream(int fd, Buf *out_buf) {
src/os.hpp
@@ -13,7 +13,7 @@
 
 #include <stdio.h>
 
-void os_spawn_process(const char *exe, ZigList<const char *> &args, bool detached);
+void os_spawn_process(const char *exe, ZigList<const char *> &args, int *return_code);
 void os_exec_process(const char *exe, ZigList<const char *> &args,
         int *return_code, Buf *out_stderr, Buf *out_stdout);
 
std/test_runner.zig
@@ -0,0 +1,45 @@
+import "std.zig";
+
+/*
+struct TestFn {
+    name: []u8,
+    func: extern fn(),
+}
+
+extern var test_fn_list: []TestFn;
+*/
+
+extern var zig_test_fn_count: isize;
+
+// TODO make this a slice of structs
+extern var zig_test_fn_list: [99999999]extern fn();
+
+pub fn main(args: [][]u8) -> %void {
+    var i : isize = 0;
+    while (i < zig_test_fn_count) {
+        %%stderr.print_str("Test ");
+        // TODO get rid of the isize
+        %%stderr.print_i64(i + isize(1));
+        %%stderr.print_str("/");
+        %%stderr.print_i64(zig_test_fn_count);
+        %%stderr.print_str(" ");
+        /*
+        %%stderr.print_str(test_fn.name);
+        */
+        %%stderr.print_str("...");
+
+/*
+        // TODO support calling function pointers as fields directly
+        const fn_ptr = test_fn.func;
+        fn_ptr();
+        */
+
+        const test_fn = zig_test_fn_list[i];
+        test_fn();
+
+        %%stderr.print_str("OK\n");
+        %%stderr.flush();
+
+        i += 1;
+    }
+}
CMakeLists.txt
@@ -122,6 +122,7 @@ set(C_HEADERS
 set(ZIG_STD_SRC
     "${CMAKE_SOURCE_DIR}/std/bootstrap.zig"
     "${CMAKE_SOURCE_DIR}/std/builtin.zig"
+    "${CMAKE_SOURCE_DIR}/std/test_runner.zig"
     "${CMAKE_SOURCE_DIR}/std/std.zig"
     "${CMAKE_SOURCE_DIR}/std/syscall.zig"
     "${CMAKE_SOURCE_DIR}/std/errno.zig"