With VNF Modularity, a single VNF may be composed from one or more Heat
Orchestration Templates, each of which represents a subset of the
-overall VNF. These component parts are referred to as “\ *VNF
-Modules*\ ”. During orchestration, these modules are deployed
+overall VNF. These component parts are referred to as "\ *VNF
+Modules*\ ". During orchestration, these modules are deployed
incrementally to create the complete VNF.
A modular Heat Orchestration Template can be either one of the following
referred to as *VNF Modularity.* With this approach, a single VNF may be
composed from one or more Heat Orchestration Templates, each of which
represents a subset of the overall VNF. These component parts are
-referred to as “\ *VNF Modules*\ ”. During orchestration, these modules
+referred to as "\ *VNF Modules*\ ". During orchestration, these modules
are deployed incrementally to create the complete VNF.
A modular Heat Orchestration Template can be either one of the following
3. Cinder Volume Module
-A VNF must be composed of one “base” module and may be composed of zero
-to many “incremental” modules. The base module must be deployed first,
+A VNF must be composed of one "base" module and may be composed of zero
+to many "incremental" modules. The base module must be deployed first,
prior to the incremental modules.
ONAP also supports the concept of an optional, independently deployed
ii. May be separated by VM type for multi-dimensional scaling
-With no growth units, Option 2 is equivalent to the “One Heat Template
-per VNF” model.
+With no growth units, Option 2 is equivalent to the "One Heat Template
+per VNF" model.
Note that modularization of VNFs is not required. A single Heat
Orchestration Template (a base module) may still define a complete VNF,
a. Must include all shared resources (e.g., private networks, server
groups, security groups)
- b. Must expose all shared resources (by UUID) as “outputs” in its
+ b. Must expose all shared resources (by UUID) as "outputs" in its
associated Heat template (i.e., ONAP Base Module Output
Parameters)
c. May include initial set of VMs
- d. May be operational as a stand-alone “minimum” configuration of the
+ d. May be operational as a stand-alone "minimum" configuration of the
VNF
2. VNFs may have one or more incremental modules which:
ii. must not be dependent on other Add-On VNF Modules
e. Multiple instances of an incremental Module may be added to the
- same VNF (e.g., incrementally grow a VNF by a fixed “add-on”
+ same VNF (e.g., incrementally grow a VNF by a fixed "add-on"
growth units)
3. Each VNF Module (base or incremental) may have (optional) an
by applying techniques such as caching and persistent transaction paths
- Eliminate replication delay impact between data centers by using a
concept of stickiness (i.e., once a client is routed to data center "A",
-the client will stay with Data center “A” until the entire session is
+the client will stay with Data center "A" until the entire session is
completed).
Minimize Cross Data-Center Traffic Requirements
:target: VNF
:keyword: MUST
:validation_mode: static
- :updated: casablanca
+ :introduced: casablanca
A VNF's Heat Orchestration Template's Cinder Volume Template **MUST**
contain either
:id: R-92635
:keyword: MUST
:validation_mode: static
+ :introduced: casablanca
A VNF's Heat Orchestration Template **MUST** be compliant with the
OpenStack Template Guide.
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF Heat Orchestration's template's parameter **MUST** be used
in a resource with the exception of the parameters for the
:id: R-91273
:target: VNF
:keyword: MAY NOT
+ :updated: casablanca
A VNF Heat Orchestration's template's parameter for the
``OS::Nova::Server`` resource property ``availability_zone``
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF's Heat Orchestration Template's parameter type **MUST** be one of
the following values:
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
If a VNF Heat Orchestration Template parameter has a default value,
it **MUST** be enumerated in the environment file.
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF's Heat Orchestration Template's parameter defined
in a non-nested YAML file as type
:id: R-40518
:target: VNF
:keyword: MAY
+ :updated: casablanca
A VNF's Heat Orchestration Template's parameter defined
in a non-nested YAML file as type
:id: R-96227
:target: VNF
:keyword: MAY
+ :updated: casablanca
A VNF's Heat Orchestration Template's parameter defined
in a non-nested YAML file as type
:id: R-79817
:target: VNF
:keyword: MAY
+ :updated: casablanca
A VNF's Heat Orchestration Template's parameter defined
in a non-nested YAML file as
:id: R-06613
:target: VNF
:keyword: MAY
+ :updated: casablanca
A VNF's Heat Orchestration Template's parameter defined
in a non-nested YAML file as type
:target: VNF
:keyword: MUST NOT
:validation_mode: static
+ :updated: casablanca
A VNF's Heat Orchestration Template's parameter defined
in a nested YAML file
:id: R-40551
:target: VNF
:keyword: MAY
+ :updated: casablanca
A VNF's Heat Orchestration Template's Nested YAML files **MAY**
(or **MAY NOT**) contain the section ``resources:``.
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
If a VNF's Heat Orchestration Template resource attribute
``property:`` uses a nested ``get_param``, the nested
:target: VNF
:keyword: MUST
:validation_mode: static
+ :introduced: casablanca
A VNF's Heat Orchestration Template's Resource **MAY** declare the
attribute ``metadata``.
:id: R-43740
:target: VNF
:keyword: MAY
+ :updated: casablanca
VNF's Heat Orchestration Template's Resource **MAY** declare the
attribute ``deletion_policy:``.
:id: R-78569
:target: VNF
:keyword: MAY
+ :updated: casablanca
VNF's Heat Orchestration Template's Resource **MAY** declare the
attribute ``external_id:``.
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF's Heat Orchestration template **MUST** have a
corresponding environment file.
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF's Heat Orchestration template's Environment File **MUST**
contain the ``parameters:`` section.
:id: R-68198
:target: VNF
:keyword: MAY
+ :updated: casablanca
A VNF's Heat Orchestration template's Environment File's
``parameters:`` section **MAY** (or **MAY NOT**) enumerate parameters.
:id: R-33132
:target: VNF
:keyword: MAY
+ :updated: casablanca
A VNF's Heat Orchestration Template **MAY** be
1.) Base Module Heat Orchestration Template (also referred to as a
:id: R-37028
:target: VNF
:keyword: MUST
+ :updated: casablanca
A VNF **MUST** be composed of one Base Module
:id: R-20974
:target: VNF
:keyword: MUST
+ :updated: casablanca
At orchestration time, the VNF's Base Module **MUST**
be deployed first, prior to any incremental modules.
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF's Cinder Volume Module, when it exists, **MUST** be 1:1
with a Base module or Incremental module.
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF's Base Module **MUST** have a corresponding Environment File.
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF's Incremental Module **MUST** have a corresponding Environment File
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF's Cinder Volume Module **MUST** have a corresponding environment file
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF Heat Orchestration Template's Base Module file name **MUST** include
case insensitive 'base' in the filename and
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
VNF Heat Orchestration Template's Incremental Module file name
**MUST** contain only alphanumeric characters and underscores
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF Heat Orchestration Template's Cinder Volume Module **MUST**
be named identical to the base or incremental module it is supporting with
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
VNF Heat Orchestration Template's Cinder Volume Module's Environment File
**MUST** be named identical to the VNF Heat Orchestration Template's
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
VNF Heat Orchestration Template's Nested YAML file name **MUST** contain
only alphanumeric characters and underscores '_' and
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF's Heat Orchestration Template's Cinder Volume Module Output
Parameter(s)
:target: VNF
:keyword: MUST
:validation_mode: static
+ :updated: casablanca
A VNF's Heat Orchestration Templates' Cinder Volume Module Output
Parameter's name and type **MUST** match the input parameter name and type
:target: VNF
:keyword: MUST
:validation_mode: static
- :updated: casablanca
+ :introduced: casablanca
A VNF's Heat Orchestration Template's ``OS::Nova::Server`` resource's
:target: VNF
:keyword: MUST
:validation_mode: static
- :updated: casablanca
+ :introduced: casablanca
The VNF's Heat Orchestration Template's Resource ``OS::Nova::Server``
property ``image`` value **MUST** be be obtained via a ``get_param``.
:target: VNF
:keyword: MUST
:validation_mode: static
- :updated: casablanca
+ :introduced: casablanca
The VNF's Heat Orchestration Template's Resource ``OS::Nova::Server``
property ``flavor`` value **MUST** be be obtained via a ``get_param``.
:target: VNF
:keyword: MUST
:validation_mode: static
- :updated: casablanca
+ :introduced: casablanca
The VNF's Heat Orchestration Template's Resource ``OS::Nova::Server``
property ``name`` value **MUST** be be obtained via a ``get_param``.
NFVIFA(17)000110 discussion paper)
**[editor note]** new relationship type as suggested in Matt
-presentation. Slide 8. With specific rules of “valid\_target\_type”
+presentation. Slide 8. With specific rules of "valid\_target\_type"
+---------------------------+--------------------------------------+
| **Shorthand Name** | VirtualStorage |
:keyword: SHOULD
:impacts: dcae
:validation_mode: in_service
- :introduced: casblanca
+ :introduced: casablanca
The xNF **SHOULD** deliver event records that fall into the event domains
supported by VES.
:keyword: MUST
:impacts: dcae
:validation_mode: in_service
- :introduced: casblanca
+ :introduced: casablanca
The xNF **MUST** deliver event records to ONAP using the common transport
mechanisms and protocols defined in this document.
:keyword: MUST
:impacts: dcae
:validation_mode: none
- :introduced: casblanca
+ :introduced: casablanca
The xNF provider **MUST** reach agreement with the Service Provider on
the selected methods for encoding, serialization and data delivery
:id: R-19624
:target: XNF
:keyword: MUST
+ :updated: casablanca
The xNF, when leveraging JSON for events, **MUST** encode and serialize
content delivered to ONAP using JSON (RFC 7159) plain text format.
:keyword: SHOULD
:impacts: dcae
:validation_mode: in_service
+ :introduced: casablanca
The xNF **SHOULD** deliver all syslog messages to the VES Collector per the
specifications in Monitoring and Management chapter.
:id: R-43958
:target: XNF
:keyword: MUST
+ :updated: casablanca
The xNF Package **MUST** include documentation describing
the tests that were conducted by the xNF provider and the test results.
| | value pairs to be | |Attribute names (variable |
| | passed to the Ansible| |names) passed to Ansible |
| | playbook. These | |shall follow Ansible valid |
-| | values would | |variable names: “Variable |
+| | values would | |variable names: "Variable |
| | correspond to | |names should be letters, |
| | instance specific | |numbers, and underscores. |
| | parameters that a | |Variables should always |
-| | playbook may need to | |start with a letter.” |
+| | playbook may need to | |start with a letter." |
| | execute an action. | | |
+---------------+----------------------+---------+----------------------------+
| NodeList |Ansible inventory | Optional|If not provided, pre-loaded |
In the above example, the Ansible Server will:
-a. Process the “FileParameters” dictionary and generate a file named
+a. Process the "FileParameters" dictionary and generate a file named
‘config.txt’ with contents set to the value of the ‘config.txt’ key.
b. Execute the playbook named ‘<VNFCode>/<Version>/ansible/configure/site.yml’
<VNF type>/<Version>/ansible/inventory/group_vars/<VNF instance name>
NOTE: Default groups will be created based on VNFC type, 3 characters,
-on VNFC name. Example: “oam”, “rdb”, “dbs”, “man”, “iox”, “app”,…
+on VNFC name. Example: "oam", "rdb", "dbs", "man", "iox", "app",…
Ansible Directories for other artifacts – VNF (special) other files –
Optional – Example – License file:
a. Includes VNF type using VNF function code 4 characters under
/storage.
- b. Includes VNF “Version” directory as part of the path to store
+ b. Includes VNF "Version" directory as part of the path to store
playbooks for this VNF version.
c. Include generic ansible root directory. Creating full directory
vm\_config\_rdb4\_hostname: vfdb9904vm006
vm\_config\_rdb4\_provider\_ip\_address: 1xx.2yy.zzz.yyy
-NOTE: Please note names in this file shall use underscore “\_” not dots
-“.” or dashes “-“.
+NOTE: Please note names in this file shall use underscore "\_" not dots
+"." or dashes "-".
-7. Perform some basic playbook validation running with “--check” option,
+7. Perform some basic playbook validation running with "--check" option,
running dummy playbooks or other.
NOTE: Each Ansible Server or cluster of Ansible Server will have its own
| | as part of the desired | | |
| | VNF action. | | |
+----------------+--------------------------+---------+----------------------+
-| PushJobFlag | This field indicates |Mandatory| If set to “True”, |
+| PushJobFlag | This field indicates |Mandatory| If set to "True", |
| | whether the VNF action | | ONAP will request a |
| | requires a push Job. Push| | push job. Ignored |
| | job object will be | | otherwise. |
| CallbackCapable| This field indicates if | Optional| If Chef cookbook is |
| | the chef-client run | | callback capable, VNF|
| | invoked by push job | | owner is required to |
-| | corresponding to the VNF | | set it to “True”. |
+| | corresponding to the VNF | | set it to "True". |
| | action is capable of | | Ignored otherwise. |
| | posting results on a | | |
| | callback URL. | | |
| | retrieve output generated| | NodeObject attributes|
| | in a chef-client run from| | [‘PushJobOutput’] for|
| | Node object attribute | | all nodes in NodeList|
-| | node[‘PushJobOutput’] for| | if set to “True”. |
+| | node[‘PushJobOutput’] for| | if set to "True". |
| | this VNF action (e.g., in| | Ignored otherwise. |
| | Audit). | | |
+----------------+--------------------------+---------+----------------------+
.. code-block:: erb
- “Environment”:{
+ "Environment":{
"name": "HAR",
"description": "VNF Chef environment for HAR",
"json\_class": "Chef::Environment",
"chef\_type": "environment",
"default\_attributes": { },
"override\_attributes": {
- “Retry\_Time”:”50”,
- “MemCache”: “1024”,
- “Database\_IP”:”10.10.1.5”
+ "Retry\_Time":"50",
+ "MemCache": "1024",
+ "Database\_IP":"10.10.1.5"
},
}
}
- “Node”: {
- “name” : “signal.network.com “
+ "Node": {
+ "name" : "signal.network.com "
"chef\_type": "node",
"json\_class": "Chef::Node",
"attributes": {
- “IPAddress1”: “192.168.1.2”,
- “IPAddress2”:”135.16.162.5”,
- “MyRole”:”BE”
+ "IPAddress1": "192.168.1.2",
+ "IPAddress2":"135.16.162.5",
+ "MyRole":"BE"
},
"override": {},
"default": {},
- “normal”:{},
- “automatic”:{},
- “chef\_environment” : “\_default”
+ "normal":{},
+ "automatic":{},
+ "chef\_environment" : "\_default"
"run\_list": [ "configure\_signal" ]
},
- “NodeList”:[“node1.vnf\_a.onap.com”, “node2.vnf\_a.onap.com”],
- “PushJobFlag”: “True”
- “CallbackCapable”:True
- “GetOutputFlag” : “False”
+ "NodeList":["node1.vnf\_a.onap.com", "node2.vnf\_a.onap.com"],
+ "PushJobFlag": "True"
+ "CallbackCapable":True
+ "GetOutputFlag" : "False"
}
The example JSON file provided by the VNF provider for each VNF action will be
a. The JSON file must be created for each action for each VNF.
b. If a VNF action involves multiple endpoints (VMs) of a VNF, ONAP will
- replicate the “Node” JSON dictionary in the template and post it to
- each FQDN (i.e., endpoint) in the NodeList after setting the “name”
+ replicate the "Node" JSON dictionary in the template and post it to
+ each FQDN (i.e., endpoint) in the NodeList after setting the "name"
field in the Node object to be the respective FQDN [#8.1.1]_. Hence, it
is required that all end points (VMs) of a VNF involved in a VNF
action support the same set of Node Object attributes.
+--------------+----------------------------+---------+-----------------------+
.. [#8.1.1]
- The “name” field is a mandatory field in a valid Chef Node Object
+ The "name" field is a mandatory field in a valid Chef Node Object
JSON dictionary.
sphinxcontrib-plantuml
xlwt==1.3.0
PyYAML>=3.10,<4
+pytest
--- /dev/null
+# -*- coding: utf8 -*-
+# org.onap.vnfrqts/requirements
+# ============LICENSE_START====================================================
+# Copyright © 2018 AT&T Intellectual Property. All rights reserved.
+#
+# Unless otherwise specified, all software contained herein is licensed
+# under the Apache License, Version 2.0 (the "License");
+# you may not use this software except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Unless otherwise specified, all documentation contained herein is licensed
+# under the Creative Commons License, Attribution 4.0 Intl. (the "License");
+# you may not use this documentation except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://creativecommons.org/licenses/by/4.0/
+#
+# Unless required by applicable law or agreed to in writing, documentation
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# ============LICENSE_END============================================
+
+"""
+This script will consume the `invalid_metadata.csv` file produced by
+`gen_requirement_changes.py`, then add/update any `:introduced:` or `:updated:`
+attributes that may be missing from req directives.
+"""
+import csv
+import os
+import re
+from collections import OrderedDict
+
+import pytest
+
+INPUT_FILE = "invalid_metadata.csv"
+
+
+def load_invalid_reqs(fileobj):
+ """Load the invalid requirements from the input file into a dict"""
+ reader = csv.reader(fileobj)
+ next(reader) # skip header
+ return {row[0]: (row[1].strip(), row[2].strip()) for row in reader}
+
+
+def check(predicate, msg):
+ """Raises a RuntimeError with the given msg if the predicate is false"""
+ if not predicate:
+ raise RuntimeError(msg)
+
+
+class MetadataFixer:
+ """Takes a dict of requirement ID to expected metadata value. The
+ NeedsVisitor will pass the requirement attributes as a a dict
+ to `__call__`. If the requirement is one that needs to be fixed, then
+ it will add or update the attributes as needed and return it to the
+ visitor, otherwise it will return the attributes unchanged."""
+
+ def __init__(self, reqs_to_fix):
+ """Initialize the fixer with a dict of requirement ID to tuple of
+ (attr name, attr value)."""
+ self.reqs_to_fix = reqs_to_fix
+
+ def __call__(self, metadata):
+ """If metadata is for a requirement that needs to be fixed, then
+ adds or updates the attribute as needed and returns it, otherwise
+ it returns metadata unchanged."""
+ r_id = metadata[":id:"]
+ if r_id in self.reqs_to_fix:
+ attr, value = self.reqs_to_fix[r_id]
+ metadata[attr] = value
+ return metadata
+
+
+class NeedsVisitor:
+ """Walks a directory for reStructuredText files and detects needs
+ directives as defined by sphinxcontrib-needs. When a directive is
+ found, then attributes are passed to a callback for processing if the
+ callback returns a dict of attributes, then the revised dict is used
+ instead of the attributes that were passed"""
+
+ def __init__(self, func):
+ self.directives = re.compile("\.\.\s+req::.*")
+ self.func = func
+
+ def process(self, root_dir):
+ """Walks the `root_dir` looking for rst to files to parse"""
+ for dir_path, sub_dirs, filenames in os.walk(root_dir):
+ for filename in filenames:
+ if filename.lower().endswith(".rst"):
+ self.handle_rst_file(os.path.join(dir_path, filename))
+
+ @staticmethod
+ def read(path):
+ """Read file at `path` and return lines as list"""
+ with open(path, "r") as f:
+ print("path=", path)
+ return list(f)
+
+ @staticmethod
+ def write(path, content):
+ """Write a content to the given path"""
+ with open(path, "w") as f:
+ for line in content:
+ f.write(line)
+
+ def handle_rst_file(self, path):
+ lines = (line for line in self.read(path))
+ new_contents = []
+ for line in lines:
+ if self.is_needs_directive(line):
+ metadata_lines = self.handle_need(lines)
+ new_contents.append(line)
+ new_contents.extend(metadata_lines)
+ else:
+ new_contents.append(line)
+ self.write(path, new_contents)
+
+ def is_needs_directive(self, line):
+ """Returns True if the line denotes the start of a needs directive"""
+ return bool(self.directives.match(line))
+
+ def handle_need(self, lines):
+ """Called when a needs directive is encountered. The lines
+ will be positioned on the line after the directive. The attributes
+ will be read, and then passed to the visitor for processing"""
+ attributes = OrderedDict()
+ indent = 4
+ for line in lines:
+ if line.strip().startswith(":"):
+ indent = self.calc_indent(line)
+ attr, value = self.parse_attribute(line)
+ attributes[attr] = value
+ else:
+ if attributes:
+ new_attributes = self.func(attributes)
+ attr_lines = self.format_attributes(new_attributes, indent)
+ return attr_lines + [line]
+ else:
+ return [line]
+
+ @staticmethod
+ def format_attributes(attrs, indent):
+ """Converts a dict back to properly indented lines"""
+ spaces = " " * indent
+ return ["{}{} {}\n".format(spaces, k, v) for k, v in attrs.items()]
+
+ @staticmethod
+ def parse_attribute(line):
+ return re.split("\s+", line.strip(), maxsplit=1)
+
+ @staticmethod
+ def calc_indent(line):
+ return len(line) - len(line.lstrip())
+
+
+if __name__ == '__main__':
+ with open(INPUT_FILE, "r") as f:
+ invalid_reqs = load_invalid_reqs(f)
+ metadata_fixer = MetadataFixer(invalid_reqs)
+ visitor = NeedsVisitor(metadata_fixer)
+ visitor.process("docs")
+
+
+# Tests
+@pytest.fixture
+def metadata_fixer():
+ fixes = {
+ "R-1234": (":introduced:", "casablanca"),
+ "R-5678": (":updated:", "casablanca"),
+ }
+ return MetadataFixer(fixes)
+
+
+def test_check_raises_when_false():
+ with pytest.raises(RuntimeError):
+ check(False, "error")
+
+
+def test_check_does_not_raise_when_true():
+ check(True, "error")
+
+
+def test_load_invalid_req():
+ contents = [
+ "reqt_id, attribute, value",
+ "R-1234,:introduced:, casablanca",
+ "R-5678,:updated:, casablanca",
+ ]
+ result = load_invalid_reqs(contents)
+ assert len(result) == 2
+ assert result["R-1234"][0] == ":introduced:"
+ assert result["R-1234"][1] == "casablanca"
+
+
+def test_metadata_fixer_adds_when_missing(metadata_fixer):
+ attributes = {":id:": "R-5678", ":introduced:": "beijing"}
+ result = metadata_fixer(attributes)
+ assert ":updated:" in result
+ assert result[":updated:"] == "casablanca"
+
+
+def test_metadata_fixer_updates_when_incorrect(metadata_fixer):
+ attributes = {":id:": "R-5678", ":updated:": "beijing"}
+ result = metadata_fixer(attributes)
+ assert ":updated:" in result
+ assert result[":updated:"] == "casablanca"
+ assert ":introduced:" not in result
+
+
+def test_needs_visitor_process(monkeypatch):
+ v = NeedsVisitor(lambda x: x)
+ paths = []
+
+ def mock_handle_rst(path):
+ paths.append(path)
+
+ monkeypatch.setattr(v, "handle_rst_file", mock_handle_rst)
+ v.process("docs")
+
+ assert len(paths) > 1
+ assert all(path.endswith(".rst") for path in paths)
+
+
+def test_needs_visitor_is_needs_directive():
+ v = NeedsVisitor(lambda x: x)
+ assert v.is_needs_directive(".. req::")
+ assert not v.is_needs_directive("test")
+ assert not v.is_needs_directive(".. code::")
+
+
+def test_needs_visitor_format_attributes():
+ v = NeedsVisitor(lambda x: x)
+ attr = OrderedDict()
+ attr[":id:"] = "R-12345"
+ attr[":updated:"] = "casablanca"
+ lines = v.format_attributes(attr, 4)
+ assert len(lines) == 2
+ assert lines[0] == " :id: R-12345"
+ assert lines[1] == " :updated: casablanca"
+
+
+def test_needs_visitor_parse_attributes():
+ v = NeedsVisitor(lambda x: x)
+ assert v.parse_attribute(" :id: R-12345") == [":id:", "R-12345"]
+ assert v.parse_attribute(" :key: one two") == [":key:", "one two"]
+
+
+def test_needs_visitor_calc_indent():
+ v = NeedsVisitor(lambda x: x)
+ assert v.calc_indent(" test") == 4
+ assert v.calc_indent(" test") == 3
+ assert v.calc_indent("test") == 0
+
+
+def test_needs_visitor_no_change(monkeypatch):
+ v = NeedsVisitor(lambda x: x)
+ lines = """.. req::
+ :id: R-12345
+ :updated: casablanca
+
+ Here's my requirement"""
+ monkeypatch.setattr(v, "read", lambda path: lines.split("\n"))
+ result = []
+ monkeypatch.setattr(v, "write", lambda _, content: result.extend(content))
+
+ v.handle_rst_file("dummy_path")
+ assert len(result) == 5
+ assert "\n".join(result) == lines
+
+
+def test_needs_visitor_with_fix(monkeypatch):
+ fixer = MetadataFixer({"R-12345": (":introduced:", "casablanca")})
+ v = NeedsVisitor(fixer)
+ lines = """.. req::
+ :id: R-12345
+
+ Here's my requirement"""
+ monkeypatch.setattr(v, "read", lambda path: lines.split("\n"))
+ result = []
+ monkeypatch.setattr(v, "write", lambda _, content: result.extend(content))
+
+ v.handle_rst_file("dummy_path")
+ assert len(result) == 5
+ assert ":introduced: casablanca" in "\n".join(result)
+
+
+def test_load_invalid_reqs():
+ input_file = [
+ "r_id,attr,value",
+ "R-12345,:updated:,casablanca"
+ ]
+ result = load_invalid_reqs(input_file)
+ assert "R-12345" in result
+ assert result["R-12345"][0] == ":updated:"
+ assert result["R-12345"][1] == "casablanca"
two version's of requirements by analyzing the needs.json file. The template
can be customized by updating release-requirement-changes.rst.jinja2.
"""
+import csv
from itertools import groupby, chain
import json
import os
print()
print("Requirements Added, but Missing :introduced: Attribute")
print("----------------------------------------------------")
+ errors = [["reqt_id", "attribute", "value"]]
for req in difference_finder.new_requirements.values():
if "introduced" not in req or req["introduced"] != current_version:
+ errors.append([req["id"], ":introduced:", current_version])
print(req["id"])
print()
print("Requirements Changed, but Missing :updated: Attribute")
print("-----------------------------------------------------")
for req in difference_finder.changed_requirements.values():
if "updated" not in req or req["updated"] != current_version:
+ errors.append([req["id"], ":updated:", current_version])
print(req["id"])
+ with open("invalid_metadata.csv", "w", newline="") as error_report:
+ error_report = csv.writer(error_report)
+ error_report.writerows(errors)
if __name__ == "__main__":
num_removed=len(differ.removed_requirements),
num_changed=len(differ.changed_requirements),
)
+
+