diff --git a/.gitignore b/.gitignore index 71d4b9f93cc..3ed0639bbcb 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,9 @@ internal/tools/bin # Node.js files for tools (e.g. markdown-toc) node_modules/ package-lock.json + +# Pytest cache +__pycache__ + +# JSON files generated by the specification parser +*.json diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 61e39a0a22f..5907966c66b 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -12,3 +12,5 @@ ol-prefix: style: ordered no-inline-html: false fenced-code-language: false +header-increment: false +blanks-around-lists: false diff --git a/README.md b/README.md index 2386bf50df4..ad4933d1dec 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Technical committee holds regular meetings, notes are held ## Table of Contents - [Overview](specification/overview.md) +- [Conformance Clause](conformance_clause.md) - [Glossary](specification/glossary.md) - [Versioning and stability for OpenTelemetry clients](specification/versioning-and-stability.md) - [Library Guidelines](specification/library-guidelines.md) @@ -73,6 +74,15 @@ Changes to the [specification](./specification/overview.md) are versioned accord Changes to the change process itself are not currently versioned but may be independently versioned in the future. +## Generating requirements in JSON + +As described in the [conformance clause](./conformance_clause.md) this specification follows the +[W3C Recommendation QA Framework: Specification Guidelines](https://www.w3.org/TR/2005/REC-qaframe-spec-20050817/). The requirements can be automatically extracted from the +specification markdown files into JSON files for easy listing. These JSON files can later be used to check the compliance of a particular implementation. + +To generate JSON files run `python tools/specification_parser/specification_parser.py` from the directory that contains this file. This will generate the corresponding JSON files +in the same directories of the markdown files that contain the requirements. + ## Acronym The official acronym used by the OpenTelemetry project is "OTel". diff --git a/conformance_clause.md b/conformance_clause.md new file mode 100644 index 00000000000..0b1b77ecf8f --- /dev/null +++ b/conformance_clause.md @@ -0,0 +1,196 @@ +# Conformance Clause + +Conformance to OpenTelemetry is defined in this conformance clause. This +conformance clause follows the +[W3C Recommendation QA Framework: Specification Guidelines](https://www.w3.org/TR/2005/REC-qaframe-spec-20050817/). + +## Parts of the specification + +### Normative Parts + +_Normative Parts_ of this specification define its strict requirements. They +are identified by a level 5 header followed by the word *Requirement* or the +words *Conditional Requirement* and an identifying name. + +#### Requirement Normative Parts + +A _Requirement Normative Part_ defines a certain requirement that the +implementations of OpenTelemetry are to implement. The mandatoriness of the +requirement is defined by the RFC 2119 word that appears in the Requirement +Normative Part. + +Here is an example of a Requirement Normative Part: + +##### Requirement Some name + +> The trace **MUST** have an identifier. + +This is the markdown code for the previous example: + +``` +##### Requirement Some name: + +> The trace **MUST** have an identifier. +``` + +The header for a requirement must be of level 5 (a header that begins with 5 `#` +characters). The requirement name must be unique per file. + +A blank line must follow the header. + +The `>` character serves as a delimiter for the Normative Part, it must be the +first character of any line that makes the content of the Normative Part. +Multiple lines can start with this character: + +``` +##### Requirement Another name: + +> The span **MAY** include +> additional metadata. +``` + +#### Conditional Requirement Normative Parts + +A _Conditional Requirement Normative Part_ has the same characteristics of a +Requirement Normative Part but it is preceded by a _condition_. Some +requirements of this specification are conditional, they are valid only for +certain implementations if a certain condition is fulfilled by such +implementation. For example, a requirement that refers to certain software +pattern would be valid for some implementations that can follow that pattern +and not be valid for other implementations that can not. The implementations +that can follow that pattern can be subjected to additional requirements, +defined in Conditional Requirement Normative parts. + +Here is an example of a Conditional Requirement Normative Part: + +##### Condition 1 + +> The API does not operate directly on the `Context`. +> +> ##### Conditional Requirement A name here +> +> > The API **MUST** provide an `extract` function to extract the `Baggage` +> > from a `Context` instance. + +The header for a condition must be of level 5. The condition number must be +unique per file and continuously increasing from 1. As more condition or requirement +levels are nested more numbers separated by periods are correspondingly to be +added to the condition number. + +A blank line must follow the header. + +The `>` character indicates the scope of the condition, it must be the first +character of any line that makes the content of the condition. Any requirement +inside the scope of the condition is valid only if the condition is true for +the particular implementation. Other conditions may also be inside the scope of +another condition. A contained condition inside the scope of a containing +condition is to be evaluated only if the containing condition is true for the +particular implementation. + +This is the markdown code for the previous example: + +``` +##### Condition 1 + +> The API does not operate directly on the `Context`. +> +> ##### Conditional Requirement Some other name +> +> > The API **MUST** provide an `extract` function to extract the `Baggage` +> > from a `Context` instance. +``` + +### Informative Parts + +Any other part of a file that includes at least one Requirement Normative Part +is considered an _Informative Part_. An Informative Part does not specify any +requirement, it is only intended to help the reader of the specification +understand it better. There is no requirement for the writing of an Informative +Part, any text, table, image, diagram, etc. can be included. + +Regardless of the language used in an informative part, it is never to be +considered normative in any way. Only requirements defined in a Requirement +Normative Part have any mandatoriness for the implementations of OpenTelemetry. + +### Normative Language + +Every one of the Requirement Normative Parts must include one and only one of +the following words: + +- MUST +- REQUIRED +- SHALL +- SHALL NOT +- MUST NOT +- SHOULD +- RECOMMENDED +- SHOULD NOT +- NOT RECOMMENDED +- MAY +- OPTIONAL + +These words are to be interpreted as they are defined in +[RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119). These two kinds of +Normative Parts (Requirement Normative Parts and Conditional Requirement +Normative Parts) are explained with more detail next. + +## OpenTelemetry Conformance Model + +### Products + +The OpenTelemetry Conformance Model defines conformance for the following +_products_: + +- API: the interfaces that the application and library developers use directly +- SDK: the implementation of the interfaces in the API +- Semantic Conventions: FIXME: add a brief description of Semantic Conventions +- OTLP: FIXME: Add a brief description of OTLP + +### Conformance Designations + +There is only one conformance designation: _Full_. This conformance designation +applies to all the products listed [here](#products). This conformance +designation requires that all requirements are implemented accordingly to their +mandatoriness. Some requirements are conditional, these requirements are to be +implemented accordingly to their mandatoriness if the corresponding +condition applies to the corresponding implementation. + +#### Profiles + +OpenTelemetry does not address nor define profiles to subdivide its technology. + +#### Modules + +OpenTelemetry does not address nor define modules to subdivide its technology. + +#### Levels + +OpenTelemetry does not address nor define levels to subdivide its technology. + +#### Deprecated Features + +OpenTelemetry does not have any deprecated features. OpenTelemetry may have +deprecated features in the future. + +#### Obsolete Features + +OpenTelemetry does not have any obsolete features. OpenTelemetry may have +obsolete features in the future. + +#### Optional Features + +OpenTelemetry does not have any optional features. OpenTelemetry may have +optional features in the future. + +#### Extensibility + +OpenTelemetry is not extensible. + +#### Conformance Claims + +OpenTelemetry conformance claims must include the following: + +- Specification Name: OpenTelemetry +- Specification Version: OpenTelemetry Version Number +- Degree of Conformance: Full +- Date: Date when the claim is made diff --git a/specification/baggage/api.md b/specification/baggage/api.md index fe203c3117d..364c3b1127d 100644 --- a/specification/baggage/api.md +++ b/specification/baggage/api.md @@ -1,4 +1,4 @@ -# Baggage API +# API **Status**: [Stable, Feature-freeze](../document-status.md) @@ -8,7 +8,7 @@ Table of Contents - [Overview](#overview) -- [Operations](#operations) +- [Functions](#functions) - [Get Value](#get-value) - [Get All Values](#get-all-values) - [Set Value](#set-value) @@ -22,115 +22,187 @@ Table of Contents ## Overview -`Baggage` is used to annotate telemetry, adding context and information to -metrics, traces, and logs. It is a set of name/value pairs describing -user-defined properties. Each name in `Baggage` MUST be associated with -exactly one value. +`Baggage` is used to annotate telemetry, adding context and information to metrics, +traces and logs. It is a set of name/value pairs that describe user-defined properties. -The Baggage API consists of: +The API consists of -- the `Baggage` -- functions to interact with the `Baggage` in a `Context` +- The `Baggage` +- Functions to interact with the `Baggage` in a `Context` + - `setValue` + - `getValue` + - `getAllValues` + - `removeValue` -The functions described here are one way to approach interacting with the -`Baggage` via having struct/object that represents the entire Baggage content. -Depending on language idioms, a language API MAY implement these functions by -interacting with the baggage via the `Context` directly. +The functions described here are one way to approach interacting with the `Baggage` via +having a struct/object that represents the entire Baggage content. -The Baggage API MUST be fully functional in the absence of an installed SDK. -This is required in order to enable transparent cross-process Baggage -propagation. If a Baggage propagator is installed into the API, it will work -with or without an installed SDK. +##### Requirement 1 -The `Baggage` container MUST be immutable, so that the containing `Context` -also remains immutable. +> Each name in `Baggage` **MUST** be associated with exactly one value. -## Operations +##### Requirement 2 + +> The order of name/value pairs in the `Baggage` **MUST NOT** be significant. + +##### Requirement 3 + +> The API **MAY** implement these functions by interacting with the baggage via the +> `Context` directly. + +##### Requirement 4 + +> The API **MUST** be fully functional in the absence of an installed SDK. + +[This](#requirement-2) is required to enable transparent cross-process Baggage +propagation. If a Baggage propagator is installed into the API, it will work with or +without an installed SDK. + +##### Requirement 5 + +> The `Baggage` container **MUST** be immutable. + +[This](#requirement-3) is required so that the containing `Context` also remains +immutable. + +## Functions ### Get Value -To access the value for a name/value pair set by a prior event, the Baggage API -MUST provide a function that takes the name as input, and returns a value -associated with the given name, or null if the given name is not present. +This function gives access to the value of a name/value pair set by a prior event. -REQUIRED parameters: +##### Requirement 6 -`Name` the name to return the value for. +> The API **MUST** provide a `getValue` function that takes a required name parameter +> and returns a value associated with the given name, or null if the given name is not +> present. ### Get All Values -Returns the name/value pairs in the `Baggage`. The order of name/value pairs -MUST NOT be significant. Based on the language specifics, the returned -value can be either an immutable collection or an iterator on the immutable -collection of name/value pairs in the `Baggage`. +This function gives access to all the name/value pairs in the `Baggage`. -### Set Value +##### Requirement 7 -To record the value for a name/value pair, the Baggage API MUST provide a -function which takes a name, and a value as input. Returns a new `Baggage` -that contains the new value. Depending on language idioms, a language API MAY -implement these functions by using a `Builder` pattern and exposing a way to -construct a `Builder` from a `Baggage`. +> The API **MUST** provide a `getAllValues` function that returns either an immutable +> collection of name/value pairs on the `Baggage` or an iterator on such collection. -REQUIRED parameters: +### Set Value -`Name` The name for which to set the value, of type string. +This operation makes it possible to record the value for a name/value pair. -`Value` The value to set, of type string. +##### Requirement 8 -OPTIONAL parameters: +> The API **MUST** provide a `setValue` function which takes a required name, a required +> value and optional metadata and returns a new `Baggage` that contains the new value. -`Metadata` Optional metadata associated with the name-value pair. This should be -an opaque wrapper for a string with no semantic meaning. Left opaque to allow -for future functionality. +##### Requirement 9 -### Remove Value +> The API **MAY** implement the `setValue` function by using a `Builder` pattern and +> exposing a way to construct a `Builder` from a `Baggage`. -To delete a name/value pair, the Baggage API MUST provide a function which -takes a name as input. Returns a new `Baggage` which no longer contains the -selected name. Depending on language idioms, a language API MAY -implement these functions by using a `Builder` pattern and exposing a way to -construct a `Builder` from a `Baggage`. +The optional `setValue` metadata is associated with the name-value pair that is being +set. -REQUIRED parameters: +##### Requirement 10 -`Name` the name to remove. +> The optional `setValue` metadata parameter **SHOULD** be an opaque wrapper for a +> string with no semantic meaning. -## Context Interaction +This metadata is left opaque to allow for future functionality. -This section defines all operations within the Baggage API that interact with -the [`Context`](../context/context.md). +### Remove Value -If an implementation of this API does not operate directly on the `Context`, it -MUST provide the following functionality to interact with a `Context` instance: +##### Requirement 11 -- Extract the `Baggage` from a `Context` instance -- Insert the `Baggage` to a `Context` instance +> To delete a name/value pair, the API **MUST** provide a `removeValue` function which +> takes a required name and returns a new `Baggage` which no longer contains the +> selected name. -The functionality listed above is necessary because API users SHOULD NOT have -access to the [Context Key](../context/context.md#create-a-key) used by the -Baggage API implementation. +##### Requirement 12 -If the language has support for implicitly propagated `Context` (see -[here](../context/context.md#optional-global-operations)), the API SHOULD also -provide the following functionality: +> The API **MAY** implement the `removeValue` function by using a `Builder` pattern and +> exposing a way to construct a `Builder` from a `Baggage`. -- Get the currently active `Baggage` from the implicit context. This is -equivalent to getting the implicit context, then extracting the `Baggage` from -the context. -- Set the currently active `Baggage` to the implicit context. This is equivalent -to getting the implicit context, then inserting the `Baggage` to the context. +## Context Interaction -All the above functionalities operate solely on the context API, and they MAY be -exposed as static methods on the baggage module, as static methods on a class -inside the baggage module (it MAY be named `BaggageUtilities`), or on the -`Baggage` class. This functionality SHOULD be fully implemented in the API when -possible. +This section defines all functions within the API that interact with the +[`Context`](../context/context.md). + +##### Condition 1 + +> The API does not operate directly on the `Context`. +> +> ##### Conditional Requirement 1.1 +> +> > The API **MUST** provide an `extract` function to extract the `Baggage` from a +> > `Context` instance. +> +> ##### Conditional Requirement 1.2 +> +> > The API **MUST** provide an `inject` function to inject the `Baggage` in a +> > `Context` instance. + +The functionality listed above is necessary because if the API does not operate directly +on the `Context`, then users should not have access to the +[Context Key](../context/context.md#create-a-key) used by the API. + +##### Requirement 13 + +> The API SHOULD NOT provide access to the its +[Context Key](../context/context.md#create-a-key). + +##### Condition 2 + +> The language has support for implicitly propagated `Context` +> (see [here](../context/context.md#optional-global-operations)). +> +> ##### Conditional Requirement 2.1 +> +> > The API **SHOULD** provide a get `Baggage` functionality to get the currently active +> > `Baggage` from the implicit context and a set `Baggage` functionality to set the +> > currently active `Baggage` into the implicit context. +> +> ##### Condition 2.1 +> +> > The API provides a functionality to get the currently active `Baggage` from the +> > implicit context and a functionality to set the currently active `Baggage` into +> > the implicit context. +> > +> > ##### Conditional Requirement 2.1.1 +> > +> > > The get `Baggage` functionality behavior **MUST** be equivalent to getting the +> > > implicit context, then extracting the `Baggage` from the context. +> > +> > ##### Conditional Requirement 2.1.2 +> > +> > > The set `Baggage` functionality behavior **MUST** be equivalent to getting the +> > > implicit context, then inserting the `Baggage` into the context. +> > +> > ##### Conditional Requirement 2.1.3 +> > +> > > The get and set `Baggage` functionalities **MUST** operate solely on the context +> > > API. +> > +> > ##### Conditional Requirement 2.1.4 +> > +> > > The get and set `Baggage` functionalities **MAY** be exposed +> > > - as static methods on the baggage module or +> > > - as static methods on a class inside the baggage module or +> > > - on the `Baggage` class +> > +> > ##### Conditional Requirement 2.1.5 +> > +> > > The get and set `Baggage` functionalities **SHOULD** be fully implemented in +> > > the API. ### Clear Baggage in the Context -To avoid sending any name/value pairs to an untrusted process, the Baggage API -MUST provide a way to remove all baggage entries from a context. +To avoid sending any name/value pairs to an untrusted process, the API must provide a +way to remove all baggage entries from a context. + +##### Requirement 14 + +> The API **MUST** provide a way to remove all baggage entries from a context. This functionality can be implemented by having the user set an empty `Baggage` object/struct into the context, or by providing an API that takes a `Context` as @@ -138,28 +210,37 @@ input, and returns a new `Context` with no `Baggage` associated. ## Propagation -`Baggage` MAY be propagated across process boundaries or across any arbitrary -boundaries (process, $OTHER_BOUNDARY1, $OTHER_BOUNDARY2, etc) for various -reasons. +`Baggage` may be propagated across process boundaries or across any arbitrary boundaries +(process, $OTHER_BOUNDARY1, $OTHER_BOUNDARY2, etc) for various reasons. -The API layer or an extension package MUST include the following `Propagator`s: +##### Requirement 15 -* A `TextMapPropagator` implementing the [W3C Baggage Specification](https://w3c.github.io/baggage). +> The API or an extension package **MUST** include a `TextMapPropagator` which +> implements the [W3C Baggage Specification](https://w3c.github.io/baggage)[^1]. + +[^1]: The W3C baggage specification does not currently assign semantic meaning to the +optional metadata. See [Propagators Distribution](../context/api-propagators.md#propagators-distribution) for how propagators are to be distributed. -Note: The W3C baggage specification does not currently assign semantic meaning -to the optional metadata. +##### Requirement 16 + +> On `extract`, a propagator **SHOULD** store all metadata as a single metadata instance +> per entry. + +##### Requirement 17 -On `extract`, the propagator should store all metadata as a single metadata instance per entry. -On `inject`, the propagator should append the metadata per the W3C specification format. -Refer to the API Propagators -[Operation](../context/api-propagators.md#operations) section for the -additional requirements these operations need to follow. +> On `inject`, a propagator **SHOULD** append the metadata per the W3C specification +> format. + +Refer to the API Propagators [Operation](../context/api-propagators.md#operations) +section for the additional requirements these operations need to follow. ## Conflict Resolution -If a new name/value pair is added and its name is the same as an existing name, -than the new pair MUST take precedence. The value is replaced with the added -value (regardless if it is locally generated or received from a remote peer). +##### Requirement 18 + +> If a new name/value pair is added and its name is the same as an existing name, then +> the new pair **MUST** take precedence, the existing value being replaced with the +> added value (regardless if it is locally generated or received from a remote peer). diff --git a/tools/specification_parser/specification_parser.py b/tools/specification_parser/specification_parser.py new file mode 100644 index 00000000000..fe065d929c6 --- /dev/null +++ b/tools/specification_parser/specification_parser.py @@ -0,0 +1,204 @@ +from re import ( + finditer, findall, compile as compile_, DOTALL, sub, match, search +) +from json import dumps +from os.path import curdir, abspath, join, splitext +from os import walk + +rfc_2119_keywords_regexes = [ + r"MUST", + r"REQUIRED", + r"SHALL", + r"MUST NOT", + r"SHALL NOT", + r"SHOULD", + r"RECOMMENDED", + r"SHOULD NOT", + r"NOT RECOMMENDED", + r"MAY", + r"OPTIONAL", +] + + +def find_markdown_file_paths(root): + markdown_file_paths = [] + + for root_path, _, file_paths, in walk(root): + for file_path in file_paths: + absolute_file_path = join(root_path, file_path) + + _, file_extension = splitext(absolute_file_path) + + if file_extension == ".md": + markdown_file_paths.append(absolute_file_path) + + return markdown_file_paths + + +def clean_content(content): + + for rfc_2119_keyword_regex in rfc_2119_keywords_regexes: + + content = sub( + f"\\*\\*{rfc_2119_keyword_regex}\\*\\*", + rfc_2119_keyword_regex, + content + ) + + return sub(r"\n>", "", content) + + +def find_rfc_2119_keyword(content): + + for rfc_2119_keyword_regex in rfc_2119_keywords_regexes: + + if search( + f"\\*\\*{rfc_2119_keyword_regex}\\*\\*", content + ) is not None: + return rfc_2119_keyword_regex + + +def parse_requirements(markdown_file_path): + + requirements = [] + + with open(markdown_file_path, "r") as markdown_file: + + for requirement in [ + requirement_match.groupdict() for requirement_match in ( + finditer( + r"##### Requirement (?P(.*?)+)\n\n" + r"> (?P(.*?))\n\n", + markdown_file.read(), + DOTALL + ) + ) + ]: + + content = requirement["content"] + + requirements.append( + { + "id": requirement["id"], + "clean id": sub(r"[^\w]", "_", requirement["id"].lower()), + "content": clean_content(content), + "RFC 2119 keyword": find_rfc_2119_keyword(content) + } + ) + + return requirements + + +regex = compile_( + r"(?P(> ?)*)(?P##### )?(?P.*)" +) + + +def parse_conditions(markdown_file_path): + + conditions = [] + + with open(markdown_file_path, "r") as markdown_file: + + for condition in findall( + r"##### Condition [0-9]+\n\n.*?\n\n", + markdown_file.read(), + DOTALL + ): + + stack = [] + + text = "" + + for line in condition.split("\n"): + regex_dict = regex.match(line).groupdict() + + level = len(regex_dict["level"].split()) + pounds = regex_dict["pounds"] + content = regex_dict["content"] + + if not level and not content: + continue + + if not pounds: + text = "".join([text, content]) + continue + + if match( + r"(> ?)*##### Condition [\.0-9]+", line + ) is not None: + + node = { + "id": content, + "content": "", + "children": [] + } + else: + + content = sub(r"Conditional Requirement ", "", content) + + node = { + "id": content, + "clean id": sub(r"[^\w]", "_", content.lower()), + "content": "", + "RFC 2119 keyword": None + } + + if not stack: + stack.append(node) + continue + + stack[-1]["content"] = clean_content(text) + + if level == len(stack) - 1: + + stack[-1]["RFC 2119 keyword"] = find_rfc_2119_keyword( + text + ) + stack.pop() + + elif level < len(stack) - 1: + stack[-1]["RFC 2119 keyword"] = find_rfc_2119_keyword( + text + ) + for _ in range(len(stack) - level): + stack.pop() + + text = "" + stack[-1]["children"].append(node) + stack.append(node) + + stack[-1]["content"] = clean_content(text) + stack[-1]["RFC 2119 keyword"] = find_rfc_2119_keyword( + text + ) + + conditions.append(stack[0]) + + return conditions + + +def write_json_specifications(requirements, conditions): + for md_absolute_file_path, requirement_sections in requirements.items(): + + with open( + "".join([splitext(md_absolute_file_path)[0], ".json"]), "w" + ) as json_file: + json_file.write(dumps(requirement_sections, indent=4)) + + +if __name__ == "__main__": + + for markdown_file_path in find_markdown_file_paths( + join(abspath(curdir), "specification") + ): + + result = [] + result.extend(parse_requirements(markdown_file_path)) + result.extend(parse_conditions(markdown_file_path)) + + if result: + with open( + "".join([splitext(markdown_file_path)[0], ".json"]), "w" + ) as json_file: + json_file.write(dumps(result, indent=4)) diff --git a/tools/specification_parser/specification_parser_test.py b/tools/specification_parser/specification_parser_test.py new file mode 100644 index 00000000000..26a4fb2dc36 --- /dev/null +++ b/tools/specification_parser/specification_parser_test.py @@ -0,0 +1,139 @@ +from unittest import TestCase +from os.path import abspath, curdir, join + +from specification_parser import ( + find_markdown_file_paths, + parse_requirements, + parse_conditions +) + + +class TestSpecificationParser(TestCase): + """ + Tests for the specification parser + """ + + @classmethod + def setUpClass(cls): + cls.current_directory_path = abspath(curdir) + cls.test_specification_md_path = join( + cls.current_directory_path, "test_specification.md" + ) + + def test_find_markdown_file_paths(self): + self.assertIn( + self.test_specification_md_path, + find_markdown_file_paths(self.current_directory_path) + ) + + def test_parse_requirements(self): + self.maxDiff = None + self.assertEqual( + [ + { + "id": "Some requirement", + "clean id": "some_requirement", + "content": "This MUST be done.", + "RFC 2119 keyword": "MUST" + }, + { + "id": "Some other requirement", + "clean id": "some_other_requirement", + "content": "This MUST NOT be done.", + "RFC 2119 keyword": "MUST NOT" + }, + { + "id": "Another requirement-name", + "clean id": "another_requirement_name", + "content": "This SHOULD be done in a certain way.", + "RFC 2119 keyword": "SHOULD" + }, + { + "id": "This is the name of-the-requirement", + "clean id": "this_is_the_name_of_the_requirement", + "content": "This MUST NOT be done.", + "RFC 2119 keyword": "MUST NOT" + }, + ], + parse_requirements(self.test_specification_md_path) + ) + + def test_parse_conditions(self): + self.maxDiff = None + self.assertEqual( + [ + { + "id": "Condition 1", + "content": "This is a condition.", + "children": [ + { + "id": "Condition 1.1", + "content": "This is a condition.", + "children": [ + { + "id": "Condition 1.1.1", + "content": "This is a condition.", + "children": [ + { + "id": "Conditional requirement name", + "clean id": "conditional_requirement_name", + "content": "This MAY be done.", + "RFC 2119 keyword": "MAY" + }, + { + "id": "Other name", + "clean id": "other_name", + "content": "This SHOULD NOT be done.", + "RFC 2119 keyword": "SHOULD NOT" + }, + { + "id": "Another name here", + "clean id": "another_name_here", + "content": "This MAY be done.", + "RFC 2119 keyword": "MAY" + } + ] + } + ] + }, + { + "id": "Condition 1.2", + "content": "This is a condition.", + "children": [ + { + "id": "And another-name", + "clean id": "and_another_name", + "content": "This MUST be done.", + "RFC 2119 keyword": "MUST" + } + ] + } + ] + }, + { + "id": "Condition 2", + "content": "This is a condition.", + "children": [ + { + "id": "Condition 2.1", + "content": "This is a condition.", + "children": [ + { + "id": "Condition 2.2", + "content": "This is a condition.", + "children": [ + { + "id": "The name here", + "clean id": "the_name_here", + "content": "This MAY be done.", + "RFC 2119 keyword": "MAY" + } + ] + } + ] + } + ] + } + ], + parse_conditions(self.test_specification_md_path) + ) diff --git a/tools/specification_parser/test_specification.md b/tools/specification_parser/test_specification.md new file mode 100644 index 00000000000..9678cc7245a --- /dev/null +++ b/tools/specification_parser/test_specification.md @@ -0,0 +1,74 @@ +# Test Specification + +## Title + +Some content. + +##### Requirement Some requirement + +> This **MUST** be done. + +##### Requirement Some other requirement + +> This **MUST NOT** be done. + +##### Requirement Another requirement-name + +> This **SHOULD** be done +> in a certain way. + +##### Condition 1 + +> This is a condition. +> +> ##### Condition 1.1 +> +> > This is a condition. +> > +> > ##### Condition 1.1.1 +> > +> > > This is a condition. +> > > +> > > ##### Conditional Requirement Conditional requirement name +> > > +> > > > This **MAY** be done. +> > > +> > > ##### Conditional Requirement Other name +> > > +> > > > This **SHOULD NOT** be done. +> > > +> > > ##### Conditional Requirement Another name here +> > > +> > > > This **MAY** be done. +> +> ##### Condition 1.2 +> +> > This is a condition. +> > +> > ##### Conditional Requirement And another-name +> > +> > > This **MUST** be done. + +Some content. + +##### Requirement This is the name of-the-requirement + +> This **MUST NOT** be done. + +##### Condition 2 + +> This is a condition. +> +> ##### Condition 2.1 +> +> > This is a condition. +> > +> > ##### Condition 2.2 +> > +> > > This is a condition. +> > > +> > > ##### Conditional Requirement The name here +> > > +> > > > This **MAY** be done. + +Some content.