diff --git a/scripts/cxx_codegen/profdata_merger/profdata_merger.cpp b/scripts/cxx_codegen/profdata_merger/profdata_merger.cpp index ec816acd1..6d60fdfa5 100644 --- a/scripts/cxx_codegen/profdata_merger/profdata_merger.cpp +++ b/scripts/cxx_codegen/profdata_merger/profdata_merger.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include @@ -779,6 +780,7 @@ struct queries { "SegmentIndex", // 11 "NestedIn", // 12 "IsLeaf", // 13 + "IsBranch", // 14 })) , // --- @@ -885,6 +887,7 @@ struct CountedRegionHasher { } }; + struct CountedRegionComparator { size_t operator()( const CounterMappingRegion& lhs, @@ -898,6 +901,35 @@ struct CountedRegionComparator { } }; + +struct FileSpan { + unsigned int LineStart{}; + unsigned int LineEnd{}; + unsigned int ColStart{}; + unsigned int ColEnd{}; +}; + +struct FileSpanHasher { + size_t operator()(const FileSpan& region) const { + return hash_combine( + region.LineStart, + region.LineEnd, + region.ColStart, + region.ColEnd); + } +}; + + +struct FileSpanComparator { + size_t operator()(const FileSpan& lhs, const FileSpan& rhs) const { + return lhs.LineStart == rhs.LineStart // + && lhs.ColStart == rhs.ColStart // + && lhs.LineEnd == rhs.LineEnd // + && lhs.ColEnd == rhs.ColEnd; + } +}; + + struct db_build_ctx { int context_id{}; int instantiation_id{}; @@ -1141,6 +1173,32 @@ void add_file(CoverageData const& file, queries& q, db_build_ctx& ctx) { return lhs.first.self_id < rhs.first.self_id; }); + std::unordered_map + branch_cache{}; + + for (auto it : enumerate(file.getBranches())) { + CountedRegion const& r = it.value(); + branch_cache.insert_or_assign( + FileSpan{ + .LineStart = r.LineStart, + .LineEnd = r.LineEnd, + .ColStart = r.ColumnStart, + .ColEnd = r.ColumnEnd, + }, + true); + q.file_branch.bind(1, ctx.context_id); + q.file_branch.bind(2, (int64_t)r.ExecutionCount); + q.file_branch.bind(3, (int64_t)r.FalseExecutionCount); + q.file_branch.bind(4, r.Folded); + q.file_branch.bind(5, (int64_t)r.LineStart); + q.file_branch.bind(6, (int64_t)r.ColumnStart); + q.file_branch.bind(7, (int64_t)r.LineEnd); + q.file_branch.bind(8, (int64_t)r.ColumnEnd); + q.func_region.bind(9, r.Kind); + q.file_branch.exec(); + q.file_branch.reset(); + } + for (auto it : enumerate(segment_pairs)) { auto const& [nesting, end] = it.value(); auto const& start = nesting.segment; @@ -1162,24 +1220,19 @@ void add_file(CoverageData const& file, queries& q, db_build_ctx& ctx) { q.segment.bind(12, nullptr); } q.segment.bind(13, nesting.is_leaf); + + FileSpan span{ + .LineStart = start.Line, + .LineEnd = end.Line, + .ColStart = start.Col, + .ColEnd = end.Col, + }; + + q.segment.bind(14, branch_cache.contains(span)); q.segment.exec(); q.segment.reset(); } - for (auto it : enumerate(file.getBranches())) { - CountedRegion const& r = it.value(); - q.file_branch.bind(1, ctx.context_id); - q.file_branch.bind(2, (int64_t)r.ExecutionCount); - q.file_branch.bind(3, (int64_t)r.FalseExecutionCount); - q.file_branch.bind(4, r.Folded); - q.file_branch.bind(5, (int64_t)r.LineStart); - q.file_branch.bind(6, (int64_t)r.ColumnStart); - q.file_branch.bind(7, (int64_t)r.LineEnd); - q.file_branch.bind(8, (int64_t)r.ColumnEnd); - q.func_region.bind(9, r.Kind); - q.file_branch.exec(); - q.file_branch.reset(); - } for (ExpansionRecord const& e : file.getExpansions()) { int function_id = get_function_id(e.Function, q, ctx); diff --git a/scripts/cxx_codegen/profdata_merger/profdata_merger.sql b/scripts/cxx_codegen/profdata_merger/profdata_merger.sql index 6dd07fad4..6b2d69b94 100644 --- a/scripts/cxx_codegen/profdata_merger/profdata_merger.sql +++ b/scripts/cxx_codegen/profdata_merger/profdata_merger.sql @@ -104,6 +104,7 @@ CREATE TABLE "CovSegment" ( "SegmentIndex" INTEGER NOT NULL, "NestedIn" INTEGER, "IsLeaf" BOOLEAN NOT NULL, + "IsBranch" BOOLEAN NOT NULL, PRIMARY KEY ("Id"), FOREIGN KEY("File") REFERENCES "CovFile" ("Id"), FOREIGN KEY("Context") REFERENCES "CovContext" ("Id"), diff --git a/scripts/py_repository/py_repository/gen_coverage_cxx.py b/scripts/py_repository/py_repository/gen_coverage_cxx.py index aedc7d6ce..4060740f3 100755 --- a/scripts/py_repository/py_repository/gen_coverage_cxx.py +++ b/scripts/py_repository/py_repository/gen_coverage_cxx.py @@ -113,6 +113,7 @@ class CovSegment(CoverageSchema): SegmentIndex = IntColumn() NestedIn = ForeignId("CovSegment.Id", nullable=True) IsLeaf = BoolColumn() + IsBranch = BoolColumn() def intersects(self, line: int, col: int) -> bool: return (self.LineStart <= line <= self.LineEnd) and (self.ColStart <= col <= diff --git a/scripts/py_scriptutils/py_scriptutils/json_utils.py b/scripts/py_scriptutils/py_scriptutils/json_utils.py index b5662d780..612b2287a 100644 --- a/scripts/py_scriptutils/py_scriptutils/json_utils.py +++ b/scripts/py_scriptutils/py_scriptutils/json_utils.py @@ -12,7 +12,8 @@ class Op(Enum): Replace = auto() Remove = auto() - Add = auto() + AddField = auto() + AppendItem = auto() @beartype @@ -39,7 +40,7 @@ def json_diff(source: Json, result.append(DiffItem(Op.Replace, path, target)) return result - if isinstance(source, list): + if isinstance(source, (list, tuple)): i = 0 while i < len(source) and i < len(target): temp_diff = json_diff(source[i], target[i], path.child(Index(i)), ignore) @@ -52,9 +53,11 @@ def json_diff(source: Json, i += 1 while i < len(target): - result.append(DiffItem(Op.Add, path.child(Index(i)), target[i])) + result.append(DiffItem(Op.AppendItem, path.child(Index(i)), target[i])) i += 1 + assert i == max(len(target), len(source)) + elif isinstance(source, dict): for key in source: if key in target: @@ -67,7 +70,7 @@ def json_diff(source: Json, for key in target: if key not in source: - result.append(DiffItem(Op.Add, path.child(Fields(key)), target[key])) + result.append(DiffItem(Op.AddField, path.child(Fields(key)), target[key])) else: result.append(DiffItem(Op.Replace, path, target)) @@ -84,7 +87,8 @@ def get_subset_diff(main_set: Json, expected_subset: Json) -> List[DiffItem]: # If some element from expect *sub*set was added, it is an expected behavior. All other operations # are returned. return [ - it for it in json_diff(source=expected_subset, target=main_set) if it.op != Op.Add + it for it in json_diff(source=expected_subset, target=main_set) + if it.op != Op.AddField ] @@ -101,8 +105,13 @@ def describe_diff( if it.op == Op.Remove: description += f"{source_name} has extra entry" - elif it.op == Op.Add: - description += f"{target_name} missing entry" + + elif it.op == Op.AddField: + description += f"{target_name} missing field" + + elif it.op == Op.AppendItem: + description += f"{target_name} missing list item" + elif it.op == Op.Replace: description += "changed entry" @@ -115,7 +124,10 @@ def get_path(value: Json) -> Json: if it.op == Op.Remove: description += f" {json.dumps(get_path(source), indent=2)}" - elif it.op == Op.Add: + elif it.op == Op.AddField: + description += f" {json.dumps(it.value, indent=2)}" + + elif it.op == Op.AppendItem: description += f" {json.dumps(it.value, indent=2)}" elif it.op == Op.Replace: diff --git a/tests/python/repo/coverage_corpus/test_file_segmentation_1.cpp b/tests/python/repo/coverage_corpus/test_file_segmentation_1.cpp index 6e3996727..31e834c7c 100644 --- a/tests/python/repo/coverage_corpus/test_file_segmentation_1.cpp +++ b/tests/python/repo/coverage_corpus/test_file_segmentation_1.cpp @@ -31,6 +31,11 @@ void coverage_branches(int arg) { } else { action(); } + + if (arg == 0 || arg == 1 || arg == 2 + || ((arg <= 5 || arg <= 5) && arg < 10)) { + action(); + } } diff --git a/tests/python/repo/test_code_coverage.py b/tests/python/repo/test_code_coverage.py index 323412294..43cc9a2f0 100644 --- a/tests/python/repo/test_code_coverage.py +++ b/tests/python/repo/test_code_coverage.py @@ -326,6 +326,7 @@ def test_file_segmentation_1(): session = open_sqlite_session(cmd.get_sqlite(), cov.CoverageSchema) main_cov = cov.get_coverage_of(session, cmd.get_code("main.cpp")) lines = code.split("\n") + # print(format_db_all(session)) segtree = cov.CoverageSegmentTree(it[0] for it in session.execute(main_cov)) df = pd.read_sql(main_cov, session.get_bind()) @@ -333,7 +334,7 @@ def test_file_segmentation_1(): # print(render_rich(dataframe_to_rich_table(df))) - # Coverage segments only overlay executable blocks and do not + # Coverage segments only overlay executable blocks and do not # account for extraneous elements such as function headers etc. assert segtree.query(line=1, col=15) assert not segtree.query(line=1, col=14) @@ -341,6 +342,24 @@ def test_file_segmentation_1(): dict(IsLeaf=True, Text="{}", ColStart=15, ColEnd=17), ]) + # print(render_rich(dataframe_to_rich_table(df[df["IsBranch"] == True]))) + assert_frame(df[df["IsBranch"] == True], [ + dict(LineStart=3, ColStart=9, LineEnd=3, ColEnd=16, Text="arg > 0"), + dict(LineStart=4, ColStart=13, LineEnd=4, ColEnd=21, Text="arg > 10"), + dict(LineStart=5, ColStart=17, LineEnd=5, ColEnd=25, Text="arg > 20"), + dict(LineStart=11, ColStart=17, LineEnd=11, ColEnd=24, Text="arg > 5"), + dict(LineStart=17, ColStart=16, LineEnd=17, ColEnd=23, Text="arg < 0"), + dict(LineStart=18, ColStart=13, LineEnd=18, ColEnd=22, Text="arg < -10"), + dict(LineStart=19, ColStart=17, LineEnd=19, ColEnd=26, Text="arg < -20"), + dict(LineStart=25, ColStart=17, LineEnd=25, ColEnd=25, Text="arg < -5"), + dict(LineStart=35, ColStart=9, LineEnd=35, ColEnd=17, Text="arg == 0"), + dict(LineStart=35, ColStart=21, LineEnd=35, ColEnd=29, Text="arg == 1"), + dict(LineStart=35, ColStart=33, LineEnd=35, ColEnd=41, Text="arg == 2"), + dict(LineStart=36, ColStart=14, LineEnd=36, ColEnd=22, Text="arg <= 5"), + dict(LineStart=36, ColStart=26, LineEnd=36, ColEnd=34, Text="arg <= 5"), + dict(LineStart=36, ColStart=39, LineEnd=36, ColEnd=47, Text="arg < 10"), + ]) + def test_file_segmentation_2(): with TemporaryDirectory() as tmp: