diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aff1b9de2..681857f9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -338,7 +338,7 @@ jobs: ./daslang _dasroot_/dastest/dastest.das -- --color --failures-only --test ../tests || ./daslang _dasroot_/dastest/dastest.das -- --color --failures-only --isolated-mode --timeout 3600 --test ../tests ;; esac - - name: "AOT Test" + - name: "Slow Release Tests" if: matrix.cmake_preset == 'Release' run: | set -eux @@ -352,6 +352,10 @@ jobs: *) cd bin ./test_aot -use-aot _dasroot_/dastest/dastest.das -- --use-aot --color --failures-only --test ../tests + + # Run coverage only in Release since it is slow. + ./daslang _dasroot_/dastest/dastest.das -- --cov-path coverage.lcov --color --test ../tests --timeout 900 + ./daslang _dasroot_/utils/dascov/main.das -- coverage.lcov --exclude tests/ ;; esac diff --git a/CMakeLists.txt b/CMakeLists.txt index bc927bfe4..1932824eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,8 +100,6 @@ MACRO(DAS_AOT_EXT input_files genList mainTarget dasAotTool dasAotToolArg) set(command_args "") list(LENGTH input_files num_files) - message(STATUS "DAS_AOT_EXT called on ${mainTarget} with files number: ${num_files}") - foreach(input ${input_files}) get_filename_component(input_src ${input} ABSOLUTE) get_filename_component(input_dir ${input_src} DIRECTORY) diff --git a/daslib/coverage.das b/daslib/coverage.das index 1a2e9eded..c89d0d506 100644 --- a/daslib/coverage.das +++ b/daslib/coverage.das @@ -3,28 +3,42 @@ * To make it work you should add coverage module: * `require daslib/coverage` * After executing tests/anything else add call to - * `get_report(filename) : string` + * `fill_coverage_report(filename) : string` * to get coverage report. To create html from it use: * `genhtml cov.lcov -o coverage-report --synthesize-missing --ignore-errors source` */ options gen2 -module coverage shared public +// `inscope` is necessary, so all modules will include it. +module coverage shared !inscope //! Code coverage instrumentation macro. //! -//! Automatically inserts coverage tracking calls at every block entry point. -//! After execution, call `get_report` to produce output in LCOV format, +//! Automatically inserts coverage tracking calls at every block entry point, +//! function entry, and branch (if/else) decision point. +//! After execution, call `fill_coverage_report` to produce output in LCOV format, //! which can be converted to HTML via ``genhtml``. require ast +require fio require rtti +require strings + require daslib/ast_boost require daslib/templates_boost -require daslib/defer -require daslib/macro_boost -struct FileCov { +let private builtin_files = fixed_array("fio.das", "ast.das", "builtin.das", "debugger.das", "network.das", "rtti.das") + +def private resolve_file_path(name : string) : string { + for (bf in builtin_files) { + if (name == bf) { + return "{get_das_root()}/src/builtin/{name}" + } + } + return name +} + +struct LineCov { //! Per-file coverage data storing line hit counts. lines : table @@ -34,67 +48,407 @@ struct FileCov { } } -var coverageData = new table(); //! Global table storing per-file line coverage hit counts. -var checkData = new table>(); //! Global table tracking which lines have already been instrumented. +struct FuncCov { + //! Per-function coverage entry storing line number, hit count, and per-line coverage. + line : uint + hits : uint + line_cov = LineCov() +} + +struct BranchCov { + //! Per-branch coverage entry storing line, block id, branch id and hit count. + line : uint + block_num : uint + branch_num : uint + hits : uint +} + +struct CoverageData { + line_cov = LineCov() // Line coverage. + func_cov : table // Function name -> Function coverage. + branch_cov : table // branch_idx -> Branch coverage. +} + +var public coverageData = table() def private single_file_report(name : string) { - var data : string; - (*coverageData) |> get(name) $(x) { - for (k, v in keys(x.lines), values(x.lines)) { - data += "DA:{k |> int},{v |> int}\n" + var fnFound = 0 + var fnHit = 0 + let fnData = build_string() <| $(writer) { + coverageData |> get(name) $(funcs) { + for (fname, entry in keys(funcs.func_cov), values(funcs.func_cov)) { + writer |> write("FN:{entry.line |> int},{fname}\n") + } + for (fname, entry in keys(funcs.func_cov), values(funcs.func_cov)) { + writer |> write("FNDA:{entry.hits |> int},{fname}\n") + fnFound += 1 + if (entry.hits > 0u) { + fnHit += 1 + } + } + } + } + var brFound = 0 + var brHit = 0 + let brData = build_string() <| $(writer) { + coverageData |> get(name) $(branches) { + for (br in values(branches.branch_cov)) { + var taken : string + if (br.hits == 0u) { + taken = "-" + } else { + taken = "{br.hits |> int}" + } + writer |> write("BRDA:{br.line |> int},{br.block_num |> int},{br.branch_num |> int},{taken}\n") + brFound += 1 + if (br.hits > 0u) { + brHit += 1 + } + } + } + } + let data = build_string() <| $(writer) { + coverageData |> get(name) $(x) { + for (k, v in keys(x.line_cov.lines), values(x.line_cov.lines)) { + writer |> write("DA:{k |> int},{v |> int}\n") + } } - }; - return "\nTN:\nSF:{name}\n{data}end_of_record\n" // lcov format + } + return "\nTN:\nSF:{name}\n{fnData}FNF:{fnFound}\nFNH:{fnHit}\n{brData}BRF:{brFound}\nBRH:{brHit}\n{data}end_of_record\n" } -def public get_report(name : string = "") : string { +def get_report(name : string = "") : string { //! Returns code coverage report in lcov format for the specified file, or all files if name is empty. if (name |> empty) { var res = "" - for (k in keys(*coverageData)) { + for (k in keys(coverageData)) { res += single_file_report(k) } return res } else { - return single_file_report(name); + return single_file_report(name) + } +} + +struct FileSummary { + file : string + lineHits : uint + lineMissed : uint + funcHits : uint + funcMissed : uint + branchHits : uint + branchMissed : uint +} + +def get_summary(cov_data) : array { + //! Returns per-file coverage summary with hit/missed counts for lines, functions, and branches. + var res : array + for (file, data in keys(cov_data), values(cov_data)) { + var s = FileSummary(file = string(file)) + for (v in values(data.line_cov.lines)) { + if (v > 0u) { + s.lineHits += 1u + } else { + s.lineMissed += 1u + } + } + for (fc in values(data.func_cov)) { + if (fc.hits > 0u) { + s.funcHits += 1u + } else { + s.funcMissed += 1u + } + } + for (br in values(data.branch_cov)) { + if (br.hits > 0u) { + s.branchHits += 1u + } else { + s.branchMissed += 1u + } + } + res |> push(s) + } + return <- res +} + +def private pct_string(hits, total : uint) : string { + if (total == 0u) { + return "-" + } + let p = (hits * 1000u / total + 5u) / 10u + return "{p:d}%" +} + +def private summary_line(var writer : StringBuilderWriter; name : string; nameWidth : int; hits, missed : uint) { + writer |> write(name) + writer |> write_chars(' ', nameWidth - name |> length) + writer |> write("{hits:d}") + writer |> write_chars(' ', 10 - "{hits:d}" |> length) + writer |> write("{missed:d}") + writer |> write_chars(' ', 10 - "{missed:d}" |> length) + let total = hits + missed + let pct = pct_string(hits, total) + writer |> write(pct) + writer |> write("\n") +} + +def public register_coverage_lines(file : string; lines : array) { + //! Registers lines as instrumented with 0 hit count (for DA:line,0 in LCOV). + coverageData |> get_with_default(file) <| $(var cov) { + for (line in lines) { + cov.line_cov.lines |> insert(line, 0u) + } } } -def public add_coverage(file : string; line : uint) { - //! Records a coverage hit for the given file and line number. - (*coverageData) |> get_with_default(file) <| $(var cov) { cov.lines |> modify(line) <| $(x : uint&) => x + 1u; } +def public register_coverage_func_lines(file : string; fname : string; lines : array) { + //! Registers per-function lines with 0 hit count so uncalled functions show missed lines. + coverageData |> get_with_default(file) <| $(var cov) { + cov.func_cov |> get_with_default(fname) <| $(var fc) { + for (line in lines) { + fc.line_cov.lines |> insert(line, 0u) + } + } + } +} + +def public register_coverage_functions(file : string; names : array; lines : array) { + //! Registers functions with 0 hit count for FN/FNDA records in LCOV. + coverageData |> get_with_default(file) <| $(var funcs) { + for (i in range(names |> length)) { + funcs.func_cov |> get_with_default(names[i]) <| $(var fc) { + fc.line = lines[i] + } + } + } +} + +def public register_coverage_branches(file : string; blines : array; block_nums : array; branch_nums : array) { + //! Registers branches with 0 hit count for BRDA records in LCOV. + coverageData |> get_with_default(file) <| $(var branches) { + for (i in range(blines |> length)) { + branches.branch_cov |> insert(i |> uint, BranchCov(line = blines[i], block_num = block_nums[i], branch_num = branch_nums[i], hits = 0u)) + } + } +} + +def public add_line_coverage(file : string; fname : string; line : uint) { + //! Records a coverage hit for the given file, function, and line number. + coverageData |> get_with_default(file) <| $(var cov) { + cov.line_cov.insert(line) + cov.func_cov |> get_with_default(fname) <| $(var fc) { + fc.line_cov.insert(line) + } + } +} + +def public add_func_coverage(file : string; name : string) { + //! Records a coverage hit for the given file and function name. + coverageData |> get_with_default(file) <| $(var funcs) { + funcs.func_cov |> get_with_default(name) <| $(var fc) { + fc.hits += 1u + } + } +} + +def public add_branch_coverage(file : string; branch_idx : uint) { + //! Records a coverage hit for the given file and branch index. + coverageData |> get_with_default(file) <| $(var branches) { + branches.branch_cov |> modify(uint(branch_idx)) <| $(value) { + var res = value + res.hits += 1u + return res + } + } } +// compile-time data for function and branch registration +struct RegisterData { + allFunctions : table + allBranches : array> + branchCounter : uint + funcLines : table> // fname -> set of lines +} + +var registration_data = table() + class CoverageMacro : AstVisitor { - //! AST visitor that instruments expression blocks with coverage tracking calls. + //! AST visitor that instruments expression blocks, function entries, and branches with coverage tracking calls. astChanged : bool = false @do_not_delete func : Function? def override preVisitFunction(var fun : FunctionPtr) { - //! Stores a pointer to the current function being visited. + //! Stores a pointer to the current function being visited and records function info for coverage. func = get_ptr(fun) + if (fun.flags.generated || fun.flags.init || fun.flags.builtIn) { + return + } + if (fun.at.fileInfo == null) { + return + } + let file = resolve_file_path(string(fun.at.fileInfo.name)) + let fname = string(fun.name) + let line = fun.at.line + registration_data |> get_with_default(file) <| $(var file_reg) { + if (!(file_reg.allFunctions |> key_exists(fname))) { + file_reg.allFunctions |> insert(fname, line) + } + } + } + + def override canVisitFunction(fun : Function?) : bool { + if (fun.flags.init || fun.flags.builtIn) { + return false + } + for (ann in fun.annotations) { + if (ann.annotation.name != "marker") { + continue + } + for (arg in ann.arguments) { + if (arg.name == "no_coverage") { + return false + } + } + } + return true + } + + def override preVisitFunctionBody(fun : FunctionPtr; expr : ExpressionPtr) { + //! Inserts function coverage call at the start of each function body. + if (fun.flags.generated || fun.flags.init || fun.flags.builtIn) { + return + } + if (fun.at.fileInfo == null) { + return + } + if (!(expr is ExprBlock)) { + return + } + let file = resolve_file_path(string(fun.at.fileInfo.name)) + let fname = string(fun.name) + var blk = unsafe(reinterpret expr) + var inscope prev : array + for (ex in blk.list) { + prev.emplace(ex) + } + blk.list |> clear() + blk.list |> emplace_new <| qmacro(_::add_func_coverage($v(file), $v(fname))) + for (el in range(prev |> length)) { + blk.list |> emplace <| prev[el] + } + astChanged = true + func.not_inferred() + } + + def override visitExprIfThenElse(var ifte : smart_ptr) : ExpressionPtr { + //! Instruments if/else branches with branch coverage tracking calls. + // if (ifte.isStatic) { + // return <- ifte + // } + if (ifte.at.fileInfo == null) { + return <- ifte + } + let file = resolve_file_path(string(ifte.at.fileInfo.name)) + if (file |> ends_with("coverage.das")) { + return <- ifte + } + let line = ifte.at.line + var block_num : uint + registration_data |> get_with_default(file) <| $(var file_reg) { + block_num = file_reg.branchCounter + file_reg.branchCounter += 1u + } + var true_idx : uint + var false_idx : uint + registration_data |> get_with_default(file) <| $(var file_reg) { + true_idx = uint(file_reg.allBranches |> length) + file_reg.allBranches |> push(tuple(line, block_num, 0u)) + + false_idx = uint(file_reg.allBranches |> length) + file_reg.allBranches |> push(tuple(line, block_num, 1u)) + } + // instrument true branch + if (ifte.if_true != null && ifte.if_true is ExprBlock) { + var true_blk = unsafe(reinterpret ifte.if_true) + var inscope prevTrue : array + for (ex in true_blk.list) { + prevTrue.emplace(ex) + } + true_blk.list |> clear() + true_blk.list |> emplace_new <| qmacro(_::add_branch_coverage($v(file), $v(true_idx))) + for (el in range(prevTrue |> length)) { + true_blk.list |> emplace <| prevTrue[el] + } + } + // instrument false branch + if (ifte.if_false != null && ifte.if_false is ExprBlock) { + var false_blk = unsafe(reinterpret ifte.if_false) + var inscope prevFalse : array + for (ex in false_blk.list) { + prevFalse.emplace(ex) + } + false_blk.list |> clear() + false_blk.list |> emplace_new <| qmacro(_::add_branch_coverage($v(file), $v(false_idx))) + for (el in range(prevFalse |> length)) { + false_blk.list |> emplace <| prevFalse[el] + } + } elif (ifte.if_false == null) { + // no else branch — create one with just the branch coverage call + var inscope elseBlk <- new ExprBlock(at = ifte.at) + elseBlk.list |> emplace_new <| qmacro(_::add_branch_coverage($v(file), $v(false_idx))) + move(ifte.if_false) <| elseBlk + } + astChanged = true + func.not_inferred() + return <- ifte } def override visitExprBlock(var blk : smart_ptr) : ExpressionPtr { //! Instruments expression blocks with coverage tracking calls. - if ((*checkData)[string(blk.at.fileInfo.name)] |> key_exists(blk.at.line)) { - return <- blk; + if (blk.at.fileInfo == null) { + return <- blk } - (*checkData)[string(blk.at.fileInfo.name)] |> insert(blk.at.line); astChanged = true func.not_inferred() - var inscope prev : array; + var inscope prev : array for (ex in blk.list) { prev.emplace(ex) } - blk.list |> clear(); - for (el in range(prev |> length())) { - blk.list |> emplace_new <| qmacro(_::add_coverage($v(string(prev[el].at.fileInfo.name)), $v(prev[el].at.line))); - blk.list |> emplace <| prev[el] + blk.list |> clear() + for (el in range(prev |> length)) { + // Todo: add linter phase to check fileInfo is never null. + if (prev[el].at.fileInfo != null) { + let file = resolve_file_path(string(prev[el].at.fileInfo.name)) + if (file |> ends_with("coverage.das")) { + blk.list |> emplace <| prev[el] + continue + } + let fname = string(func.name) + let line = prev[el].at.line + coverageData |> get_with_default(file) <| $(var data) { + data.line_cov.insert(line) + } + registration_data |> get_with_default(file) <| $(var reg) { + reg.funcLines |> get_with_default(fname) <| $(var flines) { + flines |> insert(line) + } + } + blk.list |> emplace_new <| qmacro(_::add_line_coverage($v(file), $v(fname), $v(line))) + blk.list |> emplace <| prev[el] + } } return <- blk } + + def override preVisitExprAssert(var expr : smart_ptr) { + // Once coverage added all assertions should become verify + // since side-effects added when calling coverage tools. + expr.isVerify = true + astChanged = true + func.not_inferred() + } + } //! Infer pass macro that applies coverage instrumentation to the program. @@ -103,13 +457,314 @@ class CoveragePass : AstPassMacro { //! Pass macro that instruments modules with code coverage. def override apply(prog : ProgramPtr; mod : Module?) : bool { //! Applies the coverage instrumentation visitor to the program. + if (mod.name == "coverage" || (prog._options |> find_arg("no_coverage") ?as tBool ?? false)) { + return false + } + var found = false + mod |> for_each_function("__coverage_init") <| $(fn) { + found = true + } + if (found) { + return false + } + var covModule : Module? + program_for_each_module(prog) <| $(m) { + if (m.name == "coverage") { + covModule = m + } + } + if (covModule == null) { + return false + } + add_module_require(mod, covModule, false) var astVisitor = new CoverageMacro() var inscope astVisitorAdapter <- make_visitor(*astVisitor) - visit(prog, astVisitorAdapter) + visit_module(prog, astVisitorAdapter, mod) var result = astVisitor.astChanged + if (result) { + // generate [init] function that registers all instrumented lines with 0 hits + var inscope initFn <- qmacro_function("__coverage_init") <| $() { + pass + } + var blk = unsafe(reinterpret initFn.body) + blk.list |> clear() + for (file, values in keys(coverageData), values(coverageData)) { + // register lines + var lineArr : array + for (line in keys(values.line_cov.lines)) { + lineArr |> push(line) + } + blk.list |> emplace_new <| qmacro(_::register_coverage_lines($v(string(file)), $v(lineArr))) + } + for (file, reg_data in keys(registration_data), values(registration_data)) { + // register functions + var nameArr : array + var lineArr : array + for (fname, fline in keys(reg_data.allFunctions), values(reg_data.allFunctions)) { + nameArr |> push(string(fname)) + lineArr |> push(fline) + } + blk.list |> emplace_new <| qmacro(coverage::register_coverage_functions($v(string(file)), $v(nameArr), $v(lineArr))) + + // register per-function lines + for (fname, flines in keys(reg_data.funcLines), values(reg_data.funcLines)) { + var flineArr : array + for (fl in keys(flines)) { + flineArr |> push(fl) + } + blk.list |> emplace_new <| qmacro(coverage::register_coverage_func_lines($v(string(file)), $v(string(fname)), $v(flineArr))) + } + + // register branches + var blines : array + var block_nums : array + var branch_nums : array + for (i in range(reg_data.allBranches |> length())) { + blines |> push(reg_data.allBranches[i]._0) + block_nums |> push(reg_data.allBranches[i]._1) + branch_nums |> push(reg_data.allBranches[i]._2) + } + blk.list |> emplace_new <| qmacro(coverage::register_coverage_branches($v(string(file)), $v(blines), $v(block_nums), $v(branch_nums))) + + } + initFn.flags |= FunctionFlags.generated | FunctionFlags.init | FunctionFlags.privateFunction + compiling_module() |> add_function(initFn) + } unsafe { delete astVisitor } return result } } + +def get_coverage_summary(cov_data : table; + per_function : bool) { + //! Returns a human-readable coverage summary table string (line coverage). + //! When per_function is true, also lists each function with its hit count. + var summary <- get_summary(cov_data) + var total_hits : uint + var total_missed : uint + var max_len = 4 + for (s in summary) { + if (s.file |> length > max_len) { + max_len = s.file |> length + } + } + if (per_function) { + for (file, data in keys(cov_data), values(cov_data)) { + for (fname in keys(data.func_cov)) { + if (fname |> length > max_len) { + max_len = fname |> length + } + } + } + } + let w = max_len + 2 + return build_string() <| $(writer) { + writer |> write("\nCoverage Summary\n") + writer |> write_chars('=', w + 30) + writer |> write("\n") + writer |> write("File") + writer |> write_chars(' ', w - 4) + writer |> write("Hits") + writer |> write_chars(' ', 6) + writer |> write("Missed") + writer |> write_chars(' ', 4) + writer |> write("Coverage\n") + writer |> write_chars('-', w + 30) + writer |> write("\n") + for (s in summary) { + total_hits += s.lineHits + total_missed += s.lineMissed + summary_line(writer, s.file, w, s.lineHits, s.lineMissed) + if (per_function) { + cov_data |> get(s.file) $(data) { + for (fname, fc in keys(data.func_cov), values(data.func_cov)) { + var fLineHits : uint + var fLineMissed : uint + for (v in values(fc.line_cov.lines)) { + if (v > 0u) { + fLineHits += 1u + } else { + fLineMissed += 1u + } + } + if (fLineHits + fLineMissed > 0u) { + summary_line(writer, " {fname}", w, fLineHits, fLineMissed) + } + } + } + } + } + writer |> write_chars('-', w + 30) + writer |> write("\n") + summary_line(writer, "TOTAL", w, total_hits, total_missed) + } +} + +def is_excluded(file : string; exclude_prefixes : array) : bool { + for (prefix in exclude_prefixes) { + if (file |> starts_with(prefix)) { + return true + } + } + return false +} + + +def parse_lcov(lcov_path : string; exclude_prefixes : array) : table { + // Parses an LCOV file (possibly containing multiple records for the same + // source file), populates coverageData, and calls get_coverage_summary. + var currentFile = "" + var skipFile = false + // Track function start lines per file for assigning DA lines to functions. + var funcStarts : table>> + var result = table() + fopen(lcov_path, "rb") <| $(f) { + if (f == null) { + print("Failed to open {lcov_path}\n") + return + } + while (!feof(f)) { + let line = fgets(f) |> strip() + if (line |> empty() || line == "end_of_record" || line == "TN:") { + continue + } + if (line |> starts_with("SF:")) { + currentFile = slice(line, 3) + skipFile = is_excluded(currentFile, exclude_prefixes) + continue + } + if (skipFile) { + continue + } + result |> get_with_default(currentFile) <| $(var cov) { + if (line |> starts_with("FN:")) { + // FN:line,name + let rest = slice(line, 3) + let comma = find(rest, ",") + if (comma >= 0) { + let fline = uint(to_int(slice(rest, 0, comma))) + let fname = slice(rest, comma + 1) + cov.func_cov |> get_with_default(fname) <| $(var fc) { + fc.line = fline + } + funcStarts |> get_with_default(currentFile) <| $(var starts) { + starts |> push(tuple(fline, string(fname))) + } + } + } elif (line |> starts_with("FNDA:")) { + // FNDA:hit_count,function_name + let rest = slice(line, 5) + let comma = find(rest, ",") + if (comma >= 0) { + let hits = uint(to_int(slice(rest, 0, comma))) + let fname = slice(rest, comma + 1) + cov.func_cov |> get_with_default(fname) <| $(var fc) { + fc.hits += hits + } + } + } elif (line |> starts_with("DA:")) { + // DA:line_number,hit_count + let rest = slice(line, 3) + let comma = find(rest, ",") + if (comma >= 0) { + let ln = uint(to_int(slice(rest, 0, comma))) + let hits = uint(to_int(slice(rest, comma + 1))) + if (cov.line_cov.lines |> key_exists(ln)) { + unsafe(cov.line_cov.lines[ln]) += hits + } else { + cov.line_cov.lines |> insert(ln, hits) + } + } + } elif (line |> starts_with("BRDA:")) { + // BRDA:line,block,branch,taken + let rest = slice(line, 5) + var last_comma = rfind(rest, ",") + if (last_comma >= 0) { + let taken = slice(rest, last_comma + 1) + let hits = taken == "-" ? 0u : uint(to_int(taken)) + // parse line,block,branch from the key part + let key_part = slice(rest, 0, last_comma) + let c1 = find(key_part, ",") + if (c1 >= 0) { + let br_line = uint(to_int(slice(key_part, 0, c1))) + let rest2 = slice(key_part, c1 + 1) + let c2 = find(rest2, ",") + if (c2 >= 0) { + let block_num = uint(to_int(slice(rest2, 0, c2))) + let branch_num = uint(to_int(slice(rest2, c2 + 1))) + let idx = uint(cov.branch_cov |> length) + cov.branch_cov |> insert(idx, BranchCov( + line = br_line, + block_num = block_num, + branch_num = branch_num, + hits = hits + )) + } + } + } + } + } + } + } + // Sort function starts and assign DA lines to functions for per-function line coverage. + for (file, starts in keys(funcStarts), values(funcStarts)) { + sort(starts) <| $(a, b) { + return a._0 < b._0 + } + result |> get(file) <| $(var cov) { + for (ln, hits in keys(cov.line_cov.lines), values(cov.line_cov.lines)) { + // Find function with largest start line <= ln. + var bestFunc = "" + for (s in starts) { + if (s._0 <= ln) { + bestFunc = s._1 + } else { + break + } + } + if (!bestFunc |> empty()) { + cov.func_cov |> get(bestFunc) <| $(var fc) { + if (fc.line_cov.lines |> key_exists(ln)) { + unsafe(fc.line_cov.lines[ln]) += hits + } else { + fc.line_cov.lines |> insert(ln, hits) + } + } + } + } + } + } + return <- result +} + +// ========================================================================== +// Public API +// ========================================================================== + +// Assigns to str coverage summary +[export] +def public fill_coverage_summary(var str : string?; per_function : bool = false) { + *str = get_coverage_summary(coverageData, per_function) +} + + +// Assigns to `dest` coverage in `lcov` format +[export] +def public fill_coverage_report(var dest : string?; name : string = "") { + //! Fills the string at dest with the coverage report. Used for cross-context data transfer. + *dest = get_report(name) +} + +// Returns all recorded coverage table to this moment. +[export] +def public get_coverage_details(var cov : table?) { + return <- clone(coverageData) +} + +[export] +def public summary_from_lcov_file(filename : string; exclude : array) { + let parsed_cov = parse_lcov(filename, exclude) + return get_coverage_summary(parsed_cov, true) +} diff --git a/dastest/dastest.das b/dastest/dastest.das index 0d639da39..33cfed03a 100644 --- a/dastest/dastest.das +++ b/dastest/dastest.das @@ -203,6 +203,9 @@ def main() { let isolatedMode = args |> has_value("--isolated-mode") var ctx <- SuiteCtx(args) + if (!ctx.cov_path |> empty()) { + remove(ctx.cov_path) + } if (!isolatedMode) { for (file in files) { let uri = ctx.uriPaths ? file_name_to_uri(file) : file diff --git a/dastest/suite.das b/dastest/suite.das index aac513b86..ed506501f 100644 --- a/dastest/suite.das +++ b/dastest/suite.das @@ -31,6 +31,7 @@ struct SuiteCtx { verbose : bool = false compile_only : bool = false use_aot : bool = false + cov_path : string = "" // Testing-related options. testNames : array @@ -96,6 +97,7 @@ def SuiteCtx(args : array) : SuiteCtx { ctx.projectPath = args |> get_str_arg("--test-project", "") ctx.compile_only = args |> has_value("--compile-only") ctx.use_aot = args |> has_value("--use-aot") || is_in_aot() + ctx.cov_path = args |> get_str_arg("--cov-path", "") collect_tests_names(args, ctx.testNames) ctx.bench_enabled = args |> has_value("--bench") @@ -170,7 +172,14 @@ def test_file(file_name : string; var ctx : SuiteCtx; var file_ctx : FileCtx) : cop.fail_on_no_aot = ctx.use_aot cop.threadlock_context = true cop.jit_enabled = jit_enabled() - cop.jit_module := "{get_das_root()}/daslib/just_in_time.das" + if (jit_enabled()) { + access |> add_extra_module("just_in_time", "{get_das_root()}/daslib/just_in_time.das") + } + if (!ctx.cov_path |> empty()) { + cop.export_all = true + cop.rtti = true + access |> add_extra_module("coverage", "{get_das_root()}/daslib/coverage.das") + } compile_file(file_name, access, unsafe(addr(mg)), cop) $(ok, program, output) { var expectedErrors : table if (program != null) { @@ -245,6 +254,21 @@ def test_file(file_name : string; var ctx : SuiteCtx; var file_ctx : FileCtx) : var mod = program |> get_this_module() if (mod != null && context != null) { let module_result = test_module(*mod, *context, ctx, file_ctx) + if (module_result.total > 0 && !ctx.cov_path |> empty()) { + var report = "" + unsafe(invoke_in_context(*context, "fill_coverage_report", unsafe(addr(report)), "")) + // Clone to current context. + report := report + fopen(ctx.cov_path, "ab") <| $(fw) { + if (fw != null) { + to_log(LOG_INFO, "Coverage to {ctx.cov_path}\n") + fwrite(fw, report) + } else { + to_log(LOG_ERROR, "Couldn't create output file {ctx.cov_path}\n") + } + } + + } if (module_result.failed + module_result.errors == 0) { unsafe { file_ctx.context = addr(*context) diff --git a/doc/source/stdlib/handmade/function-builtin-with_argv-0x4ad9bacb09c4f6e6.rst b/doc/source/stdlib/handmade/function-builtin-with_argv-0x4ad9bacb09c4f6e6.rst new file mode 100644 index 000000000..f69b97bbe --- /dev/null +++ b/doc/source/stdlib/handmade/function-builtin-with_argv-0x4ad9bacb09c4f6e6.rst @@ -0,0 +1 @@ +Sets ``argc``, ``argv`` to first argument, for the ``body`` block. \ No newline at end of file diff --git a/doc/source/stdlib/handmade/function-rtti-add_extra_module-0xc20a109fd8f14126.rst b/doc/source/stdlib/handmade/function-rtti-add_extra_module-0xc20a109fd8f14126.rst new file mode 100644 index 000000000..fd9e33175 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-add_extra_module-0xc20a109fd8f14126.rst @@ -0,0 +1 @@ +Adds extra module to ``FileAccess``. All files compiled via this ``FileAccess`` will include this extra module. diff --git a/doc/source/stdlib/handmade/structure_annotation-rtti-CodeOfPolicies.rst b/doc/source/stdlib/handmade/structure_annotation-rtti-CodeOfPolicies.rst index 71fa2bfea..9e796fddc 100644 --- a/doc/source/stdlib/handmade/structure_annotation-rtti-CodeOfPolicies.rst +++ b/doc/source/stdlib/handmade/structure_annotation-rtti-CodeOfPolicies.rst @@ -67,12 +67,9 @@ Force in-scope for POD-like types. Log in-scope for POD-like types. Enables debugger support. Enables debug inference flag. -Sets debug module (module which will be loaded when IDE connects). Enables profiler support. -Sets profile module (module which will be loaded when profiler connects). Enables threadlock context. JIT enabled - if enabled, JIT will be used to compile code at runtime. -JIT module - module loaded when -jit is specified. JIT all functions - if enabled, JIT will compile all functions in the module. JIT debug info - if enabled, JIT will generate debug info for JIT compiled code. JIT dll mode - if enabled, JIT will generate DLL's into JIT output folder and load them from there. diff --git a/include/daScript/ast/ast.h b/include/daScript/ast/ast.h index c65ea3c30..09117ad78 100644 --- a/include/daScript/ast/ast.h +++ b/include/daScript/ast/ast.h @@ -1482,7 +1482,6 @@ namespace das bool aot_macros = false; // enables aot of macro code (like 'qmacro_block') bool paranoid_validation = false; // todo bool cross_platform = false; // aot supports platform independent mode - string aot_module_path; string aot_result; // Path where to store cpp-result of aot // End aot config bool completion = false; // this code is being compiled for 'completion' mode @@ -1561,18 +1560,15 @@ namespace das // 3. context always has context mutex bool debugger = false; /*option*/ bool debug_infer_flag = false; // set this to true to debug macros for missing "not_inferred" - string debug_module; // profiler // only enabled if profiler is disabled // when enabled // 1. disables [fastcall] bool profiler = false; - string profile_module; // pinvoke /*option*/ bool threadlock_context = false; // has context mutex // jit bool jit_enabled = false; // enable JIT - string jit_module; // path to jit module // todo: add this params to serialization? bool jit_jit_all_functions = true; // JIT all functions by default bool jit_debug_info = false; // Add debug info to generate binary code diff --git a/include/daScript/simulate/debug_info.h b/include/daScript/simulate/debug_info.h index bef078f3f..5b4fdbd4b 100644 --- a/include/daScript/simulate/debug_info.h +++ b/include/daScript/simulate/debug_info.h @@ -224,6 +224,9 @@ namespace das return false; } + void addExtraModule ( const string & modName, const string & modFile ) { extraModules.emplace_back(modName, modFile); } + const vector> & getExtraModules () const { return extraModules; } + void lock() { locked = true; } void unlock() { locked = false; } bool isLocked() const { return locked; } @@ -231,6 +234,7 @@ namespace das virtual FileInfo * getNewFileInfo ( const string & ) { return nullptr; } protected: das_hash_map files; + vector> extraModules; bool locked = false; }; template <> struct isCloneable : false_type {}; diff --git a/src/ast/ast_lint.cpp b/src/ast/ast_lint.cpp index 1f1bb105b..186d0513a 100644 --- a/src/ast/ast_lint.cpp +++ b/src/ast/ast_lint.cpp @@ -940,6 +940,8 @@ namespace das { "heap_size_limit", Type::tInt, "string_heap_size_limit", Type::tInt, "gc", Type::tBool, + // coverage + "no_coverage", Type::tBool, // aot "no_aot", Type::tBool, "aot_prologue", Type::tBool, diff --git a/src/ast/ast_parse.cpp b/src/ast/ast_parse.cpp index 6e473d1ab..ef1b878e4 100644 --- a/src/ast/ast_parse.cpp +++ b/src/ast/ast_parse.cpp @@ -829,6 +829,17 @@ namespace das { reqR.extraDepModule = true; } } + // If the module was already promoted to the global list + // by a prior compilation reuse it directly instead of + // recompilation. Without this, a second Module* with the + // same nameHash ends up in the library and causes + // a hash-collision crash in addModule. + auto promotedMod = Module::requireEx(modName, true); + if ( promotedMod && promotedMod->promoted ) { + promotedMod->fromExtraDependency = true; + libGroup.addModule(promotedMod); + return true; + } auto finfo = access->getFileInfo(modFile); ModuleInfo info; info.fileName = finfo->name; @@ -1085,25 +1096,19 @@ namespace das { vector namelessMismatches; uint64_t preqT = 0; string modName; + bool allGood = true; + for ( const auto & em : access->getExtraModules() ) { + allGood = addExtraDependency(em.first, em.second, missing, circular, notAllowed, req, dependencies, namelessReq, namelessMismatches, access, libGroup, policies, &logs) && allGood; + } + if ( !allGood ) { + auto res = make_smart(); + res->error("internal error", logs.str(), "", LineInfo(), CompilationError::syntax_error); + return res; + } if ( getPrerequisits(fileName, access, modName, req, missing, circular, notAllowed, chain, dependencies, namelessReq, namelessMismatches, libGroup, nullptr, 1, !policies.ignore_shared_modules) ) { preqT = get_time_usec(time0); disableSerializationOnDebugger(req); - bool allGood = true; - if ( policies.debugger ) { - allGood = addExtraDependency("debug", policies.debug_module, missing, circular, notAllowed, req, dependencies, namelessReq, namelessMismatches, access, libGroup, policies, &logs) && allGood; - } else if ( policies.profiler ) { - allGood = addExtraDependency("profiler", policies.profile_module, missing, circular, notAllowed, req, dependencies, namelessReq, namelessMismatches, access, libGroup, policies, &logs) && allGood; - } /* else */ if ( !policies.aot_module_path.empty() ) { - allGood = addExtraDependency("ast_aot_macro", policies.aot_module_path, missing, circular, notAllowed, req, dependencies, namelessReq, namelessMismatches, access, libGroup, policies, &logs) && allGood; - } /* else */ if ( policies.jit_enabled ) { - allGood = addExtraDependency("just_in_time", policies.jit_module, missing, circular, notAllowed, req, dependencies, namelessReq, namelessMismatches, access, libGroup, policies, &logs) && allGood; - } - if ( !allGood ) { - auto res = make_smart(); - res->error("internal error", logs.str(), "", LineInfo(), CompilationError::syntax_error); - return res; - } if ( !verifyModuleNamesUnique(req, logs) ) { auto res = make_smart(); res->error("Several modules with invalid names", logs.str(), "", LineInfo(), diff --git a/src/builtin/module_builtin_ast_serialize.cpp b/src/builtin/module_builtin_ast_serialize.cpp index 051439652..d2aa6a967 100644 --- a/src/builtin/module_builtin_ast_serialize.cpp +++ b/src/builtin/module_builtin_ast_serialize.cpp @@ -2220,9 +2220,7 @@ namespace das { << value.force_inscope_pod << value.log_inscope_pod << value.debugger - << value.debug_module << value.profiler - << value.profile_module << value.jit_enabled << value.jit_jit_all_functions << value.jit_debug_info diff --git a/src/builtin/module_builtin_rtti.cpp b/src/builtin/module_builtin_rtti.cpp index df14eb460..a2257ec2d 100644 --- a/src/builtin/module_builtin_rtti.cpp +++ b/src/builtin/module_builtin_rtti.cpp @@ -789,15 +789,12 @@ namespace das { // debugger addField("debugger"); addField("debug_infer_flag"); - addField("debug_module"); // profiler addField("profiler"); - addField("profile_module"); // threadlock context addField("threadlock_context"); // jit addField("jit_enabled"); - addField("jit_module"); addField("jit_jit_all_functions"); addField("jit_debug_info"); addField("jit_opt_level"); @@ -1163,6 +1160,12 @@ namespace das { return access->addFsRoot(mod, path); } + void rtti_add_extra_module ( smart_ptr_raw access, const char * modName, const char * modFile, Context * context, LineInfoArg * at ) { + if ( !modName ) context->throw_error_at(at, "expecting module name"); + if ( !modFile ) context->throw_error_at(at, "expecting module file path"); + access->addExtraModule(modName, modFile); + } + #if !DAS_NO_FILEIO @@ -1573,6 +1576,9 @@ namespace das { addExtern(*this, lib, "add_file_access_root", SideEffects::modifyExternal, "rtti_add_file_access_root") ->args({"access","mod","path"}); + addExtern(*this, lib, "add_extra_module", + SideEffects::modifyExternal, "rtti_add_extra_module") + ->args({"access","modName","modFile","context","line"}); addExtern(*this, lib, "program_for_each_module", SideEffects::modifyExternal, "rtti_builtin_program_for_each_module") ->args({"program","block","context","line"}); diff --git a/src/builtin/module_builtin_runtime.cpp b/src/builtin/module_builtin_runtime.cpp index a047fd4fb..b5b33c87a 100644 --- a/src/builtin/module_builtin_runtime.cpp +++ b/src/builtin/module_builtin_runtime.cpp @@ -1433,6 +1433,13 @@ namespace das arr = g_CommandLineArguments; } + void withCommandLineArguments( const Array & arr, const TBlock & body, Context * context, LineInfoArg * at ) { + auto prev = g_CommandLineArguments; + g_CommandLineArguments = arr; + context->invoke(body, nullptr, nullptr, at); + g_CommandLineArguments = prev; + } + char * builtin_das_root ( Context * context, LineInfoArg * at ) { return context->allocateString(getDasRoot(), at); } @@ -1712,6 +1719,9 @@ namespace das addExtern(*this, lib, "builtin_get_command_line_arguments", SideEffects::accessExternal,"getCommandLineArguments") ->arg("arguments"); + addExtern(*this, lib, "with_argv", + SideEffects::invoke, "withArgv") + ->args({"new_arguments", "block","context","line"}); // compile-time functions addExtern(*this, lib, "is_compiling", SideEffects::accessExternal, "is_compiling"); diff --git a/tests/jit_tests/jit_exe.das b/tests/jit_tests/jit_exe.das index 54224f625..76566cab5 100644 --- a/tests/jit_tests/jit_exe.das +++ b/tests/jit_tests/jit_exe.das @@ -37,7 +37,7 @@ def test_hello_world(t : T?) { cop.jit_enabled = true cop.jit_exe_mode = true cop.jit_dll_mode = false - cop.jit_module := "{get_das_root()}/daslib/just_in_time.das" + access |> add_extra_module("just_in_time", "{get_das_root()}/daslib/just_in_time.das") cop.jit_output_path := EXE_PATH compile_file(SCRIPT_PATH, access, unsafe(addr(mg)), cop) <| $(ok, program, errors) { t |> equal(ok, true) diff --git a/tests/language/capture_string.das b/tests/language/capture_string.das index cdfc49bae..a2e7c6b1b 100644 --- a/tests/language/capture_string.das +++ b/tests/language/capture_string.das @@ -1,4 +1,5 @@ options gen2 +options no_coverage module foobar diff --git a/tests/language/failed_const_and_block_folding.das b/tests/language/failed_const_and_block_folding.das index 9eed9bf10..0d96d53c1 100644 --- a/tests/language/failed_const_and_block_folding.das +++ b/tests/language/failed_const_and_block_folding.das @@ -4,6 +4,8 @@ options gen2 expect 40209:2 +options no_coverage + //options log_optimization_passes=true // options log=true diff --git a/tests/language/failed_static_assert_in_infer.das b/tests/language/failed_static_assert_in_infer.das index 5c7bd28fb..31e308a48 100644 --- a/tests/language/failed_static_assert_in_infer.das +++ b/tests/language/failed_static_assert_in_infer.das @@ -1,4 +1,5 @@ options gen2 +options no_coverage // options log=true,log_infer_passes=false,log_optimization_passes=true,optimize=true expect 40100 diff --git a/tests/language/new_delete.das b/tests/language/new_delete.das index f7c14156b..0d1d48029 100644 --- a/tests/language/new_delete.das +++ b/tests/language/new_delete.das @@ -2,6 +2,7 @@ options gen2 // options log=true // options log_infer_passes=true // log_nodes=false +options no_coverage options persistent_heap = true require strings diff --git a/tests/language/serialization.das b/tests/language/serialization.das index afdfe7140..e296e8fe8 100644 --- a/tests/language/serialization.das +++ b/tests/language/serialization.das @@ -3,6 +3,10 @@ options no_unused_function_arguments = false options no_unused_block_arguments = false options persistent_heap +// todo: resize(array) is becoming +// unsafe under coverage. +options no_coverage + require dastest/testing_boost public require daslib/archive require strings diff --git a/tests/linq/test_linq_concat.das b/tests/linq/test_linq_concat.das index 843571921..5d3d1ca47 100644 --- a/tests/linq/test_linq_concat.das +++ b/tests/linq/test_linq_concat.das @@ -40,6 +40,11 @@ def test_append(t : T?) { append_inplace(a, 5) t |> success(a.Equal([0, 1, 2, 3, 4, 5])) } + t |> run("append from array") @(t : T?) { + let arr = [for (x in 0..5); x] + var result = append(arr, 5) + t |> success(result.Equal([0, 1, 2, 3, 4, 5])) + } } [test] @@ -74,6 +79,11 @@ def test_prepend(t : T?) { prepend_inplace(a, -1) t |> success(a.Equal([-1, 0, 1, 2, 3, 4])) } + t |> run("prepend from array") @(t : T?) { + let arr = [for (x in 0..5); x] + var result = prepend(arr, -1) + t |> success(result.Equal([-1, 0, 1, 2, 3, 4])) + } } [test] @@ -108,4 +118,10 @@ def test_concat(t : T?) { concat_inplace(a, [for (x in 5..10); x]) t |> success(a.Equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) } + t |> run("concat from arrays") @(t : T?) { + let a = [for (x in 0..5); x] + let b = [for (x in 5..10); x] + var result = concat(a, b) + t |> success(result.Equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) + } } diff --git a/tests/linq/test_linq_partition.das b/tests/linq/test_linq_partition.das index 5f12bc614..2a4dd6ea3 100644 --- a/tests/linq/test_linq_partition.das +++ b/tests/linq/test_linq_partition.das @@ -113,6 +113,14 @@ def test_take(t : T?) { t |> success(c.a.Equal([i, i * 10])) } } + t |> run("take from array") @(t : T?) { + let arr = [for (x in 0..5); x] + var result = take(arr, 3) + t |> equal(3, length(result)) + for (c, i in result, 0..3) { + t |> equal(c, i) + } + } t |> run("take to array") @(t : T?) { var query_array = take_to_array( [iterator for(x in 0..5); x], diff --git a/tests/linq/test_linq_transform.das b/tests/linq/test_linq_transform.das index e504e32a2..65a21eab3 100644 --- a/tests/linq/test_linq_transform.das +++ b/tests/linq/test_linq_transform.das @@ -83,6 +83,25 @@ def test_select(t : T?) { t |> equal(q._1.a[1], i * 10) } } + t |> run("select from array") @(t : T?) { + var nums = [10, 20, 30, 40, 50] + var query = select(nums) + for (i, q in 0..5, query) { + t |> equal(q._0, i) + t |> equal(q._1, (i + 1) * 10) + } + } + t |> run("select from array with result selector") @(t : T?) { + var nums = [10, 20, 30] + var query = _select( + nums, + (_, _ * 2) + ) + for (i, q in 0..3, query) { + t |> equal(q._0, (i + 1) * 10) + t |> equal(q._1, (i + 1) * 20) + } + } t |> run("select to sequence") @(t : T?) { var chars = ["a", "b", "c", "d", "e"] var query = select(chars).to_sequence() diff --git a/utils/aot/main.das b/utils/aot/main.das index df72b459c..ba752d209 100644 --- a/utils/aot/main.das +++ b/utils/aot/main.das @@ -156,29 +156,25 @@ def main() { aotlib_files = get_list_of_files(args, "-aotlib") ctx_files = get_list_of_files(args, "-ctx") } - // If no files provided return `0` - // Otherwise if at least 1 file succeeded return `0` - var result = (aot_files |> empty() && - aotlib_files |> empty() && - ctx_files |> empty()) + var result = true using <| $(var cop : CodeOfPolicies) { updateCOP(cop, !gen1, gen2_make, false) for ((aot_in, aot_out) in aot_files) { let res = aot(aot_in, false, cross_platform, cop) let written = write_result(res, aot_out, "Aot", force_overwrite) - result = result || written + result = result && written } updateCOP(cop, !gen1, gen2_make, true) for ((aot_in, aot_out) in aotlib_files) { let res = aot(aot_in, false, cross_platform, cop) let written = write_result(res, aot_out, "Aot library", force_overwrite) - result = result || written + result = result && written } updateCOP(cop, !gen1, gen2_make, false) for ((ctx_in, ctx_out) in ctx_files) { let res = standalone_aot(ctx_in, ctx_out, cross_platform, false, cop) let written = write_result(res, ctx_out, "Standalone ctx", force_overwrite) - result = result || written + result = result && written } } unsafe { diff --git a/utils/daScript/main.cpp b/utils/daScript/main.cpp index 71d488c5c..303006807 100644 --- a/utils/daScript/main.cpp +++ b/utils/daScript/main.cpp @@ -49,7 +49,6 @@ static CodeOfPolicies getPolicies() { CodeOfPolicies policies; policies.aot = aotEnabled; policies.aot_module = true; - policies.aot_module_path = ""; if (aotMacros) { policies.aot_macros = true; policies.export_all = true; // need it for aot to export macros @@ -307,7 +306,7 @@ int das_aot_main ( int argc, char * argv[] ) { #endif Module::Initialize(); daScriptEnvironment::getBound()->g_isInAot = true; - bool compiled = false; + bool compiled = true; if ( standaloneContext ) { StandaloneContextCfg cfg = {standaloneContextName, standaloneClassName ? standaloneClassName : "StandaloneContext"}; cfg.cross_platform = cross_platform; @@ -350,7 +349,7 @@ int das_aot_main ( int argc, char * argv[] ) { if (!is_ok && !quiet) { tout << "Failed to compile `" << string(in_file) << "` in aot.\n"; } - compiled |= is_ok; + compiled &= is_ok; } } else { size_t id = 2; @@ -360,7 +359,7 @@ int das_aot_main ( int argc, char * argv[] ) { if (!is_ok && !quiet) { tout << "Failed to compile `" << out << "` in aot.\n"; } - compiled |= is_ok; + compiled &= is_ok; id += 2; } } @@ -380,10 +379,10 @@ bool compile_and_run ( const string & fn, const string & mainFnName, bool output CodeOfPolicies policies; if ( debuggerRequired ) { policies.debugger = true; - policies.debug_module = getDasRoot() + "/daslib/debug.das"; + access->addExtraModule("debug", getDasRoot() + "/daslib/debug.das"); } else if ( profilerRequired ) { policies.profiler = true; - policies.profile_module = getDasRoot() + "/daslib/profiler.das"; + access->addExtraModule("profiler", getDasRoot() + "/daslib/profiler.das"); } /*else*/ if ( jitEnabled != JitMode::None ) { policies.jit_enabled = true; switch (jitEnabled) { @@ -392,13 +391,13 @@ bool compile_and_run ( const string & fn, const string & mainFnName, bool output case JitMode::Direct: break; default: break; } - policies.jit_module = getDasRoot() + "/daslib/just_in_time.das"; + access->addExtraModule("just_in_time", getDasRoot() + "/daslib/just_in_time.das"); policies.jit_output_path = jitOutPath; policies.dll_search_paths.emplace_back(getDasRoot() + "/lib"); } else if (aotEnabled) { policies.aot = false; policies.aot_module = true; - policies.aot_module_path = getDasRoot() + "/daslib/aot_macro.das"; + access->addExtraModule("ast_aot_macro", getDasRoot() + "/daslib/aot_macro.das"); policies.aot_result = aotResult; daScriptEnvironment::getBound()->g_isInAot = true; } diff --git a/utils/dascov/README.md b/utils/dascov/README.md new file mode 100644 index 000000000..164cfc626 --- /dev/null +++ b/utils/dascov/README.md @@ -0,0 +1,83 @@ +# Code coverage + +A command-line tool for measuring code coverage of das code. + +## Overview + +dascov support two options: +- brief output to stdout +- output in lcov format to a file + +Key capabilities: + +- Measures per file code coverage +- Measures per file per function code coverage +- Measures branches coverage + +## Usage +### Standalone tool +To measure coverage of the command: +``` +daslang main.das -- +``` +It should be replaced with one of the: +``` +daslang utils/dascov/main.das -- - main.das +daslang utils/dascov/main.das -- cov.lcov main.das +``` + +Brief coverage for file: +``` +options gen2 + +var x = 123 + +[export] +def main() { + x += 1 + if (x > 1) { + print("hit") + } else { + print("not hit") + } +} +``` +Looks like this: +``` +Coverage Summary +============================================== +File Hits Missed Coverage +---------------------------------------------- +main.das 3 1 75% + main 3 1 75% +---------------------------------------------- +TOTAL 3 1 75% +``` + +Coverage can be recovered from recorded file as well. For example dastest +creates coverage.lcov, and it can be parsed via: +``` +# ./bin/daslang utils/dascov/main.das -- cov.txt --exclude tests/ +Coverage Summary +===================================================================================================================================== +File Hits Missed Coverage +------------------------------------------------------------------------------------------------------------------------------------- +/home/alexey-churkin/daScript2/daScript2/daslib/fuzzer.das 145 0 100% + fuzzer`fuzz_numeric_op3`10015655030841158810 4 0 100% + fuzzer`fuzz_numeric_vec_scal_op2`10935617188872172278 9 0 100% + fuzzer`fuzz_int_vector_op2`14625781784477760102 8 0 100% + +``` + +### Macro coverage +`require daslib/coverage` can be added to your project. +This will include macro and add coverage instrumentation to your project. + +Results can be obtained via: +``` +fill_report() +get_summary_string() +``` + + can either be a `-` to output to stdout in brief format +or filename to store coverage in `lcov` format: diff --git a/utils/dascov/main.das b/utils/dascov/main.das new file mode 100644 index 000000000..6b20b99d9 --- /dev/null +++ b/utils/dascov/main.das @@ -0,0 +1,97 @@ +options gen2 + +options no_coverage + +require daslib/coverage +require daslib/ast_boost +require debugapi +require fio +require strings + +def public compile_and_simulate(input : string, var access; var mg : ModuleGroup?, cop, blk) { + //! Compiles a daslang file, simulates it, and invokes a callback with the program and context. + compile_file(input, access, mg, cop) $(ok; var program : smart_ptr; issues) { + if (!ok) { + print("failed to compile {input}\n{issues}\n") + return + } + simulate(program) $(sok; var pctx : smart_ptr; serrors) { + if (!sok) { + panic("Failed to simulate {serrors}") + } + blk(program, pctx) + } + } +} + + +[export] +def main() { + // Usage: + // ./bin/daslang utils/dascov/main.das -- [--exclude ]... + // - print merged coverage summary from an existing LCOV file + // - --exclude skips files starting with (e.g. --exclude tests/) + // ./bin/daslang utils/dascov/main.das -- + // - if output file == `-` print short statistic to stdout + // - if output file is a regular file print to file in lcov format. + let args <- get_command_line_arguments() + let sep = find_index(args, "--") + 1 + let numArgs = args |> length() - sep + // Collect --exclude prefixes. + var excludes : array + for (i in range(sep, args |> length() - 1)) { + if (args[i] == "--exclude") { + excludes |> push(args[i + 1]) + } + } + if (numArgs == 1 || (numArgs >= 2 && args[sep + 1] == "--exclude")) { + // LCOV summary mode: parse existing LCOV file and print summary. + print(coverage::summary_from_lcov_file(args[sep], excludes)) + return + } + var coverage_file = args[sep] + var input_file = args[sep + 1] + var new_args = array() + for (i in range(args |> length())) { + if (i == 1) { + new_args |> push(input_file) + } elif (i == sep + 1 || i == sep) { + // Do nothing + } else { + new_args |> push(args[i]) + } + } + using <| $(var cop : CodeOfPolicies) { + using() $(var mg : ModuleGroup) { + var inscope access <- make_file_access("") + cop.threadlock_context = true + access |> add_extra_module("coverage", "{get_das_root()}/daslib/coverage.das") + var report : string + compile_and_simulate(input_file, access, unsafe(addr(mg)), cop) $(program : ProgramPtr; var pctx : smart_ptr) { + with_argv(new_args) { + unsafe(invoke_in_context(pctx, "main")) + } + if (coverage_file == "-") { + var rep = "" + unsafe(invoke_in_context(pctx, "fill_coverage_summary", unsafe(addr(rep)), true)) + // Clone to current context. + report = clone_string(rep) + print(report) + } else { + var rep = "" + unsafe(invoke_in_context(pctx, "fill_coverage_report", unsafe(addr(rep)), "")) + // Clone to current context. + report = clone_string(rep) + fopen(coverage_file, "wb") <| $(fw) { + if (fw != null) { + to_log(LOG_INFO, "Coverage to {coverage_file}\n") + fwrite(fw, report) + } else { + to_log(LOG_ERROR, "Couldn't create output file {coverage_file}\n") + } + } + } + } + } + } +} diff --git a/utils/dascov/tests/multi_func.das b/utils/dascov/tests/multi_func.das new file mode 100644 index 000000000..a2d982199 --- /dev/null +++ b/utils/dascov/tests/multi_func.das @@ -0,0 +1,14 @@ +options gen2 + +def called_func() { + print("called\n") +} + +def uncalled_func() { + print("uncalled\n") +} + +[export] +def main() { + called_func() +} diff --git a/utils/dascov/tests/nested_branch.das b/utils/dascov/tests/nested_branch.das new file mode 100644 index 000000000..e5d995c2c --- /dev/null +++ b/utils/dascov/tests/nested_branch.das @@ -0,0 +1,15 @@ +options gen2 + +[export] +def main() { + var x = 10 + if (x > 5) { + if (x > 20) { + print("deep\n") + } else { + print("shallow\n") + } + } else { + print("low\n") + } +} diff --git a/utils/dascov/tests/simple_branch.das b/utils/dascov/tests/simple_branch.das new file mode 100644 index 000000000..5ba3747f1 --- /dev/null +++ b/utils/dascov/tests/simple_branch.das @@ -0,0 +1,17 @@ +options gen2 + +var x = 123 + +def foo() { + print("NO") +} + +[export] +def main() { + x += 1 + if (x > 1) { + print("This") + } else { + print("That") + } +} \ No newline at end of file diff --git a/utils/dascov/tests/simple_linear.das b/utils/dascov/tests/simple_linear.das new file mode 100644 index 000000000..6b602d4b6 --- /dev/null +++ b/utils/dascov/tests/simple_linear.das @@ -0,0 +1,8 @@ +options gen2 + +[export] +def main() { + var x = 1 + x += 2 + print("{x}\n") +} diff --git a/utils/dascov/tests/test_exact_cov.das b/utils/dascov/tests/test_exact_cov.das new file mode 100644 index 000000000..324507fe2 --- /dev/null +++ b/utils/dascov/tests/test_exact_cov.das @@ -0,0 +1,183 @@ +options gen2 + +options no_coverage + +require daslib/ast_boost +require daslib/coverage +require daslib/strings_boost +require dastest/testing_boost + +require debugapi +require fio +require rtti +require strings + +def compile_and_simulate(input : string; var access; var mg : ModuleGroup?; cop; blk) { + compile_file(input, access, mg, cop) $(ok; var program : smart_ptr; issues) { + if (!ok) { + print("failed to compile {input}\n{issues}\n") + return + } + simulate(program) $(sok; var pctx : smart_ptr; serrors) { + if (!sok) { + panic("Failed to simulate {serrors}") + } + blk(program, pctx) + } + } +} + +def get_coverage_report(input_file : string) : string { + var report : string + using <| $(var cop : CodeOfPolicies) { + using() $(var mg : ModuleGroup) { + var inscope access <- make_file_access("") + cop.threadlock_context = true + access |> add_extra_module("coverage", "{get_das_root()}/daslib/coverage.das") + compile_and_simulate(input_file, access, unsafe(addr(mg)), cop) $(program : ProgramPtr; var pctx : smart_ptr) { + with_argv(array()) { + unsafe(invoke_in_context(pctx, "main")) + } + var rep = "" + unsafe(invoke_in_context(pctx, "fill_coverage_report", unsafe(addr(rep)), "")) + // Clone to current context. + report = clone_string(rep) + } + } + } + return report +} + +def has_line(report : string; line : string) : bool { + return find(report, line) >= 0 +} + +def normalize_report(report : string) : string { + // Replace absolute path prefix with relative "tests/" prefix + let root = "{get_das_root()}/utils/dascov/" + return replace(report, root, "") +} + +[test] +def test_simple_branch(t : T?) { + let input_file = "{get_das_root()}/utils/dascov/tests/simple_branch.das" + let report = normalize_report(get_coverage_report(input_file)) + t |> run("source file") <| @(t : T?) { + t |> success(report |> has_line("SF:tests/simple_branch.das")) + } + t |> run("functions registered") <| @(t : T?) { + t |> success(report |> has_line("FN:5,foo")) + t |> success(report |> has_line("FN:10,main")) + } + t |> run("foo not called, main called") <| @(t : T?) { + t |> success(report |> has_line("FNDA:0,foo")) + t |> success(report |> has_line("FNDA:1,main")) + } + t |> run("function counts") <| @(t : T?) { + t |> success(report |> has_line("FNF:2")) + t |> success(report |> has_line("FNH:1")) + } + t |> run("branches") <| @(t : T?) { + // if (x > 1) on line 12: true branch taken, false branch not taken + t |> success(report |> has_line("BRDA:12,0,0,1")) + t |> success(report |> has_line("BRDA:12,0,1,-")) + t |> success(report |> has_line("BRF:2")) + t |> success(report |> has_line("BRH:1")) + } + t |> run("line coverage") <| @(t : T?) { + // main body lines are hit + t |> success(report |> has_line("DA:11,1")) + t |> success(report |> has_line("DA:12,1")) + t |> success(report |> has_line("DA:13,1")) + // else branch line not hit + t |> success(report |> has_line("DA:15,0")) + // foo body not hit + t |> success(report |> has_line("DA:6,0")) + } +} + +[test] +def test_simple_linear(t : T?) { + let input_file = "{get_das_root()}/utils/dascov/tests/simple_linear.das" + let report = normalize_report(get_coverage_report(input_file)) + t |> run("source file") <| @(t : T?) { + t |> success(report |> has_line("SF:tests/simple_linear.das")) + } + t |> run("main called") <| @(t : T?) { + t |> success(report |> has_line("FNDA:1,main")) + t |> success(report |> has_line("FNF:1")) + t |> success(report |> has_line("FNH:1")) + } + t |> run("no branches") <| @(t : T?) { + t |> success(report |> has_line("BRF:0")) + t |> success(report |> has_line("BRH:0")) + } + t |> run("all lines hit") <| @(t : T?) { + t |> success(report |> has_line("DA:5,1")) + t |> success(report |> has_line("DA:6,1")) + t |> success(report |> has_line("DA:7,1")) + } +} + +[test] +def test_multi_func(t : T?) { + let input_file = "{get_das_root()}/utils/dascov/tests/multi_func.das" + let report = normalize_report(get_coverage_report(input_file)) + t |> run("all functions registered") <| @(t : T?) { + t |> success(report |> has_line("FN:3,called_func")) + t |> success(report |> has_line("FN:7,uncalled_func")) + t |> success(report |> has_line("FN:12,main")) + } + t |> run("called_func and main hit, uncalled_func not") <| @(t : T?) { + t |> success(report |> has_line("FNDA:1,called_func")) + t |> success(report |> has_line("FNDA:0,uncalled_func")) + t |> success(report |> has_line("FNDA:1,main")) + } + t |> run("function counts") <| @(t : T?) { + t |> success(report |> has_line("FNF:3")) + t |> success(report |> has_line("FNH:2")) + } + t |> run("uncalled function lines are 0") <| @(t : T?) { + t |> success(report |> has_line("DA:8,0")) + } + t |> run("called function lines are hit") <| @(t : T?) { + t |> success(report |> has_line("DA:4,1")) + t |> success(report |> has_line("DA:13,1")) + } +} + +[test] +def test_nested_branch(t : T?) { + let input_file = "{get_das_root()}/utils/dascov/tests/nested_branch.das" + let report = normalize_report(get_coverage_report(input_file)) + t |> run("main registered and called") <| @(t : T?) { + t |> success(report |> has_line("FN:4,main")) + t |> success(report |> has_line("FNDA:1,main")) + } + t |> run("outer branch: true taken, false not") <| @(t : T?) { + // if (x > 5) on line 6, block 1 + t |> success(report |> has_line("BRDA:6,1,0,1")) + t |> success(report |> has_line("BRDA:6,1,1,-")) + } + t |> run("inner branch: true not taken, false taken") <| @(t : T?) { + // if (x > 20) on line 7, block 0 + t |> success(report |> has_line("BRDA:7,0,0,-")) + t |> success(report |> has_line("BRDA:7,0,1,1")) + } + t |> run("branch counts") <| @(t : T?) { + // 2 if statements = 4 branches + t |> success(report |> has_line("BRF:4")) + // outer true + inner false = 2 hit + t |> success(report |> has_line("BRH:2")) + } + t |> run("covered lines") <| @(t : T?) { + t |> success(report |> has_line("DA:5,1")) // var x = 10 + t |> success(report |> has_line("DA:6,1")) // if (x > 5) + t |> success(report |> has_line("DA:7,1")) // if (x > 20) + t |> success(report |> has_line("DA:10,1")) // feint("shallow") + } + t |> run("uncovered lines") <| @(t : T?) { + t |> success(report |> has_line("DA:8,0")) // feint("deep") + t |> success(report |> has_line("DA:13,0")) // feint("low") + } +}