Developing docsteady#

See also the API docs

Releasing a new version#

  1. conda activate docsteady-dev (environment used for development)

    ( if not created previously conda create --name docsteady-dev docsteady -c lsst-dm -c conda-forge )

  2. In the branch, create the tag and push it

  3. conda build recipe/

  4. on a different terminal activate the base conda env, where you have anaconda installed, and you have logged in

    (You may need to anaconda login - credentials in 1password)

  5. copy and paste the anaconda upload command line proposed at the end of the build, and add --user lsst-dm:

    (otherwise it will be uploaded to the user logged in) e.g. ` anaconda upload --user lsst-dm /usr/local/anaconda3/conda-bld/noarch/docsteady-2.0_0_gae9669c-py_0.tar.bz2 `

  6. merge the branch

Developing#

docsteady is a pure python tool but depends on pandoc, which is a C++ compiled library available only as a conda package. It has been observed that any small change in the version of pandoc may lead to unexpected changes in the resulting LaTeX~format.

Therefore, in order to ensure the expected pandoc behavior, it is important to set-up the conda environment corresponding to the latest docsteady working version. The environment set-up is explained in section install.

The docsteady source code is available at On github <https://github.com/lsst-dm/docsteady>__

To test changes done locally in the source code, use the following procedure:

  • (if not already available) create the environment as specified in section install

  • activate the environment: conda activate docsteady-env

  • clone docsteady repository and checkout a ticket branch

  • do your changes

  • install the updates in the docsteady-env environment: conda install .

  • activate the same docsteady-env environment in a different terminal to test the new changes

  • once the changes are OK, commit them in the repository and open a PR for merging the branch to master

Before Committing#

Enable pre-commit in github this will run black and flake8 on your code before you commit.

The project is now set up with tox - run tox before your commit to do type checking run tests etc.

Before tagging a release make sure the the pushed version passes github actions.

New Recipe#

With pypproject.toml the best way to get a new conda recipe seems to e grayskull. You can pip install grayskull then:

grayskull pypi --strict-conda-forge docsteady

However this does not provide a full working recipe. The source needs to be path: ../. I also found the entry point was incorrect and needed to be changed to docsteady=docsteady:cli.

In the last release 2.5 the docsteady source had to be moved under ‘’src’’ for conda build to pick it up.

Documentation Procedure#

This is the general approach for docsteady generated documents:

  • Create a document handle in DocuShare

  • Use the document handle to create a repository in GitHub using sqrbot-jr, which will also create the corresponding landing page in lsst.io

  • Configure a github action

  • Render the document to a ticket branch, or to the textbf{jira-sync} special branch. Never auto-generate the document directly to master

  • Ensure that the document is correctly published in the corresponding LSST The Docs landing page and that everybody who is interested can access it.

  • Create a GitHub Pull Request to let contributors and stakeholders comment on the changes.

  • When a set of activities are completed, and all comments have been addressed, merge the branch/PR to master.

  • In case the special textbf{jira-sync} branch is used, after merging it to master, delete it and recreate from the latest master. Documentation tags corresponding to official issues of the document in Docushare can also be done in the jira-sync special branch.

Authentication#

Two generic set of credentials to access the Jira REST API and the Jira database have been defined. These credentials are available at 1password.com, in the LSST-IT architecture vault, but not yet integrated into docsteady. In order to use these credentials, they have to be configured using environment variables, added as options from the command line, or entered when prompted, as specified in this technical note.

For the VCD, there is the option of running directly against the Jira database. In the 1password vault, the credentials and database IP address are under Jira DataBase RO Credentials.

For the GitHub Action, the REST API credentials have been added as secrets in the GitHub organization for PSE and DM reports.

Writing Templates#

The templating engine we use is jinja2. (See http://jinja.pocoo.org/docs/2.10/). We use pandoc for converting things between different formats.

In general, you can write a a template using jinja in any language supported by pandoc, including latex, html, markdown, and restructured text. Our default language is latex.

Resolving templates#

For both goals, docsteady will first look for a template in `load-from`, which defaults to the current working directory, and if no template is found, it will then default to the templates defined in this package under `docsteady/templates`.

  • In the case of the generate-spec goal, it will by default look for a spec template.

  • In the case of generate-cycle goal, it will look for a cycle template.

  • When no options are presented to docsteady, the defaults are: - dm-spec.latex.jinja2 for generate-spec - dm-cycle.latex.jinja2 for generate-cycle - The generate format is {namespace}-{goal}.{template_format}.jinja2

  • An appendix can be processed separately. Accordingly, the defaults are: - dm-spec-appendix.latex.jinja2 for generate-spec - dm-cycle-appendix.latex.jinja2 for generate-cycle - The general format is {namespace}-{goal}-appendix.{template_format}.jinja2

Fields#

String, Integer, etc…#

This is just simple types and are treated as such in the templates.

Timestamps (arrow)#

Timestamps are parsed and loaded to arrow objects. This allows flexible formatting when writing out to template. Timestamps are converted to US/Pacific by default.

A naive formatting of an arrow timestamp looks like this:

{{ testresult.execution_date.format('YYYY-MM-DD HH:mm:ss') }}

For more information on formatting and conversion, see the arrow documentation: https://arrow.readthedocs.io/en/latest/.

HtmlPandocField#

Fields that are designated as HtmlPandocField means that docsteady will take the HTML output verbatim from Jira and translate that directly to the template language. This is possible because the Adaptavist Test Management framework provides a rich text editor, and stores the output as HTML. For Latex templates, this means your HTML is close to WYSIWYG in Latex.

MarkdownableHtmlPandocField#

Fields that are designated as MarkdownableHtmlPandocField will be interpreted primarily as an HtmlPandocField _unless_ a special tag is found in the first line of the field:

[markdown]: #

If that tag is found, the text in jira field is interpreted as plain text, (ignoring any HTML formatting) and translated to the target template language. This includes code highlighting.

A more complete example:

~~~markdown
[markdown]: #

# This is a markdown version of a test step

We can embed code in it:

```python
import sys
sys.exit(1)
```

… And it will be formatted in the final document just fine.

Models#

There are currently two main types of documents that can be generated - test specs and test cycles. In both cases, there are models in the respective files (docsteady/spec.py, docsteady/cycle.py) and a small amount of code to aid in building the models.

Spec model and generate-spec target#

generate-spec objects#

  • testcases: List of testcases (ordered) (type: List[TestCase])

  • requirements_to_testcases: Map of requirement key to testcase key (type: Dict[str, str])

  • requirements_map: All found requirements - requirement key to requirement (type: Dict[str, Issue])

  • testcases_map: All found testcases - testcase key to testcase (type: Dict[str, TestCase]). This includes all test cases found in test scripts.

Spec Model#

The following is a simplified version of the code in docsteady/spec.py to aid template development.

class TestCase(Schema):
        key = fields.String(required=True)
        name = fields.String(required=True)
        #: String of owner's fullname
        owner = fields.Function(deserialize=lambda obj: owner_for_id(obj))
        #: Owner's Jira ID
        owner_id = fields.String(load_from="owner", required=True)
        #: Url of this test case
        jira_url = fields.String()
        component = fields.String()
        #: Nominal type is an arrow Timestamp
        created_on = fields.Function(deserialize=lambda o: as_arrow(o['createdOn']))
        precondition = HtmlPandocField()
        objective = HtmlPandocField()
        version = fields.Integer(load_from='majorVersion', required=True)
        status = fields.String(required=True)
        priority = fields.String(required=True)
        labels = fields.List(fields.String(), missing=list())
        #: Nominal type is a List[TestStep], see below
        test_script = fields.Method(deserialize="process_steps", load_from="testScript", required=True)
        issue_links = fields.List(fields.String(), load_from="issueLinks")

        # Just in case it's necessary - these aren't guaranteed to be correct
        custom_fields = fields.Dict(load_from="customFields")

        # custom fields go here and in pre_load
        verification_type = fields.String()
        verification_configuration = HtmlPandocField()
        predecessors = HtmlPandocField()
        critical_event = fields.String()
        associated_risks = HtmlPandocField()
        unit_under_test = HtmlPandocField()
        required_software = HtmlPandocField()
        test_equipment = HtmlPandocField()
        test_personnel = HtmlPandocField()
        safety_hazards = HtmlPandocField()
        required_ppe = HtmlPandocField()
        postcondition = HtmlPandocField()

        # synthesized fields (See @pre_load and @post_load)
        doc_href = fields.String()

        #: See below
        requirements = fields.Nested(Issue, many=True)

    class Issue(Schema):
        key = fields.String(required=True)
        summary = fields.String()
        jira_url = fields.String()

    class TestStep(Schema):
        index = fields.Integer()
        test_case_key = fields.String(load_from="testCaseKey")
        description = MarkdownableHtmlPandocField()
        expected_result = MarkdownableHtmlPandocField(load_from="expectedResult")
        test_data = MarkdownableHtmlPandocField(load_from="testData")

Simple Example#

If you added example template (docsteady/templates/example-spec.markdown.jinja2), defined as:

# Testcases

{% for testcase in testcases %}
## {{ testcase.name }}
On the web at {{ testcase.jira_url }}

### Requirements:
{% for requirement in testcase.requirements %}
* {{ requirement.key }} at {{ requirement.jira_url }}
{% endfor %}

{% endfor %}
You could generate the resultant file, in latex (by default) via::

docsteady --namespace example --template markdown generate-spec "/Data Management/Prompt

Or actually ask for it in markdown::

docsteady --namespace example --template markdown generate-spec --format markdown "/Data Management/Prompt"

Or HTML::

docsteady --namespace example --template markdown generate-spec --format html "/Data Management/Prompt"

Cycle model and generate-cycle#

generate-cycle template objects#

  • testcycle: Test Cycle object (type: TestCycle)

  • testresult: List of Test results as found from the test cycle (type: List[TestResult])

  • testcases_map: All found testcases when processing test results - testcase key to testcase (type: Dict[str, TestCase]). This includes all test cases found from the test results.

Cycle model#

class TestCycle(Schema):
    key = fields.String(required=True)
    name = fields.String(required=True)
    description = fields.String(required=True)
    status = fields.String(required=True)
    execution_time = fields.Integer(required=True, load_from="executionTime")
    created_on = fields.Function(deserialize=lambda o: as_arrow(o['createdOn']))
    updated_on = fields.Function(deserialize=lambda o: as_arrow(o['updatedOn']))
    planned_start_date = fields.Function(deserialize=lambda o: as_arrow(o['plannedStartDate']))
    owner_id = fields.String(load_from="owner", required=True)
    owner = fields.Function(deserialize=lambda obj: owner_for_id(obj))
    created_by = fields.Function(deserialize=lambda obj: owner_for_id(obj), load_from="createdBy")
    custom_fields = fields.Dict(load_from="customFields")
    items = fields.Nested(TestCycleItem, many=True)

    # custom fields
    software_version = HtmlPandocField()

class TestCycleItem(Schema):
    id = fields.Integer(required=True)
    test_case_key = fields.Function(deserialize=lambda key: test_case_for_key(key)["key"],
                                    load_from='testCaseKey', required=True)
    user_id = fields.String(load_from="userKey")
    user = fields.Function(deserialize=lambda obj: owner_for_id(obj["userKey"]))
    execution_date = fields.Function(deserialize=lambda o: as_arrow(o['executionDate']))
    status = fields.String(required=True)

class TestResult(Schema):
    id = fields.Integer(required=True)
    key = fields.String(required=True)
    automated = fields.Boolean(required=True)
    environment = fields.String()
    execution_time = fields.Integer(load_from='executionTime', required=True)
    test_case_key = fields.Function(deserialize=lambda key: test_case_for_key(key)["key"],
                                    load_from='testCaseKey', required=True)
    execution_date = fields.Function(deserialize=lambda o: as_arrow(o), required=True,
                                     load_from='executionDate')
    script_results = fields.Nested(ScriptResult, many=True, load_from="scriptResults",
                                   required=True)
    issues = fields.Nested(Issue, many=True)
    issue_links = fields.List(fields.String(), load_from="issueLinks")
    user_id = fields.String(load_from="userKey")
    user = fields.Function(deserialize=lambda obj: owner_for_id(obj), load_from="userKey")
    status = fields.String(load_from='status', required=True)

class ScriptResult(Schema):
    index = fields.Integer(load_from='index')
    expected_result = MarkdownableHtmlPandocField(load_from='expectedResult')
    execution_date = fields.String(load_from='executionDate')
    description = MarkdownableHtmlPandocField(load_from='description')
    comment = MarkdownableHtmlPandocField(load_from='comment')
    status = fields.String(load_from='status')