Skip to content

Commit e1fa9ab

Browse files
docs: Added tests to packaging
Added integration tests to the packaging. Added docstrings to functions inside the extension. Formatting also done.
1 parent 0af25cb commit e1fa9ab

File tree

6 files changed

+259
-43
lines changed

6 files changed

+259
-43
lines changed

docs/BUILD

+4-5
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ load("@rules_python//python:pip.bzl", "compile_pip_requirements")
4646
load("@rules_python//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")
4747
load("//tools/feature_flags:feature_flags.bzl", "define_feature_flags")
4848

49-
5049
define_feature_flags(name = "filter_tags")
5150

5251
# all_requirements include esbonio which we don't need most of the time.
@@ -65,24 +64,24 @@ sphinx_docs(
6564
config = ":conf.py",
6665
extra_opts = [
6766
"--keep-going",
68-
"-Dfilter_tags_file_path=$(location :filter_tags)"
67+
"-Dfilter_tags_file_path=$(location :filter_tags)",
6968
],
7069
formats = [
7170
"html",
7271
],
7372
sphinx = ":sphinx_build",
74-
tools = [":filter_tags"],
7573
tags = [
7674
"manual",
7775
],
76+
tools = [":filter_tags"],
7877
)
7978

8079
sphinx_build_binary(
8180
name = "sphinx_build",
8281
deps = sphinx_requirements + [
8382
":extensions",
8483
"//docs/_tooling/sphinx_extensions",
85-
"//docs/_tooling/sphinx_extensions/sphinx_extensions/build:modularity"
84+
"//docs/_tooling/sphinx_extensions/sphinx_extensions/build:modularity",
8685
],
8786
)
8887

@@ -135,7 +134,7 @@ py_binary(
135134
deps = sphinx_requirements + [
136135
":extensions",
137136
"//docs/_tooling/sphinx_extensions",
138-
"//docs/_tooling/sphinx_extensions/sphinx_extensions/build:modularity"
137+
"//docs/_tooling/sphinx_extensions/sphinx_extensions/build:modularity",
139138
],
140139
)
141140

docs/_tooling/sphinx_extensions/sphinx_extensions/build/modularity/modularity.py

+47-29
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
11
from pprint import pprint
2+
from sphinx.application import Sphinx
23
from sphinx_needs.data import SphinxNeedsData, NeedsInfoType
34
from sphinx_needs.config import NeedsSphinxConfig
5+
from sphinx.environment import BuildEnvironment
46
from sphinx.util import logging
57
from copy import deepcopy
68
import os
79
import json
810

911
logger = logging.getLogger(__name__)
1012

11-
def read_filter_tags(app):
13+
14+
def read_filter_tags(app: Sphinx):
15+
"""
16+
Helper function to read in the 'filter_tags' provided by `feature_flag.bzl`.
17+
18+
Args:
19+
app: The current running Sphinx-Build application
20+
21+
Returns:
22+
- List of tags e.g. ['some-ip','another-tag']
23+
OR
24+
- Empty list if there is an exception when reading the file.
25+
26+
Errors:
27+
If 'filter_tags_file_path' can't be found in the config
28+
"""
29+
# asserting our worldview
30+
assert hasattr(
31+
app.config, "filter_tags_file_path"
32+
), "Config missing filter_tags_file_path, this is mandatory."
1233
print(f"Attempting to read filter tags from: {app.config.filter_tags_file_path}")
1334
try:
1435
with open(app.config.filter_tags_file_path, "r") as f:
@@ -20,50 +41,47 @@ def read_filter_tags(app):
2041
print(f"Could not read filter tags. Error: {e}")
2142
return []
2243

23-
def set_hide(app, env):
44+
45+
def hide_needs(app: Sphinx, env: BuildEnvironment):
46+
"""
47+
Function that hides needs (requirements) if *all* of their tags are in the specified tags to hide.
48+
Also deletes any references to hidden requirements, e.g. in 'satisfies' option.
49+
50+
Args:
51+
app: The current running Sphinx-Build application, this will be supplied automatically
52+
env: The current running BuildEnvironment, this will be supplied automatically
53+
"""
2454
filter_tags = read_filter_tags(app)
2555
Need_Data = SphinxNeedsData(env)
2656
needs = Need_Data.get_needs_mutable()
27-
extra_links = [x['option'] for x in NeedsSphinxConfig(env.config).extra_links]
57+
extra_links = [x["option"] for x in NeedsSphinxConfig(env.config).extra_links]
2858
rm_needs_docs = set()
2959
rm_needs = []
3060
for need_id, need in needs.items():
31-
if need['tags'] and all(tag in filter_tags for tag in need['tags']):
61+
if need["tags"] and all(tag in filter_tags for tag in need["tags"]):
3262
rm_needs.append(need_id)
3363
need["hide"] = True
3464

35-
# Remove references
65+
# Remove references
3666
for need_id in needs:
3767
for opt in extra_links:
3868
needs[need_id][opt] = [x for x in needs[need_id][opt] if x not in rm_needs]
39-
needs[need_id][opt + "_back"] = [x for x in needs[need_id][opt + "_back"] if x not in rm_needs]
69+
needs[need_id][opt + "_back"] = [
70+
x for x in needs[need_id][opt + "_back"] if x not in rm_needs
71+
]
4072

4173

4274
def setup(app):
43-
app.add_config_value('filter_tags', [], 'env', [list])
44-
app.add_config_value('filter_tags_file_path', None, 'env') # Change default from "" to None
45-
75+
app.add_config_value("filter_tags", [], "env", [list])
76+
app.add_config_value(
77+
"filter_tags_file_path", None, "env"
78+
) # Change default from "" to None
79+
4680
# Add validation
47-
def validate_config(app, config):
48-
if not config.filter_tags_file_path:
49-
logger.warning("No filter_tags_file_path configured")
50-
elif not os.path.exists(config.filter_tags_file_path):
51-
logger.error(f"Filter tags file not found at: {config.filter_tags_file_path}")
52-
53-
app.connect('config-inited', validate_config)
54-
app.connect("env-updated", set_hide)
55-
56-
return {
57-
'version': '1.0',
58-
'parallel_read_safe': True,
59-
'parallel_write_safe': True,
60-
}
81+
app.connect("env-updated", hide_needs)
6182

62-
6383
return {
64-
'version': '1.0',
65-
'parallel_read_safe': True,
66-
'parallel_write_safe': True,
84+
"version": "1.0",
85+
"parallel_read_safe": True,
86+
"parallel_write_safe": True,
6787
}
68-
69-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
load("@pip_sphinx//:requirements.bzl", "all_requirements", "requirement")
2+
load("//tools/testing/pytest:defs.bzl", "score_py_pytest")
3+
4+
score_py_pytest(
5+
name = "test_modularity",
6+
size = "small",
7+
srcs = [
8+
"test_modularity.py",
9+
],
10+
args = [
11+
"--basetemp /tmp/pytest",
12+
"-s",
13+
],
14+
imports = ["."],
15+
plugins = [
16+
"sphinx.testing.fixtures",
17+
],
18+
visibility = [
19+
"//visibility:public",
20+
],
21+
deps = [
22+
"//docs/_tooling/sphinx_extensions/sphinx_extensions/build:modularity",
23+
],
24+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import pytest
2+
import json
3+
from pathlib import Path
4+
from sphinx_needs.data import SphinxNeedsData
5+
from sphinx.testing.util import SphinxTestApp
6+
7+
8+
# ╭──────────────────────────────────────────────────────────────────────────────╮
9+
# │ Integration Tests via Sphinx │
10+
# ╰──────────────────────────────────────────────────────────────────────────────╯
11+
12+
13+
@pytest.fixture(scope="session")
14+
def sphinx_base_dir(tmp_path_factory):
15+
return tmp_path_factory.mktemp("sphinx")
16+
17+
18+
@pytest.fixture(scope="session")
19+
def sphinx_app_setup(sphinx_base_dir):
20+
def _create_app(conf_content, rst_content, filter_tags=None):
21+
src_dir = sphinx_base_dir / "src"
22+
src_dir.mkdir(exist_ok=True)
23+
24+
(src_dir / "conf.py").write_text(conf_content)
25+
(src_dir / "index.rst").write_text(rst_content)
26+
(src_dir / "filter_tags.txt").write_text(filter_tags)
27+
28+
app = SphinxTestApp(
29+
freshenv=True,
30+
srcdir=Path(src_dir),
31+
confdir=Path(src_dir),
32+
outdir=sphinx_base_dir / "out",
33+
buildername="html",
34+
warningiserror=True,
35+
confoverrides={"filter_tags_file_path": str(src_dir / "filter_tags.txt")},
36+
)
37+
38+
return app
39+
40+
return _create_app
41+
42+
43+
@pytest.fixture(scope="session")
44+
def positive_filter_tags():
45+
return "test-feat, some-ip"
46+
47+
48+
@pytest.fixture(scope="session")
49+
def negative_filter_tags():
50+
return "tag1, tag2"
51+
52+
53+
@pytest.fixture(scope="session")
54+
def empty_filter_tags():
55+
return ""
56+
57+
58+
@pytest.fixture(scope="session")
59+
def basic_conf():
60+
return """
61+
extensions = [
62+
"sphinx_needs",
63+
"modularity",
64+
]
65+
needs_types = [
66+
dict(directive="test_req", title="Testing Requirement", prefix="TREQ_", color="#BFD8D2", style="node"),
67+
dict(directive="tool_req", title="Tooling Requirement", prefix="TOOL_", color="#BFD8D2", style="node"),
68+
]
69+
needs_extra_links = [
70+
{
71+
"option": "satisfies",
72+
"incoming": "is satisfied by",
73+
"outgoing": "satisfies",
74+
"style_start": "-up",
75+
"style_end": "->",
76+
}
77+
]
78+
"""
79+
80+
81+
@pytest.fixture(scope="session")
82+
def basic_rst_file():
83+
return """
84+
.. tool_req:: Test Tool Requirement
85+
:id: TOOL_REQ_1
86+
:tags: test-feat, feature1
87+
:satisfies: TEST_REQ_1, TEST_REQ_20
88+
89+
Some content for the tool requirement
90+
91+
.. test_req:: Testing Requirement 1
92+
:id: TEST_REQ_1
93+
:tags: test-feat
94+
95+
Some content should be here
96+
97+
.. test_req:: Testing Requirement 20
98+
:id: TEST_REQ_20
99+
:tags: feature1
100+
101+
More content, this one is different
102+
"""
103+
104+
105+
def test_modularity_hide_ok(
106+
sphinx_app_setup, basic_conf, basic_rst_file, positive_filter_tags, sphinx_base_dir
107+
):
108+
app = sphinx_app_setup(basic_conf, basic_rst_file, positive_filter_tags)
109+
try:
110+
app.build()
111+
Needs_Data = SphinxNeedsData(app.env)
112+
needs_data = {x["id"]: x for x in Needs_Data.get_needs_view().values()}
113+
html = (app.outdir / "index.html").read_text()
114+
115+
assert "TOOL_REQ_1" in needs_data
116+
assert "TEST_REQ_1" in needs_data
117+
assert "TEST_REQ_20" in needs_data
118+
assert needs_data["TOOL_REQ_1"]["hide"] == False
119+
assert needs_data["TOOL_REQ_1"]["satisfies"] == ["TEST_REQ_20"]
120+
assert needs_data["TEST_REQ_1"]["hide"] == True
121+
assert needs_data["TEST_REQ_20"]["hide"] == False
122+
123+
assert "TOOL_REQ_1" in html
124+
assert "TEST_REQ_20" in html
125+
# making sure we do not see this in the final HTML
126+
assert "TEST_REQ_1" not in html
127+
finally:
128+
app.cleanup()
129+
130+
131+
def test_modularity_hide_no_match(
132+
sphinx_app_setup, basic_conf, basic_rst_file, negative_filter_tags, sphinx_base_dir
133+
):
134+
app = sphinx_app_setup(basic_conf, basic_rst_file, negative_filter_tags)
135+
try:
136+
app.build()
137+
Needs_Data = SphinxNeedsData(app.env)
138+
needs_data = {x["id"]: x for x in Needs_Data.get_needs_view().values()}
139+
html = (app.outdir / "index.html").read_text()
140+
141+
assert "TOOL_REQ_1" in needs_data
142+
assert "TEST_REQ_1" in needs_data
143+
assert "TEST_REQ_20" in needs_data
144+
assert needs_data["TEST_REQ_1"]["hide"] == False
145+
assert needs_data["TEST_REQ_20"]["hide"] == False
146+
assert needs_data["TOOL_REQ_1"]["hide"] == False
147+
assert needs_data["TOOL_REQ_1"]["satisfies"] == ["TEST_REQ_1", "TEST_REQ_20"]
148+
149+
assert "TOOL_REQ_1" in html
150+
assert "TEST_REQ_1" in html
151+
assert "TEST_REQ_20" in html
152+
finally:
153+
app.cleanup()
154+
155+
156+
def test_modularity_hide_no_filters(
157+
sphinx_app_setup, basic_conf, basic_rst_file, empty_filter_tags, sphinx_base_dir
158+
):
159+
app = sphinx_app_setup(basic_conf, basic_rst_file, empty_filter_tags)
160+
try:
161+
app.build()
162+
Needs_Data = SphinxNeedsData(app.env)
163+
needs_data = {x["id"]: x for x in Needs_Data.get_needs_view().values()}
164+
html = (app.outdir / "index.html").read_text()
165+
166+
# The outcome should be the same as when we found no matches, just making sure it doesn't error and nothing happens
167+
assert "TOOL_REQ_1" in needs_data
168+
assert "TEST_REQ_1" in needs_data
169+
assert "TEST_REQ_20" in needs_data
170+
assert needs_data["TEST_REQ_1"]["hide"] == False
171+
assert needs_data["TEST_REQ_20"]["hide"] == False
172+
assert needs_data["TOOL_REQ_1"]["hide"] == False
173+
assert needs_data["TOOL_REQ_1"]["satisfies"] == ["TEST_REQ_1", "TEST_REQ_20"]
174+
175+
assert "TOOL_REQ_1" in html
176+
assert "TEST_REQ_1" in html
177+
assert "TEST_REQ_20" in html
178+
finally:
179+
app.cleanup()

docs/conf.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,7 @@
3434
# -- General configuration ---------------------------------------------------
3535
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
3636

37-
extensions = [
38-
"sphinx_design",
39-
"sphinx_needs",
40-
"modularity"
41-
]
37+
extensions = ["sphinx_design", "sphinx_needs", "modularity"]
4238

4339
exclude_patterns = [
4440
"Thumbs.db",

0 commit comments

Comments
 (0)