Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion strictdoc/core/project_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Pattern

import toml

Expand Down Expand Up @@ -123,6 +123,7 @@ def __init__(
exclude_source_paths: Optional[List[str]] = None,
grammars: Optional[Dict[str, str]] = None,
test_report_root_dict: Optional[Dict[str, str]] = None,
tree_map_test_path_pattern: Optional[str] = None,
source_nodes: Optional[List[SourceNodesEntry]] = None,
html2pdf_strict: bool = False,
html2pdf_template: Optional[str] = None,
Expand Down Expand Up @@ -293,6 +294,12 @@ def __init__(
self.test_report_root_dict: Dict[str, str] = (
test_report_root_dict if test_report_root_dict is not None else {}
)
self.tree_map_test_path_pattern: Pattern[str] = re.compile(
Comment thread
stanislaw marked this conversation as resolved.
tree_map_test_path_pattern
if tree_map_test_path_pattern is not None
else r"tests/"
)

self.source_nodes: List[SourceNodesEntry] = (
source_nodes if source_nodes is not None else []
)
Expand Down Expand Up @@ -896,6 +903,7 @@ def _load_from_dictionary(
include_source_paths: List[str] = []
exclude_source_paths: List[str] = []
test_report_root_dict: Dict[str, str] = {}
tree_map_test_path_pattern: Optional[str] = None

@stanislaw stanislaw May 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious if there is an opportunity for reusing the already implemented filtering mechanism. Could we use the same approach that is used for include_doc_paths and include_source_paths? These filters are based on PathFilter.

See around these lines:

path_filter_includes = PathFilter(
            include_paths, positive_or_negative=True
        )

This way the new tree_map_test_path_pattern will simply follow the same configuration interface (an array of lines to whitelist) and use the same filtering mechanism.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will look into this!

I would like the properties of "tests are within a certain path" but also allow "this path contains both tests & implementation" to coexist (for example to cover the go convention). I wonder what inline test definitions next to implementation (rust example) would look like, too 🤔

source_nodes: List[SourceNodesEntry] = []
html2pdf_strict: bool = False
html2pdf_template: Optional[str] = None
Expand Down Expand Up @@ -1003,6 +1011,10 @@ def _load_from_dictionary(
assert isinstance(test_report_root_entry_, dict)
test_report_root_dict.update(test_report_root_entry_)

tree_map_test_path_pattern = project_content.get(
"tree_map_test_path_pattern", tree_map_test_path_pattern
)

section_behavior = project_content.get(
"section_behavior", section_behavior
)
Expand Down Expand Up @@ -1051,6 +1063,7 @@ def _load_from_dictionary(
include_source_paths=include_source_paths,
exclude_source_paths=exclude_source_paths,
test_report_root_dict=test_report_root_dict,
tree_map_test_path_pattern=tree_map_test_path_pattern,
source_nodes=source_nodes,
html2pdf_strict=html2pdf_strict,
html2pdf_template=html2pdf_template,
Expand Down
8 changes: 5 additions & 3 deletions strictdoc/features/tree_map/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ def get_node_stats(
node_
)
for source_file_tuple_ in source_files:
if "tests/" in source_file_tuple_[0]:
if project_config.tree_map_test_path_pattern.search(
source_file_tuple_[0]
):
node_stats.child_nodes_with_links_to_test_files = 1
else:
node_stats.child_nodes_with_links_to_source_files = 1
Expand Down Expand Up @@ -547,11 +549,11 @@ def hover_(row_: Any) -> Any:
parts.append(
GraphSection(
title="Requirements coverage with test",
description="""\
description=f"""\
This graph shows which requirements are covered by at least one test.
A requirement is also considered covered if it has child requirements that are
themselves covered by tests. A source file is considered a test file if its path
contains "tests/".
matches "{project_config.tree_map_test_path_pattern.pattern}".

<ul>
<li>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!reports/
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just copied a nearby test, which is not super slim & focused at the new change..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, copying is of course fine but let's reduce it to the bare minimum we need to exercise this new feature.

In particular, let's remove this XML file. And also I think it should be enough to only leave this test function in the test file as it is the only one you are actually testing against:

TEST(MyTestSuiteName, TestName)

(the background for this is that StrictDoc has lots of test boilerplate and I try to reduce how much boilerplate is needed and used by each test)

<testsuite name="(empty)"
tests="5"
failures="0"
disabled="0"
skipped="0"
hostname=""
time="0"
timestamp="2025-03-18T16:42:24"
>
<testcase name="MyTestSuiteName.TestName" classname="MyTestSuiteName.TestName" time="0.00497625" status="run">
<properties/>
<system-out>Running main() from /home/johan/Development/projects/line/build/_deps/googletest-src/googletest/src/gtest_main.cc
Note: Google Test filter = MyTestSuiteName.TestName
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from MyTestSuiteName
[ RUN ] MyTestSuiteName.TestName
[ OK ] MyTestSuiteName.TestName (0 ms)
[----------] 1 test from MyTestSuiteName (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
</system-out>
</testcase>
<testcase name="MyTestHelper.TestName2" classname="MyTestHelper.TestName2" time="0.00496465" status="run">
<properties/>
<system-out>Running main() from /home/johan/Development/projects/line/build/_deps/googletest-src/googletest/src/gtest_main.cc
Note: Google Test filter = MyTestHelper.TestName2
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from MyTestHelper
[ RUN ] MyTestHelper.TestName2
[ OK ] MyTestHelper.TestName2 (0 ms)
[----------] 1 test from MyTestHelper (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
</system-out>
</testcase>
<testcase name="MyTestPattern/MyTestHelperPattern.TestName3/1" classname="MyTestPattern/MyTestHelperPattern.TestName3/1" time="0.00425819" status="run">
<properties/>
<system-out>Running main() from /home/johan/Development/projects/line/build/_deps/googletest-src/googletest/src/gtest_main.cc
Note: Google Test filter = MyTestPattern/MyTestHelperPattern.TestName3/0
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from MyTestPattern/MyTestHelperPattern
[ RUN ] MyTestPattern/MyTestHelperPattern.TestName3/0
[ OK ] MyTestPattern/MyTestHelperPattern.TestName3/0 (0 ms)
[----------] 1 test from MyTestPattern/MyTestHelperPattern (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
</system-out>
</testcase>
<testcase name="MyTestPattern/MyTestHelperPattern.TestName3/2" classname="MyTestPattern/MyTestHelperPattern.TestName3/2" time="0.00458189" status="run">
<properties/>
<system-out>Running main() from /home/johan/Development/projects/line/build/_deps/googletest-src/googletest/src/gtest_main.cc
Note: Google Test filter = MyTestPattern/MyTestHelperPattern.TestName3/1
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from MyTestPattern/MyTestHelperPattern
[ RUN ] MyTestPattern/MyTestHelperPattern.TestName3/1
[ OK ] MyTestPattern/MyTestHelperPattern.TestName3/1 (0 ms)
[----------] 1 test from MyTestPattern/MyTestHelperPattern (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
</system-out>
</testcase>
<testcase name="MyTestPattern/MyTestHelperPattern.TestName3/3" classname="MyTestPattern/MyTestHelperPattern.TestName3/3" time="0.00450156" status="run">
<properties/>
<system-out>Running main() from /home/johan/Development/projects/line/build/_deps/googletest-src/googletest/src/gtest_main.cc
Note: Google Test filter = MyTestPattern/MyTestHelperPattern.TestName3/2
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from MyTestPattern/MyTestHelperPattern
[ RUN ] MyTestPattern/MyTestHelperPattern.TestName3/2
[ OK ] MyTestPattern/MyTestHelperPattern.TestName3/2 (0 ms)
[----------] 1 test from MyTestPattern/MyTestHelperPattern (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
</system-out>
</testcase>
</testsuite>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]

features = [
"REQUIREMENT_TO_SOURCE_TRACEABILITY",
"SOURCE_FILE_LANGUAGE_PARSERS",
"TREE_MAP_SCREEN",
]

tree_map_test_path_pattern = "verification/"
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This test mainly verifies that the test results are resolved for multiple test
# cases and multiple files.

RUN: %strictdoc export %S --output-dir %T | filecheck %s --dump-input=fail

# Ensure that the test report document is generated.
CHECK: Published: Test report: (empty)
RUN: %check_exists --file "%T/html/%THIS_TEST_FOLDER/reports/tests_unit.ctest.junit.html"

# Ensure that the source and test files are generated.
RUN: %check_exists --file "%T/html/_source_files/verification/test1.cpp.html"

# Ensure that the test report document has the right content.
RUN: %cat "%T/html/%THIS_TEST_FOLDER/reports/tests_unit.ctest.junit.html" | filecheck %s --check-prefix CHECK-TEST-REPORT
CHECK-TEST-REPORT:href="../../_source_files/verification/test1.cpp.html#MyTestSuiteName.TestName#17#21">
CHECK-TEST-REPORT: verification/test1.cpp, <i>lines: 17-21</i>, function TEST(MyTestSuiteName, TestName)

RUN: %cat "%T/html/tree_map.html" | filecheck %s --check-prefix CHECK-TREE-MAP
CHECK-TREE-MAP: matches "verification/"
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include <gtest/gtest.h>

//
// Code that we want to test
//

int Multiplication(int value1, int value2)
{
// Multiply like a boss
return value1 * value2;
}

//
// GTest code below
//

TEST(MyTestSuiteName, TestName)
{
// Test it
EXPECT_EQ(Multiplication(2, 2), 4);
}

class MyTestHelper : public testing::Test
{
protected:
MyTestHelper()
{
// Setup MyTestHelper
}
};

TEST_F(MyTestHelper, TestName2)
{
// Test it
EXPECT_EQ(Multiplication(1, 4), 4);
}

class MyTestHelperPattern : public testing::TestWithParam<int>
{
};

INSTANTIATE_TEST_SUITE_P(MyTestPattern, MyTestHelperPattern, testing::Values(1, 2, 3));

TEST_P(MyTestHelperPattern, TestName3)
{
// Test it
EXPECT_NE(Multiplication(GetParam(), GetParam()), 0);
}
26 changes: 26 additions & 0 deletions tests/unit/strictdoc/core/test_project_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
import re

from strictdoc.core.project_config import ProjectConfig, ProjectFeature

Expand Down Expand Up @@ -61,3 +62,28 @@ def test_61_validate_invalid_host():
def test_62_validate_invalid_port():
with pytest.raises(AssertionError):
_ = ProjectConfig(server_port=1000000)


def test_100_tree_map_test_path_pattern_default():
default_config = ProjectConfig()
assert default_config.tree_map_test_path_pattern.pattern == "tests/"


def test_101_tree_map_test_path_pattern_custom():
explicitly_provided = ProjectConfig(tree_map_test_path_pattern="custom/")
assert explicitly_provided.tree_map_test_path_pattern.pattern == "custom/"


def test_102_tree_map_test_path_pattern_bad_regex():
with pytest.raises(re.error):
_ = ProjectConfig(tree_map_test_path_pattern="(unclosed parenthesis")


def test_103_tree_map_test_path_pattern_enables_multiple_filename_styles():
explicitly_provided = ProjectConfig(
tree_map_test_path_pattern=r"(^test/)|_test\.go$"
)
pat = explicitly_provided.tree_map_test_path_pattern
assert pat.search("test/some_test.py")
assert pat.search("main_test.go")
assert not pat.search("main.go")