From 23e314d1daf6e4c664907decef215a67ab7c058f Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Thu, 29 Oct 2020 22:45:34 -0600 Subject: [PATCH 1/3] Add compliance with W3C QA Framework: Specification Guidelines --- .gitignore | 6 + .markdownlint.yaml | 2 + .vscode/settings.json | 1 + README.md | 1 + conformance_clause.md | 199 +++++++++++++ specification/baggage/api.md | 261 ++++++++++++------ .../specification_parser.py | 135 +++++++++ .../specification_parser_test.py | 47 ++++ .../test_specification.md | 18 ++ 9 files changed, 580 insertions(+), 90 deletions(-) create mode 100644 conformance_clause.md create mode 100644 tools/specification_parser/specification_parser.py create mode 100644 tools/specification_parser/specification_parser_test.py create mode 100644 tools/specification_parser/test_specification.md 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/.vscode/settings.json b/.vscode/settings.json index 204dc438d8d..29c9a5c7b05 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "MD029": {"style": "ordered"}, "MD033": false, "MD040": false, + "MD001": false, }, "yaml.schemas": { "https://raw.githubusercontent.com/open-telemetry/build-tools/v0.7.0/semantic-conventions/semconv.schema.json": [ diff --git a/README.md b/README.md index 2386bf50df4..418bc2cc0b5 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) diff --git a/conformance_clause.md b/conformance_clause.md new file mode 100644 index 00000000000..586783a7ff5 --- /dev/null +++ b/conformance_clause.md @@ -0,0 +1,199 @@ +# 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 number. + +#### 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 4 + +> The trace **MUST** have an identifier. + +This is the markdown code for the previous example: + +``` +##### Requirement 4: + +> 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 number must be unique per file and continuously increasing +from 1. + +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 6: + +> 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 1.1 +> +> > 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. The conditional requirement +number must be composed of the containing condition number a period and an +unique number 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 or conditional requirement numbers. + +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 1.1: +> +> > 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:w +- 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..cad7f853764 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..efdaeb3706b --- /dev/null +++ b/tools/specification_parser/specification_parser.py @@ -0,0 +1,135 @@ +from re import finditer, findall +from json import dumps +from os.path import curdir, abspath, join, splitext +from os import walk + + +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 parse_requirements(markdown_file_paths): + requirements = {} + + for markdown_file_path in markdown_file_paths: + + with open(markdown_file_path, "r") as markdown_file: + + requirement_matches = [ + requirement_match.groupdict() for requirement_match in ( + finditer( + r"##### (?PRequirement [0-9]+)\n\n" + r"(?P(>.*\n)+)", + markdown_file.read(), + ) + ) + ] + + if not requirement_matches: + continue + + md_file_path = "".join([splitext(markdown_file_path)[0], ".md"]) + + requirements[md_file_path] = {} + + for requirement in requirement_matches: + + requirement_key = requirement["key"] + + assert ( + requirement_key not in + requirements[md_file_path].keys() + ), "Repeated requirement key {} found in {}".format( + requirement_key, markdown_file_path + ) + + requirement_description = requirement["description"].strip() + + rfc_2119_keyword_matches = [] + + for rfc_2119_keyword_regex in [ + # 2. MUST NOT + r"MUST NOT", + r"SHALL NOT", + # 1. MUST + r"MUST(?! NOT)", + r"REQUIRED", + r"SHALL(?! NOT)", + # 4. SHOULD NOT + r"SHOULD NOT", + r"NOT RECOMMENDED", + # 3. SHOULD + r"SHOULD(?! NOT)", + r"(? 0 + ), "No RFC 2119 keywords were found in {}".format( + requirement_key_path + ) + + assert ( + len(rfc_2119_keyword_matches) == 1 + ), "More than one RFC 2119 keyword was found in {}".format( + requirement_key_path + ) + + requirements[md_file_path][requirement_key] = {} + + requirements[md_file_path][requirement_key]["description"] = ( + requirement_description + ) + + rfc_2119_keyword_matches.reverse() + + requirements[md_file_path][requirement_key][ + "RFC 2119 Keywords" + ] = rfc_2119_keyword_matches + + return requirements + + +def write_json_specifications(requirements): + 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__": + + write_json_specifications( + parse_requirements( + find_markdown_file_paths( + join(abspath(curdir), "..", "..", "specification") + ) + ) + ) diff --git a/tools/specification_parser/specification_parser_test.py b/tools/specification_parser/specification_parser_test.py new file mode 100644 index 00000000000..08311da634d --- /dev/null +++ b/tools/specification_parser/specification_parser_test.py @@ -0,0 +1,47 @@ +from unittest import TestCase +from os.path import abspath, curdir, join + +from specification_parser import ( + find_markdown_file_paths, + parse_requirements, +) + + +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" + ) + cls.parsed_requirements = parse_requirements( + [cls.test_specification_md_path] + )[cls.test_specification_md_path] + + def test_find_markdown_file_paths(self): + self.assertIn( + self.test_specification_md_path, + find_markdown_file_paths(self.current_directory_path) + ) + + def test_requirement_1(self): + assert self.parsed_requirements["Requirement 1"] == { + "description": "> This **MUST** be done.", + "RFC 2119 Keywords": ["MUST"] + } + + def test_requirement_2(self): + assert self.parsed_requirements["Requirement 2"] == { + "description": "> This **MUST NOT** be done.", + "RFC 2119 Keywords": ["MUST NOT"] + } + + def test_requirement_3(self): + assert self.parsed_requirements["Requirement 3"] == { + "description": "> This **SHOULD** be done\n> in a certain way.", + "RFC 2119 Keywords": ["SHOULD"] + } diff --git a/tools/specification_parser/test_specification.md b/tools/specification_parser/test_specification.md new file mode 100644 index 00000000000..d3c74adc0f5 --- /dev/null +++ b/tools/specification_parser/test_specification.md @@ -0,0 +1,18 @@ +# Test Specification + +## Title + +Some content. + +##### Requirement 1 + +> This **MUST** be done. + +##### Requirement 2 + +> This **MUST NOT** be done. + +##### Requirement 3 + +> This **SHOULD** be done +> in a certain way. From b503896398f8434f4cf623e1ebdf3a7a9c31913c Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 12 Oct 2021 11:09:25 +0200 Subject: [PATCH 2/3] Add condition parsing to the script --- .vscode/settings.json | 1 - README.md | 9 + conformance_clause.md | 6 +- specification/baggage/api.md | 62 ++--- .../specification_parser.py | 231 +++++++++++------- .../specification_parser_test.py | 119 +++++++-- .../test_specification.md | 56 +++++ 7 files changed, 348 insertions(+), 136 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 29c9a5c7b05..204dc438d8d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,6 @@ "MD029": {"style": "ordered"}, "MD033": false, "MD040": false, - "MD001": false, }, "yaml.schemas": { "https://raw.githubusercontent.com/open-telemetry/build-tools/v0.7.0/semantic-conventions/semconv.schema.json": [ diff --git a/README.md b/README.md index 418bc2cc0b5..ad4933d1dec 100644 --- a/README.md +++ b/README.md @@ -74,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 index 586783a7ff5..7df61c7fa08 100644 --- a/conformance_clause.md +++ b/conformance_clause.md @@ -93,11 +93,11 @@ particular implementation. This is the markdown code for the previous example: ``` -##### Condition 1: +##### Condition 1 > The API does not operate directly on the `Context`. > -> ##### Conditional Requirement 1.1: +> ##### Conditional Requirement 1.1 > > > The API **MUST** provide an `extract` function to extract the `Baggage` > > from a `Context` instance. @@ -124,7 +124,7 @@ the following words: - REQUIRED - SHALL - SHALL NOT -- MUST NOT:w +- MUST NOT - SHOULD - RECOMMENDED - SHOULD NOT diff --git a/specification/baggage/api.md b/specification/baggage/api.md index cad7f853764..364c3b1127d 100644 --- a/specification/baggage/api.md +++ b/specification/baggage/api.md @@ -161,39 +161,39 @@ on the `Context`, then users should not have access to the > > 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 > > -> > ##### Condition 2.1 +> > ##### Conditional Requirement 2.1.5 > > -> > > 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. +> > > The get and set `Baggage` functionalities **SHOULD** be fully implemented in +> > > the API. ### Clear Baggage in the Context diff --git a/tools/specification_parser/specification_parser.py b/tools/specification_parser/specification_parser.py index efdaeb3706b..402e5c4826a 100644 --- a/tools/specification_parser/specification_parser.py +++ b/tools/specification_parser/specification_parser.py @@ -1,8 +1,24 @@ -from re import finditer, findall +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 = [] @@ -19,103 +35,148 @@ def find_markdown_file_paths(root): return markdown_file_paths -def parse_requirements(markdown_file_paths): - requirements = {} +def clean_content(content): - for markdown_file_path in markdown_file_paths: + for rfc_2119_keyword_regex in rfc_2119_keywords_regexes: - with open(markdown_file_path, "r") as markdown_file: + content = sub( + f"\\*\\*{rfc_2119_keyword_regex}\\*\\*", + rfc_2119_keyword_regex, + content + ) - requirement_matches = [ - requirement_match.groupdict() for requirement_match in ( - finditer( - r"##### (?PRequirement [0-9]+)\n\n" - r"(?P(>.*\n)+)", - markdown_file.read(), - ) - ) - ] + return sub(r"\n>", "", content) - if not requirement_matches: - continue - md_file_path = "".join([splitext(markdown_file_path)[0], ".md"]) +def find_rfc_2119_keyword(content): - requirements[md_file_path] = {} + for rfc_2119_keyword_regex in rfc_2119_keywords_regexes: - for requirement in requirement_matches: + if search( + f"\\*\\*{rfc_2119_keyword_regex}\\*\\*", content + ) is not None: + return rfc_2119_keyword_regex - requirement_key = requirement["key"] - assert ( - requirement_key not in - requirements[md_file_path].keys() - ), "Repeated requirement key {} found in {}".format( - requirement_key, markdown_file_path - ) +def parse_requirements(markdown_file_path): - requirement_description = requirement["description"].strip() - - rfc_2119_keyword_matches = [] - - for rfc_2119_keyword_regex in [ - # 2. MUST NOT - r"MUST NOT", - r"SHALL NOT", - # 1. MUST - r"MUST(?! NOT)", - r"REQUIRED", - r"SHALL(?! NOT)", - # 4. SHOULD NOT - r"SHOULD NOT", - r"NOT RECOMMENDED", - # 3. SHOULD - r"SHOULD(?! NOT)", - r"(? 0 - ), "No RFC 2119 keywords were found in {}".format( - requirement_key_path + for requirement in [ + requirement_match.groupdict() for requirement_match in ( + finditer( + r"##### (?PRequirement [0-9]+)\n\n" + r"> (?P(.*?))\n\n", + markdown_file.read(), + DOTALL + ) ) + ]: - assert ( - len(rfc_2119_keyword_matches) == 1 - ), "More than one RFC 2119 keyword was found in {}".format( - requirement_key_path + content = requirement["content"] + + requirements.append( + { + "id": requirement["id"], + "content": clean_content(content), + "RFC 2119 keyword": find_rfc_2119_keyword(content) + } ) - requirements[md_file_path][requirement_key] = {} + return requirements + + +def parse_conditions(markdown_file_path): + + conditions = [] - requirements[md_file_path][requirement_key]["description"] = ( - requirement_description + 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 = [] + + regex = compile_( + r"(?P(> ?)*)(?P##### )?(?P.*)" ) - rfc_2119_keyword_matches.reverse() + text = "" - requirements[md_file_path][requirement_key][ - "RFC 2119 Keywords" - ] = rfc_2119_keyword_matches + for line in condition.split("\n"): + regex_dict = regex.match(line).groupdict() - return requirements + level = len(regex_dict["level"].split()) + pounds = regex_dict["pounds"] + content = regex_dict["content"] + if not level and not content: + continue -def write_json_specifications(requirements): + if not pounds: + text = "".join([text, content]) + continue + + if match( + r"(> ?)*##### Condition [\.0-9]+", line + ) is not None: + + node = { + "id": content, + "content": "", + "children": [] + } + else: + node = { + "id": content, + "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 = "" + from ipdb import set_trace + try: + stack[-1]["children"].append(node) + except: + set_trace() + 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( @@ -126,10 +187,16 @@ def write_json_specifications(requirements): if __name__ == "__main__": - write_json_specifications( - parse_requirements( - find_markdown_file_paths( - join(abspath(curdir), "..", "..", "specification") - ) - ) - ) + 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 index 08311da634d..a84a5c83f49 100644 --- a/tools/specification_parser/specification_parser_test.py +++ b/tools/specification_parser/specification_parser_test.py @@ -4,6 +4,7 @@ from specification_parser import ( find_markdown_file_paths, parse_requirements, + parse_conditions ) @@ -18,9 +19,6 @@ def setUpClass(cls): cls.test_specification_md_path = join( cls.current_directory_path, "test_specification.md" ) - cls.parsed_requirements = parse_requirements( - [cls.test_specification_md_path] - )[cls.test_specification_md_path] def test_find_markdown_file_paths(self): self.assertIn( @@ -28,20 +26,103 @@ def test_find_markdown_file_paths(self): find_markdown_file_paths(self.current_directory_path) ) - def test_requirement_1(self): - assert self.parsed_requirements["Requirement 1"] == { - "description": "> This **MUST** be done.", - "RFC 2119 Keywords": ["MUST"] - } - - def test_requirement_2(self): - assert self.parsed_requirements["Requirement 2"] == { - "description": "> This **MUST NOT** be done.", - "RFC 2119 Keywords": ["MUST NOT"] - } + def test_parse_requirements(self): + self.assertEqual( + [ + { + "id": "Requirement 1", + "content": "This MUST be done.", + "RFC 2119 keyword": "MUST" + }, + { + "id": "Requirement 2", + "content": "This MUST NOT be done.", + "RFC 2119 keyword": "MUST NOT" + }, + { + "id": "Requirement 3", + "content": "This SHOULD be done in a certain way.", + "RFC 2119 keyword": "SHOULD" + }, + { + "id": "Requirement 4", + "content": "This MUST NOT be done.", + "RFC 2119 keyword": "MUST NOT" + }, + ], + parse_requirements([self.test_specification_md_path]) + ) - def test_requirement_3(self): - assert self.parsed_requirements["Requirement 3"] == { - "description": "> This **SHOULD** be done\n> in a certain way.", - "RFC 2119 Keywords": ["SHOULD"] - } + def test_parse_conditions(self): + 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 1.1.1.1", + "content": "This MAY be done.", + "RFC 2119 keyword": "MAY" + }, + { + "id": "Conditional Requirement 1.1.1.2", + "content": "This SHOULD NOT be done.", + "RFC 2119 keyword": "SHOULD NOT" + }, + { + "id": "Conditional Requirement 1.1.1.3", + "content": "This MAY be done.", + "RFC 2119 keyword": "MAY" + } + ] + } + ] + }, + { + "id": "Condition 1.2", + "content": "This is a condition.", + "children": [ + { + "id": "Conditional Requirement 1.2.1", + "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": "Conditional Requirement 2.2.1", + "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 index d3c74adc0f5..57b003876b2 100644 --- a/tools/specification_parser/test_specification.md +++ b/tools/specification_parser/test_specification.md @@ -16,3 +16,59 @@ Some content. > 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 1.1.1.1 +> > > +> > > > This **MAY** be done. +> > > +> > > ##### Conditional Requirement 1.1.1.2 +> > > +> > > > This **SHOULD NOT** be done. +> > > +> > > ##### Conditional Requirement 1.1.1.3 +> > > +> > > > This **MAY** be done. +> +> ##### Condition 1.2 +> +> > This is a condition. +> > +> > ##### Conditional Requirement 1.2.1 +> > +> > > This **MUST** be done. + +Some content. + +##### Requirement 4 + +> 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 2.2.1 +> > > +> > > > This **MAY** be done. + +Some content. From a0e0b2693a2049427cb2b18537f9a8eed9e6f968 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 3 Nov 2021 12:40:36 +0100 Subject: [PATCH 3/3] Update to handle requirement names --- conformance_clause.md | 21 +++++------- .../specification_parser.py | 22 +++++++------ .../specification_parser_test.py | 33 ++++++++++++------- .../test_specification.md | 18 +++++----- 4 files changed, 52 insertions(+), 42 deletions(-) diff --git a/conformance_clause.md b/conformance_clause.md index 7df61c7fa08..0b1b77ecf8f 100644 --- a/conformance_clause.md +++ b/conformance_clause.md @@ -10,7 +10,7 @@ conformance clause follows the _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 number. +words *Conditional Requirement* and an identifying name. #### Requirement Normative Parts @@ -21,21 +21,20 @@ Normative Part. Here is an example of a Requirement Normative Part: -##### Requirement 4 +##### Requirement Some name > The trace **MUST** have an identifier. This is the markdown code for the previous example: ``` -##### Requirement 4: +##### 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 number must be unique per file and continuously increasing -from 1. +characters). The requirement name must be unique per file. A blank line must follow the header. @@ -44,7 +43,7 @@ first character of any line that makes the content of the Normative Part. Multiple lines can start with this character: ``` -##### Requirement 6: +##### Requirement Another name: > The span **MAY** include > additional metadata. @@ -68,17 +67,15 @@ Here is an example of a Conditional Requirement Normative Part: > The API does not operate directly on the `Context`. > -> ##### Conditional Requirement 1.1 +> ##### 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. The conditional requirement -number must be composed of the containing condition number a period and an -unique number continuously increasing from 1. As more condition or requirement +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 or conditional requirement numbers. +added to the condition number. A blank line must follow the header. @@ -97,7 +94,7 @@ This is the markdown code for the previous example: > The API does not operate directly on the `Context`. > -> ##### Conditional Requirement 1.1 +> ##### Conditional Requirement Some other name > > > The API **MUST** provide an `extract` function to extract the `Baggage` > > from a `Context` instance. diff --git a/tools/specification_parser/specification_parser.py b/tools/specification_parser/specification_parser.py index 402e5c4826a..fe065d929c6 100644 --- a/tools/specification_parser/specification_parser.py +++ b/tools/specification_parser/specification_parser.py @@ -67,7 +67,7 @@ def parse_requirements(markdown_file_path): for requirement in [ requirement_match.groupdict() for requirement_match in ( finditer( - r"##### (?PRequirement [0-9]+)\n\n" + r"##### Requirement (?P(.*?)+)\n\n" r"> (?P(.*?))\n\n", markdown_file.read(), DOTALL @@ -80,6 +80,7 @@ def parse_requirements(markdown_file_path): 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) } @@ -88,6 +89,11 @@ def parse_requirements(markdown_file_path): return requirements +regex = compile_( + r"(?P(> ?)*)(?P##### )?(?P.*)" +) + + def parse_conditions(markdown_file_path): conditions = [] @@ -102,10 +108,6 @@ def parse_conditions(markdown_file_path): stack = [] - regex = compile_( - r"(?P(> ?)*)(?P##### )?(?P.*)" - ) - text = "" for line in condition.split("\n"): @@ -132,8 +134,12 @@ def parse_conditions(markdown_file_path): "children": [] } else: + + content = sub(r"Conditional Requirement ", "", content) + node = { "id": content, + "clean id": sub(r"[^\w]", "_", content.lower()), "content": "", "RFC 2119 keyword": None } @@ -159,11 +165,7 @@ def parse_conditions(markdown_file_path): stack.pop() text = "" - from ipdb import set_trace - try: - stack[-1]["children"].append(node) - except: - set_trace() + stack[-1]["children"].append(node) stack.append(node) stack[-1]["content"] = clean_content(text) diff --git a/tools/specification_parser/specification_parser_test.py b/tools/specification_parser/specification_parser_test.py index a84a5c83f49..26a4fb2dc36 100644 --- a/tools/specification_parser/specification_parser_test.py +++ b/tools/specification_parser/specification_parser_test.py @@ -27,33 +27,39 @@ def test_find_markdown_file_paths(self): ) def test_parse_requirements(self): + self.maxDiff = None self.assertEqual( [ { - "id": "Requirement 1", + "id": "Some requirement", + "clean id": "some_requirement", "content": "This MUST be done.", "RFC 2119 keyword": "MUST" }, { - "id": "Requirement 2", + "id": "Some other requirement", + "clean id": "some_other_requirement", "content": "This MUST NOT be done.", "RFC 2119 keyword": "MUST NOT" }, { - "id": "Requirement 3", + "id": "Another requirement-name", + "clean id": "another_requirement_name", "content": "This SHOULD be done in a certain way.", "RFC 2119 keyword": "SHOULD" }, { - "id": "Requirement 4", + "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]) + parse_requirements(self.test_specification_md_path) ) def test_parse_conditions(self): + self.maxDiff = None self.assertEqual( [ { @@ -69,17 +75,20 @@ def test_parse_conditions(self): "content": "This is a condition.", "children": [ { - "id": "Conditional Requirement 1.1.1.1", + "id": "Conditional requirement name", + "clean id": "conditional_requirement_name", "content": "This MAY be done.", "RFC 2119 keyword": "MAY" }, { - "id": "Conditional Requirement 1.1.1.2", + "id": "Other name", + "clean id": "other_name", "content": "This SHOULD NOT be done.", "RFC 2119 keyword": "SHOULD NOT" }, { - "id": "Conditional Requirement 1.1.1.3", + "id": "Another name here", + "clean id": "another_name_here", "content": "This MAY be done.", "RFC 2119 keyword": "MAY" } @@ -92,7 +101,8 @@ def test_parse_conditions(self): "content": "This is a condition.", "children": [ { - "id": "Conditional Requirement 1.2.1", + "id": "And another-name", + "clean id": "and_another_name", "content": "This MUST be done.", "RFC 2119 keyword": "MUST" } @@ -113,7 +123,8 @@ def test_parse_conditions(self): "content": "This is a condition.", "children": [ { - "id": "Conditional Requirement 2.2.1", + "id": "The name here", + "clean id": "the_name_here", "content": "This MAY be done.", "RFC 2119 keyword": "MAY" } @@ -124,5 +135,5 @@ def test_parse_conditions(self): ] } ], - parse_conditions([self.test_specification_md_path]) + parse_conditions(self.test_specification_md_path) ) diff --git a/tools/specification_parser/test_specification.md b/tools/specification_parser/test_specification.md index 57b003876b2..9678cc7245a 100644 --- a/tools/specification_parser/test_specification.md +++ b/tools/specification_parser/test_specification.md @@ -4,15 +4,15 @@ Some content. -##### Requirement 1 +##### Requirement Some requirement > This **MUST** be done. -##### Requirement 2 +##### Requirement Some other requirement > This **MUST NOT** be done. -##### Requirement 3 +##### Requirement Another requirement-name > This **SHOULD** be done > in a certain way. @@ -29,15 +29,15 @@ Some content. > > > > > This is a condition. > > > -> > > ##### Conditional Requirement 1.1.1.1 +> > > ##### Conditional Requirement Conditional requirement name > > > > > > > This **MAY** be done. > > > -> > > ##### Conditional Requirement 1.1.1.2 +> > > ##### Conditional Requirement Other name > > > > > > > This **SHOULD NOT** be done. > > > -> > > ##### Conditional Requirement 1.1.1.3 +> > > ##### Conditional Requirement Another name here > > > > > > > This **MAY** be done. > @@ -45,13 +45,13 @@ Some content. > > > This is a condition. > > -> > ##### Conditional Requirement 1.2.1 +> > ##### Conditional Requirement And another-name > > > > > This **MUST** be done. Some content. -##### Requirement 4 +##### Requirement This is the name of-the-requirement > This **MUST NOT** be done. @@ -67,7 +67,7 @@ Some content. > > > > > This is a condition. > > > -> > > ##### Conditional Requirement 2.2.1 +> > > ##### Conditional Requirement The name here > > > > > > > This **MAY** be done.