├── .coveragerc ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── change-coverage.yml │ ├── execute-tests.yml │ ├── test-linux.yml │ ├── test-osx.yml │ └── test-windows.yml ├── .gitignore ├── .mypy.ini ├── .noserc ├── .pre-commit-hooks.yaml ├── .project ├── .pydevproject ├── .pydocstyle.ini ├── .pylint.ini ├── .readthedocs.yaml ├── .scrutinizer.yml ├── .tool-versions ├── .verchew.ini ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Doorstop.sublime-project ├── LICENSE.md ├── MANIFEST.in ├── Makefile ├── Procfile ├── README.md ├── bin ├── checksum ├── example-adapter.wsgi ├── open ├── post_compile └── verchew ├── docs ├── about │ ├── changelog.md │ ├── contributing.md │ └── license.md ├── api │ └── scripting.md ├── assets │ └── logo.xcf ├── cli │ ├── creation.md │ ├── interchange.md │ ├── publishing.md │ ├── reordering.md │ └── validation.md ├── examples.md ├── generate.py ├── getting-started │ ├── installation.md │ ├── quickstart.md │ └── setup.md ├── gui │ ├── desktop-gui.png │ └── overview.md ├── images │ └── logo-black-white.png ├── index.md ├── reference │ ├── document.md │ ├── item.md │ └── tree.md ├── requirements.txt └── web.md ├── doorstop ├── __init__.py ├── cli │ ├── __init__.py │ ├── commands.py │ ├── main.py │ ├── tests │ │ ├── __init__.py │ │ ├── docs │ │ │ ├── .doorstop.yml │ │ │ ├── HLT001.yml │ │ │ ├── HLT002.yml │ │ │ ├── HLT003.yml │ │ │ ├── HLT004.yml │ │ │ └── HLT005.yml │ │ ├── files │ │ │ ├── A001.txt │ │ │ ├── A002.txt │ │ │ ├── B001.txt │ │ │ ├── B002.txt │ │ │ ├── C001.txt │ │ │ ├── C002.txt │ │ │ ├── C003.txt │ │ │ ├── exported-map.csv │ │ │ ├── exported.csv │ │ │ ├── exported.tsv │ │ │ ├── exported.xlsx │ │ │ └── template.yml │ │ ├── test_all.py │ │ ├── test_main.py │ │ ├── test_utilities.py │ │ └── tutorial.py │ └── utilities.py ├── common.py ├── core │ ├── __init__.py │ ├── base.py │ ├── builder.py │ ├── document.py │ ├── editor.py │ ├── exporter.py │ ├── files │ │ └── templates │ │ │ ├── html │ │ │ ├── bootstrap.bundle.min.js │ │ │ ├── bootstrap.min.css │ │ │ ├── doorstop.css │ │ │ ├── general.css │ │ │ ├── jquery.min.js │ │ │ ├── logo-black-white.png │ │ │ ├── output │ │ │ │ ├── chtml.js │ │ │ │ ├── chtml │ │ │ │ │ └── fonts │ │ │ │ │ │ ├── tex.js │ │ │ │ │ │ └── woff-v2 │ │ │ │ │ │ ├── MathJax_AMS-Regular.woff │ │ │ │ │ │ ├── MathJax_Calligraphic-Bold.woff │ │ │ │ │ │ ├── MathJax_Calligraphic-Regular.woff │ │ │ │ │ │ ├── MathJax_Fraktur-Bold.woff │ │ │ │ │ │ ├── MathJax_Fraktur-Regular.woff │ │ │ │ │ │ ├── MathJax_Main-Bold.woff │ │ │ │ │ │ ├── MathJax_Main-Italic.woff │ │ │ │ │ │ ├── MathJax_Main-Regular.woff │ │ │ │ │ │ ├── MathJax_Math-BoldItalic.woff │ │ │ │ │ │ ├── MathJax_Math-Italic.woff │ │ │ │ │ │ ├── MathJax_Math-Regular.woff │ │ │ │ │ │ ├── MathJax_SansSerif-Bold.woff │ │ │ │ │ │ ├── MathJax_SansSerif-Italic.woff │ │ │ │ │ │ ├── MathJax_SansSerif-Regular.woff │ │ │ │ │ │ ├── MathJax_Script-Regular.woff │ │ │ │ │ │ ├── MathJax_Size1-Regular.woff │ │ │ │ │ │ ├── MathJax_Size2-Regular.woff │ │ │ │ │ │ ├── MathJax_Size3-Regular.woff │ │ │ │ │ │ ├── MathJax_Size4-Regular.woff │ │ │ │ │ │ ├── MathJax_Typewriter-Regular.woff │ │ │ │ │ │ ├── MathJax_Vector-Bold.woff │ │ │ │ │ │ ├── MathJax_Vector-Regular.woff │ │ │ │ │ │ └── MathJax_Zero.woff │ │ │ │ ├── svg.js │ │ │ │ └── svg │ │ │ │ │ └── fonts │ │ │ │ │ └── tex.js │ │ │ └── tex-mml-chtml.js │ │ │ └── latex │ │ │ ├── doorstop.cls │ │ │ ├── doorstop.yml │ │ │ └── logo-black-white.png │ ├── importer.py │ ├── item.py │ ├── publisher.py │ ├── publishers │ │ ├── __init__.py │ │ ├── _latex_functions.py │ │ ├── base.py │ │ ├── html.py │ │ ├── latex.py │ │ ├── markdown.py │ │ ├── tests │ │ │ ├── helpers.py │ │ │ ├── helpers_latex.py │ │ │ ├── test_publisher_html.py │ │ │ ├── test_publisher_html_doc.py │ │ │ ├── test_publisher_latex.py │ │ │ ├── test_publisher_latex_doc.py │ │ │ ├── test_publisher_latex_environments.py │ │ │ ├── test_publisher_markdown.py │ │ │ ├── test_publisher_markdown_doc.py │ │ │ └── test_publisher_text.py │ │ └── text.py │ ├── reference_finder.py │ ├── template.py │ ├── tests │ │ ├── __init__.py │ │ ├── docs │ │ │ ├── .doorstop.yml │ │ │ ├── LLT001.yml │ │ │ ├── LLT002.yml │ │ │ ├── LLT003.yml │ │ │ ├── LLT004.yml │ │ │ ├── LLT005.yml │ │ │ ├── LLT007.yml │ │ │ ├── LLT008.yml │ │ │ ├── LLT009.yml │ │ │ └── LLT010.yml │ │ ├── files │ │ │ ├── .doorstop.skip │ │ │ ├── .doorstop.yml │ │ │ ├── .venv │ │ │ │ └── doorstop │ │ │ │ │ └── reqs │ │ │ │ │ └── .doorstop.yml │ │ │ ├── REQ001.yml │ │ │ ├── REQ002.yml │ │ │ ├── REQ003.yml │ │ │ ├── REQ006.yml │ │ │ ├── REQ2-001.yml │ │ │ ├── a │ │ │ │ ├── b │ │ │ │ │ └── template.yml │ │ │ │ └── template.yml │ │ │ ├── child │ │ │ │ ├── .doorstop.skip │ │ │ │ ├── .doorstop.yml │ │ │ │ ├── TST001.yml │ │ │ │ └── TST002.yml │ │ │ ├── exported-huge.xlsx │ │ │ ├── exported-modified.csv │ │ │ ├── exported.csv │ │ │ ├── exported.tsv │ │ │ ├── exported.xlsx │ │ │ ├── exported.yml │ │ │ ├── external │ │ │ │ ├── text.txt │ │ │ │ └── text2.txt │ │ │ ├── formula.xlsx │ │ │ ├── index.html │ │ │ ├── index.md │ │ │ ├── index2.html │ │ │ ├── index2.md │ │ │ ├── new │ │ │ │ ├── .doorstop.skip │ │ │ │ └── .doorstop.yml │ │ │ ├── parent │ │ │ │ ├── .doorstop.skip │ │ │ │ ├── .doorstop.yml │ │ │ │ ├── SYS001.yml │ │ │ │ └── SYS002.yml │ │ │ ├── published.html │ │ │ ├── published.md │ │ │ ├── published.txt │ │ │ ├── published2.html │ │ │ ├── published2.md │ │ │ ├── published2.txt │ │ │ ├── skipped.txt │ │ │ ├── subfolder │ │ │ │ ├── REQ004.yml │ │ │ │ └── REQ005.yml │ │ │ └── testmatrix.csv │ │ ├── files_md │ │ │ ├── .doorstop.skip │ │ │ ├── .doorstop.yml │ │ │ ├── REQ001.md │ │ │ └── REQ002.yml │ │ ├── helpers.py │ │ ├── test_all.py │ │ ├── test_builder.py │ │ ├── test_common.py │ │ ├── test_document.py │ │ ├── test_exporter.py │ │ ├── test_fixtures │ │ │ ├── 001-item-references-utf8-keyword │ │ │ │ ├── REQ-UTF8.yml │ │ │ │ └── files │ │ │ │ │ └── text-utf8.txt │ │ │ └── 002-utf8-characters │ │ │ │ ├── REQ-CYRILLIC.yml │ │ │ │ ├── REQ-CYRILLIC_crlf.yml │ │ │ │ └── REQ-MIT.yml │ │ ├── test_importer.py │ │ ├── test_item.py │ │ ├── test_item_extensions.py │ │ ├── test_item_validator.py │ │ ├── test_publisher.py │ │ ├── test_reference_finder.py │ │ ├── test_template.py │ │ ├── test_tree.py │ │ ├── test_types.py │ │ ├── test_yaml_validator.py │ │ └── validators │ │ │ └── validator_dummy.py │ ├── tree.py │ ├── types.py │ ├── validators │ │ └── item_validator.py │ ├── vcs │ │ ├── __init__.py │ │ ├── base.py │ │ ├── git.py │ │ ├── mercurial.py │ │ ├── mockvcs.py │ │ ├── subversion.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── test_all.py │ │ │ ├── test_base.py │ │ │ └── test_commands.py │ └── yaml_validator.py ├── gui │ ├── __init__.py │ ├── application.py │ ├── main.py │ ├── resources.py │ ├── tests │ │ ├── __init__.py │ │ └── test_all.py │ ├── utilTkinter.py │ └── widget.py ├── server │ ├── __init__.py │ ├── client.py │ ├── main.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_all.py │ │ ├── test_api.py │ │ ├── test_client.py │ │ └── test_server.py │ └── utilities.py ├── settings.py ├── tests │ └── test_init.py └── views │ ├── base.tpl │ ├── document_list.tpl │ ├── doorstop.tpl │ └── item_list.tpl ├── git_hooks └── check_unreviewed_requirements.sh ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml ├── pytest.log ├── reqs ├── .doorstop.yml ├── REQ001.yml ├── REQ002.yml ├── REQ003.yml ├── REQ004.yml ├── REQ006.yml ├── REQ007.yml ├── REQ008.yml ├── REQ009.yml ├── REQ010.yml ├── REQ011.yml ├── REQ012.yml ├── REQ013.yml ├── REQ014.yml ├── REQ015.yml ├── REQ016.yml ├── REQ017.yml ├── REQ018.yml ├── REQ019.yml ├── assets │ └── logo-black-white.png ├── ext │ ├── .doorstop.yml │ ├── .req_sha_item_validator.py │ ├── EXT001.yml │ ├── EXT002.yml │ ├── test-modified.file │ └── test.file └── tutorial │ ├── .doorstop.yml │ ├── TUT001.yml │ ├── TUT002.yml │ ├── TUT003.yml │ ├── TUT004.yml │ ├── TUT005.yml │ ├── TUT008.yml │ ├── TUT009.yml │ ├── TUT010.yml │ ├── TUT011.yml │ ├── TUT012.yml │ ├── TUT013.yml │ ├── TUT014.yml │ ├── TUT015.yml │ ├── TUT016.yml │ ├── TUT017.yml │ ├── TUT018.yml │ ├── TUT019.yml │ ├── TUT020.yml │ ├── TUT021.yml │ ├── TUT022.yml │ ├── TUT023.yml │ ├── TUT024.yml │ └── TUT025.yml └── scent.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | 3 | branch = true 4 | 5 | omit = 6 | .venv/* 7 | */tests/* 8 | */__main__.py 9 | 10 | dynamic_context = test_function 11 | 12 | [report] 13 | 14 | omit = 15 | */gui/main.py 16 | */gui/application.py 17 | */gui/utilTkinter.py 18 | */gui/widget.py 19 | 20 | exclude_lines = 21 | pragma: no cover 22 | raise NotImplementedError 23 | except DistributionNotFound 24 | 25 | skip_covered = false 26 | 27 | [html] 28 | show_contexts = True 29 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.bat] 12 | end_of_line = crlf 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [*.py] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [*.yml] 22 | indent_style = space 23 | 24 | [Makefile] 25 | indent_style = tab 26 | indent_size = 4 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | CHANGELOG.md merge=union 3 | 4 | *.xlsx binary 5 | *.min.js binary 6 | *.min.css binary 7 | doorstop/core/files/templates/html/output/*.js binary 8 | doorstop/core/files/templates/html/tex-mml-chtml.js binary 9 | doorstop/core/tests/files/published*.html binary 10 | 11 | # Python, Git, and Cygwin have different ideas about the correct line endings 12 | *.yml -text 13 | *.txt -text 14 | *.md -text 15 | *.html -text 16 | *.csv -text 17 | *.tsv -text 18 | -------------------------------------------------------------------------------- /.github/workflows/change-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Execute tests 2 | on: 3 | workflow_call: 4 | inputs: 5 | basepath: 6 | required: false 7 | type: string 8 | os: 9 | required: true 10 | type: string 11 | workpath: 12 | required: true 13 | type: string 14 | 15 | jobs: 16 | change-coverage: 17 | runs-on: ubuntu-latest 18 | name: Change coverage 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.11" 28 | architecture: x64 29 | 30 | - uses: Gr1N/setup-poetry@v9 31 | 32 | - name: Check system dependencies 33 | run: make doctor 34 | 35 | - uses: actions/cache@v4 36 | with: 37 | path: .venv 38 | key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} 39 | 40 | - name: Install project dependencies 41 | run: make install 42 | 43 | - name: Check coverage 44 | run: | 45 | TEST_INTEGRATION=true poetry run pytest doorstop --doctest-modules --cov=doorstop --cov-report=xml --cov-report=term-missing 46 | git fetch origin develop:develop 47 | # TEST_INTEGRATION=true poetry run diff-cover ./coverage.xml --fail-under=100 --compare-branch=develop 48 | TEST_INTEGRATION=true poetry run diff-cover ./coverage.xml --fail-under=100 --compare-branch=$(git for-each-ref --sort=-committerdate refs/heads/develop | cut -f 1 -d ' ') 49 | -------------------------------------------------------------------------------- /.github/workflows/execute-tests.yml: -------------------------------------------------------------------------------- 1 | name: Execute tests 2 | on: 3 | workflow_call: 4 | inputs: 5 | basepath: 6 | required: false 7 | type: string 8 | os: 9 | required: true 10 | type: string 11 | workpath: 12 | required: true 13 | type: string 14 | secrets: 15 | CODECOV_TOKEN: 16 | required: true 17 | 18 | jobs: 19 | test: 20 | runs-on: ${{ inputs.os }} 21 | strategy: 22 | matrix: 23 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 24 | name: Python ${{ matrix.python-version }} 25 | 26 | defaults: 27 | run: 28 | working-directory: ${{ inputs.workpath }} 29 | 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | 34 | - name: Change path on Windows 35 | if: ${{ inputs.os == 'windows-latest' }} 36 | # Cannot start powershell from a path that does not exist, so change 37 | # working directory for this step only. 38 | working-directory: ${{ inputs.basepath }} 39 | run: | 40 | mkdir -p ${{ inputs.workpath }} 41 | mv $env:GITHUB_WORKSPACE\* ${{ inputs.workpath }}\ -Force 42 | 43 | - uses: actions/setup-python@v5 44 | with: 45 | python-version: ${{ matrix.python-version }} 46 | architecture: x64 47 | 48 | - uses: Gr1N/setup-poetry@v9 49 | 50 | - name: Check system dependencies 51 | run: make doctor 52 | 53 | - uses: actions/cache@v4 54 | with: 55 | path: .venv 56 | key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} 57 | 58 | - name: Install project dependencies 59 | run: make install 60 | 61 | - name: Run tests 62 | run: make test 63 | 64 | - name: Upload coverage 65 | uses: codecov/codecov-action@v4 66 | if: ${{ inputs.os == 'ubuntu-latest' && matrix.python-version == '3.9' && (github.event_name == 'push' && !startsWith(github.ref, 'refs/heads/dependabot/') || (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork)) }} 67 | with: 68 | token: ${{ secrets.CODECOV_TOKEN }} 69 | fail_ci_if_error: true 70 | 71 | - name: Run checks 72 | run: make check 73 | if: ${{ inputs.os == 'ubuntu-latest' }} 74 | 75 | - name: Run demo 76 | run: make demo 77 | if: ${{ inputs.os == 'ubuntu-latest' }} 78 | -------------------------------------------------------------------------------- /.github/workflows/test-linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ develop ] 7 | 8 | jobs: 9 | Coverage: 10 | uses: ./.github/workflows/change-coverage.yml 11 | with: 12 | os: "ubuntu-latest" 13 | workpath: "/home/runner/work/doorstop/doorstop" 14 | if: github.event_name == 'pull_request' 15 | 16 | Test: 17 | uses: ./.github/workflows/execute-tests.yml 18 | with: 19 | os: "ubuntu-latest" 20 | workpath: "/home/runner/work/doorstop/doorstop" 21 | secrets: 22 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 23 | -------------------------------------------------------------------------------- /.github/workflows/test-osx.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ develop ] 7 | 8 | jobs: 9 | Test: 10 | uses: ./.github/workflows/execute-tests.yml 11 | with: 12 | os: "macos-13" 13 | workpath: "/Users/runner/work/doorstop/doorstop" 14 | secrets: 15 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 16 | -------------------------------------------------------------------------------- /.github/workflows/test-windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ develop ] 7 | 8 | jobs: 9 | Test: 10 | uses: ./.github/workflows/execute-tests.yml 11 | with: 12 | basepath: 'D:\' 13 | os: "windows-latest" 14 | workpath: 'C:\a\doorstop\doorstop' 15 | secrets: 16 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary Python files 2 | *.pyc 3 | *.egg-info 4 | __pycache__ 5 | .ipynb_checkpoints 6 | setup.py 7 | 8 | # Temporary OS files 9 | Icon* 10 | 11 | # Temporary virtual environment files 12 | /.cache/ 13 | /.venv/ 14 | /.python-version 15 | 16 | # Temporary server files 17 | .env 18 | *.pid 19 | 20 | # Generated documentation 21 | /docs/gen/ 22 | /docs/apidocs/ 23 | /site/ 24 | /*.html 25 | /docs/*.png 26 | texput.log 27 | 28 | # Google Drive 29 | *.gdoc 30 | *.gsheet 31 | *.gslides 32 | *.gdraw 33 | 34 | # Testing and coverage results 35 | /.coverage 36 | /.coverage.* 37 | /htmlcov/ 38 | /xmlreport/ 39 | /pyunit.xml 40 | /tmp/ 41 | *.tmp 42 | coverage.xml 43 | 44 | # Build and release directories 45 | /build/ 46 | /dist/ 47 | *.spec 48 | **/MathJax 49 | 50 | # Sublime Text 51 | *.sublime-workspace 52 | 53 | # Eclipse 54 | .settings 55 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | 3 | ignore_missing_imports = true 4 | no_implicit_optional = true 5 | check_untyped_defs = true 6 | 7 | cache_dir = .cache/mypy/ 8 | 9 | [mypy-doorstop.*.tests.*] 10 | 11 | ignore_errors = True 12 | -------------------------------------------------------------------------------- /.noserc: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | 3 | exe=1 4 | 5 | with-doctest=1 6 | 7 | with-coverage=1 8 | cover-package=doorstop.common,doorstop.core, 9 | doorstop.cli.main,doorstop.cli.utilities, 10 | doorstop.gui,doorstop.server 11 | cover-erase=1 12 | 13 | verbosity=1 14 | logging-level=DEBUG 15 | logging-format=[%(levelname)-8s] %(message)s 16 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | # expected stages: commit, commit-msg, manual, merge-commit, post-checkout, post-commit, post-merge, post-rewrite, prepare-commit-msg, push 2 | 3 | - id: check-doorstop-errors 4 | args: ["-W"] 5 | name: check for doorstop errors 6 | description: ensures the changes introduces no errors 7 | entry: doorstop 8 | language: python 9 | verbose: true 10 | pass_filenames: false 11 | stages: [commit, push, manual] 12 | 13 | - id: check-unreviewed-items 14 | name: ensure that all requirements are reviewed before being committed 15 | description: ensure that all documents/requirements are reviewed before being committed 16 | entry: git_hooks/check_unreviewed_requirements.sh 17 | language: script 18 | verbose: true 19 | pass_filenames: false 20 | stages: [merge-commit, manual] 21 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Doorstop 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | 19 | 1398478527151 20 | 21 | 26 22 | 23 | org.eclipse.ui.ide.multiFilter 24 | 1.0-name-matches-false-false-__pycache__ 25 | 26 | 27 | 28 | 1398478527152 29 | 30 | 26 31 | 32 | org.eclipse.ui.ide.multiFilter 33 | 1.0-name-matches-false-false-*.egg-info 34 | 35 | 36 | 37 | 1398478527153 38 | 39 | 26 40 | 41 | org.eclipse.ui.ide.multiFilter 42 | 1.0-name-matches-false-false-env 43 | 44 | 45 | 46 | 1398478527154 47 | 48 | 22 49 | 50 | org.eclipse.ui.ide.multiFilter 51 | 1.0-name-matches-false-false-README.rst 52 | 53 | 54 | 55 | 1398478527155 56 | 57 | 26 58 | 59 | org.eclipse.ui.ide.multiFilter 60 | 1.0-name-matches-false-false-apidocs 61 | 62 | 63 | 64 | 1398478527156 65 | 66 | 22 67 | 68 | org.eclipse.ui.ide.multiFilter 69 | 1.0-name-matches-false-false-*.sublime-* 70 | 71 | 72 | 73 | 1398478527157 74 | 75 | 22 76 | 77 | org.eclipse.ui.ide.multiFilter 78 | 1.0-name-matches-false-false-README*.html 79 | 80 | 81 | 82 | 1398478527158 83 | 84 | 26 85 | 86 | org.eclipse.ui.ide.multiFilter 87 | 1.0-name-matches-false-false-build 88 | 89 | 90 | 91 | 1398478527159 92 | 93 | 26 94 | 95 | org.eclipse.ui.ide.multiFilter 96 | 1.0-name-matches-false-false-dist 97 | 98 | 99 | 100 | 1429034866152 101 | 102 | 26 103 | 104 | org.eclipse.ui.ide.multiFilter 105 | 1.0-name-matches-false-false-htmlcov 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | python 3.0 4 | Doorstop 5 | 6 | /${PROJECT_DIR_NAME} 7 | 8 | 9 | -------------------------------------------------------------------------------- /.pydocstyle.ini: -------------------------------------------------------------------------------- 1 | [pydocstyle] 2 | 3 | # D211: No blank lines allowed before class docstring 4 | add_select = D211 5 | 6 | # D100: Missing docstring in public module 7 | # D101: Missing docstring in public class 8 | # D102: Missing docstring in public method 9 | # D103: Missing docstring in public function 10 | # D104: Missing docstring in public package 11 | # D105: Missing docstring in magic method 12 | # D107: Missing docstring in __init__ 13 | # D202: No blank lines allowed after function docstring 14 | add_ignore = D100,D101,D102,D103,D104,D105,D107,D202 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.9" 7 | jobs: 8 | post_create_environment: 9 | # Install poetry 10 | # https://python-poetry.org/docs/#installing-manually 11 | - pip install poetry 12 | post_install: 13 | # Install dependencies with 'docs' dependency group 14 | # https://python-poetry.org/docs/managing-dependencies/#dependency-groups 15 | # VIRTUAL_ENV needs to be set manually for now. 16 | # See https://github.com/readthedocs/readthedocs.org/pull/11152/ 17 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install 18 | 19 | mkdocs: 20 | configuration: mkdocs.yml 21 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | tests: 3 | override: 4 | - pylint-run --rcfile=.pylint.ini 5 | - py-scrutinizer-run 6 | checks: 7 | python: 8 | code_rating: true 9 | duplicate_code: true 10 | filter: 11 | excluded_paths: 12 | - "*/tests/*" 13 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | python 3.13.3 2 | poetry 2.1.1 3 | -------------------------------------------------------------------------------- /.verchew.ini: -------------------------------------------------------------------------------- 1 | [Make] 2 | 3 | cli = make 4 | version = GNU Make 5 | 6 | [Python] 7 | 8 | cli = python 9 | version = 3.9 || 3.10 || 3.11 || 3.12 || 3.13 10 | 11 | [Poetry] 12 | 13 | cli = poetry 14 | version = 2 15 | 16 | [Graphviz] 17 | 18 | cli = dot 19 | cli_version_arg = -V 20 | version = 9 || 10 || 11 || 12 21 | optional = true 22 | message = This is only needed to generate UML diagrams for documentation. 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | ".cache/": true, 4 | ".venv/": true, 5 | "*.egg-info": true, 6 | "pip-wheel-metadata/": true, 7 | "**/__pycache__": true, 8 | "**/*.pyc": true, 9 | "**/.ipynb_checkpoints": true, 10 | "**/tmp/": true, 11 | "dist/": true, 12 | "htmlcov/": true, 13 | "prof/": true, 14 | "site/": true, 15 | }, 16 | "python.defaultInterpreterPath": ".venv/bin/python", 17 | "pylint.args": ["--rcfile=.pylint.ini"], 18 | "editor.formatOnSave": true, 19 | "cSpell.words": [ 20 | "Autobuild", 21 | "basepath", 22 | "cget", 23 | "choco", 24 | "clevel", 25 | "cname", 26 | "codecov", 27 | "codeql", 28 | "cors", 29 | "coveragespace", 30 | "curr", 31 | "cust", 32 | "doorhole", 33 | "dunder", 34 | "endfirsthead", 35 | "endfoot", 36 | "endhead", 37 | "endlastfoot", 38 | "enduml", 39 | "evalue", 40 | "EXTS", 41 | "frontmatter", 42 | "Gitter", 43 | "graphviz", 44 | "HLINE", 45 | "hyperref", 46 | "isort", 47 | "itered", 48 | "lgpl", 49 | "LINESEPERATOR", 50 | "linkify", 51 | "LONGTABLE", 52 | "macfsevents", 53 | "mkdir", 54 | "mkdocs", 55 | "mockvcs", 56 | "mylevel", 57 | "mypy", 58 | "nlev", 59 | "nlevel", 60 | "nuid", 61 | "nums", 62 | "openpyxl", 63 | "pids", 64 | "plantuml", 65 | "plev", 66 | "plevel", 67 | "puid", 68 | "pydocstyle", 69 | "pyficache", 70 | "pygments", 71 | "pyinstaller", 72 | "pync", 73 | "pyyaml", 74 | "quickstart", 75 | "sargs", 76 | "setuptools", 77 | "sgignores", 78 | "spdx", 79 | "textit", 80 | "textwidth", 81 | "tkinter", 82 | "UID's", 83 | "uids", 84 | "unreviewed", 85 | "USERPROFILE", 86 | "venv", 87 | "vvignores", 88 | "winfo", 89 | "workpath", 90 | "xlsx", 91 | "yamlfile" 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Requirements 4 | 5 | * Make: 6 | * macOS: `$ xcode-select --install` 7 | * Linux: [https://www.gnu.org/software/make](https://www.gnu.org/software/make) 8 | * Windows: [https://mingw.org/download/installer](https://mingw.org/download/installer) 9 | * Python: `$ pyenv install` 10 | * Poetry: [https://python-poetry.org/docs/#installation](https://python-poetry.org/docs/#installation) 11 | * Graphviz: 12 | * macOS: `$ brew install graphviz` 13 | * Linux: [https://graphviz.org/download](https://graphviz.org/download/) 14 | * Windows: [https://graphviz.org/download](https://graphviz.org/download/) 15 | 16 | To confirm these system dependencies are configured correctly: 17 | 18 | ```sh 19 | $ make doctor 20 | ``` 21 | 22 | ## Installation 23 | 24 | Install project dependencies into a virtual environment: 25 | 26 | ```sh 27 | $ make install 28 | ``` 29 | 30 | # Development Tasks 31 | 32 | ## Manual 33 | 34 | Run the tests: 35 | 36 | ```sh 37 | $ make test 38 | ``` 39 | 40 | Run static analysis: 41 | 42 | ```sh 43 | $ make check 44 | ``` 45 | 46 | Build the documentation: 47 | 48 | ```sh 49 | $ make docs 50 | ``` 51 | 52 | Local install for external testing: 53 | 54 | ```sh 55 | $ make dev-install 56 | ``` 57 | 58 | Clean everything: 59 | ```sh 60 | $ make clean 61 | ``` 62 | 63 | Compare coverage to current `develop` branch to see if changes causes reduced coverage. 64 | Please run before creating a PR. 65 | ```sh 66 | $ make test-cover 67 | ``` 68 | 69 | ## Automatic 70 | 71 | Keep all of the above tasks running on change: 72 | 73 | ```sh 74 | $ make dev 75 | ``` 76 | 77 | > In order to have OS X notifications, `brew install terminal-notifier`. 78 | 79 | # Continuous Integration 80 | 81 | The CI server will report overall build status: 82 | 83 | ```sh 84 | $ make ci 85 | ``` 86 | 87 | # Release Tasks 88 | 89 | Release to PyPI: 90 | 91 | ```sh 92 | $ make upload 93 | ``` 94 | -------------------------------------------------------------------------------- /Doorstop.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | "file_exclude_patterns": [".coverage", "*.sublime-workspace"], 7 | "folder_exclude_patterns": [".*", "__pycache__", "build", "dist", "env", "*.egg-info"] 8 | } 9 | ], 10 | "settings": 11 | { 12 | "tab_size": 4, 13 | "translate_tabs_to_spaces": true 14 | }, 15 | "SublimeLinter": 16 | { 17 | "linters": 18 | { 19 | "pep257": { 20 | "@disable": false, 21 | "args": [], 22 | "excludes": [] 23 | }, 24 | "pep8": { 25 | "@disable": false, 26 | "args": [], 27 | "excludes": [], 28 | "ignore": "E501", 29 | "max-line-length": 79, 30 | "select": "" 31 | }, 32 | "pylint": { 33 | "@disable": false, 34 | "rcfile": ".pylint.ini" 35 | } 36 | } 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst *.txt *.md 2 | recursive-include docs *.rst *.txt *.md 3 | graft */files 4 | graft */*/files 5 | graft doorstop/core/files/assets -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: sappy public --port=${PORT} 2 | -------------------------------------------------------------------------------- /bin/checksum: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import hashlib 5 | import sys 6 | 7 | 8 | def run(paths): 9 | hash_md5 = hashlib.md5() 10 | 11 | for path in paths: 12 | try: 13 | with open(path, 'rb') as f: 14 | for chunk in iter(lambda: f.read(4096), b''): 15 | hash_md5.update(chunk) 16 | except IOError: 17 | hash_md5.update(path.encode()) 18 | 19 | print(hash_md5.hexdigest()) 20 | 21 | 22 | if __name__ == '__main__': 23 | run(sys.argv[1:]) 24 | -------------------------------------------------------------------------------- /bin/example-adapter.wsgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Example adapter.wsgi for doorstop. 4 | 5 | import os 6 | import sys 7 | import bottle 8 | 9 | app = None 10 | 11 | def application(environ, start_response): 12 | global app 13 | if not app: 14 | doorstop_path = "/opt/doorstop" 15 | os.chdir(os.path.dirname(__file__)) 16 | 17 | project_path = environ['DOORSTOP_PROJECT_DIR'] 18 | baseurl = environ['DOORSTOP_BASE_URL'] 19 | 20 | parameters = [ 21 | '--project', project_path, 22 | '--baseurl', baseurl 23 | '--wsgi' 24 | ] 25 | 26 | sys.path.append(doorstop_path) 27 | from doorstop.server.main import main as servermain 28 | 29 | servermain(parameters) 30 | 31 | app = bottle.default_app() 32 | 33 | return app(environ, start_response) 34 | 35 | -------------------------------------------------------------------------------- /bin/open: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | 7 | 8 | COMMANDS = { 9 | 'linux': "open", 10 | 'win32': "cmd /c start", 11 | 'cygwin': "cygstart", 12 | 'darwin': "open", 13 | } 14 | 15 | 16 | def run(path): 17 | command = COMMANDS.get(sys.platform, "open") 18 | os.system(command + ' ' + path) 19 | 20 | 21 | if __name__ == '__main__': 22 | run(sys.argv[-1]) 23 | -------------------------------------------------------------------------------- /bin/post_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | indent() { 6 | sed "s/^/ /" 7 | } 8 | 9 | puts-step() { 10 | echo "-----> $@" 11 | } 12 | 13 | puts-step "Installing dependencies with poetry" 14 | poetry install | indent 15 | 16 | puts-step "Generating HTML from requirements" 17 | touch .mockvcs 18 | poetry run doorstop publish all public | indent 19 | -------------------------------------------------------------------------------- /docs/about/changelog.md: -------------------------------------------------------------------------------- 1 | ../../CHANGELOG.md -------------------------------------------------------------------------------- /docs/about/contributing.md: -------------------------------------------------------------------------------- 1 | ../../CONTRIBUTING.md -------------------------------------------------------------------------------- /docs/about/license.md: -------------------------------------------------------------------------------- 1 | ../../LICENSE.md -------------------------------------------------------------------------------- /docs/api/scripting.md: -------------------------------------------------------------------------------- 1 |

Scripting Interface

2 | 3 | Being written in Python, Doorstop allows you to leverage the full power of Python to write scripts to manipulate requirements, run custom queries across all documents, and even inject your own validation rules. 4 | 5 | # REPL 6 | 7 | For ad hoc introspection, let Doorstop build your tree of documents in your preferred Python [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) or notebook session: 8 | 9 | ```python 10 | >>> import doorstop 11 | >>> tree = doorstop.build() 12 | >>> tree 13 | 14 | >>> len(tree.documents) 15 | 4 16 | >>> document = tree.find_document('REQ') 17 | >>> document 18 | Document('/Users/Browning/Documents/doorstop/reqs') 19 | >>> sum(1 for item in document if item.active) 20 | 18 21 | ``` 22 | 23 | # Generic Scripting 24 | 25 | For reusable workflows, create a Python script that acts on your tree of documents: 26 | 27 | ```python 28 | #!/usr/bin/env python 29 | 30 | import doorstop 31 | 32 | tree = doorstop.build() 33 | document = tree.find_document('REQ') 34 | count = sum(1 for item in document if item.active) 35 | 36 | print(f"{count} active items in {document}") 37 | ``` 38 | 39 | # Validation Hooks 40 | 41 | To extend the default set of [validations](../cli/validation.md) that can be performed, Doorstop provides a "hook" mechanism to simplify scripts that need to operate on multiple documents or items. 42 | 43 | For this use case, create a script to call in place of the default command-line interface: 44 | 45 | ```python 46 | #!/usr/bin/env python 47 | 48 | import sys 49 | from doorstop import build, DoorstopInfo, DoorstopWarning, DoorstopError 50 | 51 | 52 | def main(): 53 | tree = build() 54 | success = tree.validate(document_hook=check_document, item_hook=check_item) 55 | sys.exit(0 if success else 1) 56 | 57 | 58 | def check_document(document, tree): 59 | if sum(1 for i in document if i.normative) < 10: 60 | yield DoorstopInfo("fewer than 10 normative items") 61 | 62 | 63 | def check_item(item, document, tree): 64 | if not item.get('type'): 65 | yield DoorstopWarning("no type specified") 66 | if item.derived and not item.get('rationale'): 67 | yield DoorstopError("derived but no rationale") 68 | 69 | 70 | if __name__ == '__main__': 71 | main() 72 | ``` 73 | 74 | Both `document_hook` and `item_hook` are optional, but if provided these callbacks will be passed each corresponding instance. Each callback should yield instances of Doorstop's exception classes based on severity of the issue. 75 | 76 | ## Validation Hook per folder 77 | 78 | Doorstop also has an extension which allows creating an item validation per folder, allowing the document to have different validations for each document section. 79 | To enable this mechanism you must insert into your `.doorstop.yml` file the following lines: 80 | 81 | ```yaml 82 | extensions: 83 | item_validator: .req_sha_item_validator.py # a python file path relative to .doorstop.yml 84 | ``` 85 | 86 | or 87 | 88 | ```yaml 89 | extensions: 90 | item_validator: ../validators/my_complex_validator.py # a python file path relative to .doorstop.yml 91 | ``` 92 | 93 | The referenced file must have a function called `item_validator` with a single parameter `item`. 94 | 95 | Example: 96 | 97 | ```python 98 | 99 | 100 | def item_validator(item): 101 | if getattr(item, "references") == None: 102 | return [] # early return 103 | for ref in item.references: 104 | if ref['sha'] != item._hash_reference(ref['path']): 105 | yield DoorstopError("Hash has changed and it was not reviewed properly") 106 | 107 | ``` 108 | 109 | Although it is not required, it is recommended to yield a Doorstop type such as, 110 | `DoorstopInfo`, `DoorstopError`, or `DoorstopWarning`. 111 | -------------------------------------------------------------------------------- /docs/assets/logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/docs/assets/logo.xcf -------------------------------------------------------------------------------- /docs/cli/interchange.md: -------------------------------------------------------------------------------- 1 | # Exporting Requirements 2 | 3 | Documents can be exported for editing or to exchange with other systems: 4 | 5 | ```sh 6 | $ doorstop export TST 7 | TST001: 8 | active: true 9 | dervied: false 10 | level: 1 11 | links: 12 | - REQ001 13 | normative: true 14 | ref: '' 15 | text: | 16 | Verify the foobar will foo and bar. 17 | ``` 18 | 19 | Or a file can be created using one of the supported extensions: 20 | 21 | ```sh 22 | $ doorstop export TST path/to/tst.csv 23 | exporting TST to path/to/tst.csv... 24 | exported: path/to/tst.csv 25 | ``` 26 | 27 | Supported formats: 28 | 29 | - YAML: `.yml` 30 | - Comma-Separated Values: `.csv` 31 | - Tab-Separated Values: `.tsv` 32 | - Microsoft Office Excel: `.xlsx` 33 | 34 | # Importing Requirements 35 | 36 | Items can be created/updated from the export formats: 37 | 38 | ```sh 39 | $ doorstop import path/to/tst.csv TST 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/cli/reordering.md: -------------------------------------------------------------------------------- 1 | # Documents Headings 2 | 3 | The items in a document are arranged according to their level attribute, with levels ending in .0 4 | creating headings and the subsequent items forming sub headings. 5 | 6 | A document can contain an arbitraty number of headings and sub headings. 7 | 8 | A normative heading item will display the item's uid as the heading text. A non normative heading 9 | displays the first line of the item text, with any subsequent text displayed in the body of the heading. 10 | 11 | 12 | # Automatic item reordering 13 | 14 | Items in a document can be automatically reordered to remove gaps or duplicate entries in item headings. 15 | 16 | ```sh 17 | $ doorstop reorder --auto REQ 18 | building tree... 19 | reordering document REQ... 20 | reordered document: REQ 21 | ``` 22 | 23 | # Manual item reordering 24 | 25 | Manual reordering creates an index.yml file in the document directory which describes the desired outline 26 | of the document. The index.yml is edited, changing the indentation and order of the items to update the 27 | levels of each item. 28 | 29 | ```sh 30 | $ doorstop reorder --tool vi REQ 31 | building tree... 32 | reorder from 'reqs/index.yml'? [y/n] y 33 | reordering document REQ... 34 | reordered document: REQ 35 | ``` 36 | 37 | ## Adding a new item 38 | 39 | An item can be added by adding a new line to the index.yml containing an unknown UID, e.g. new. 40 | A comment following the new item ID can be used to set the item text. 41 | If the line after a new item is further indented the item is treated as a heading and marked 42 | as non normative. 43 | 44 | ```yaml 45 | ############################################################################### 46 | # THIS TEMPORARY FILE WILL BE DELETED AFTER DOCUMENT REORDERING 47 | # MANUALLY INDENT, DEDENT, & MOVE ITEMS TO THEIR DESIRED LEVEL 48 | # A NEW ITEM WILL BE ADDED FOR ANY UNKNOWN IDS, i.e. - new: 49 | # THE COMMENT WILL BE USED AS THE ITEM TEXT FOR NEW ITEMS 50 | # CHANGES WILL BE REFLECTED IN THE ITEM FILES AFTER CONFIRMATION 51 | ############################################################################### 52 | 53 | initial: 1.0 54 | outline: 55 | - REQ018: # Overview 56 | - REQ019: # Doorstop is a requirements management tool that leverage... 57 | - NEW: # The text of a new item 58 | - NEW: # A new heading 59 | - NEW: # The text of a new ite, 60 | ``` 61 | 62 | ## Deleting an item 63 | 64 | Deleting a line from index.yml will result in the item being deleted. 65 | -------------------------------------------------------------------------------- /docs/cli/validation.md: -------------------------------------------------------------------------------- 1 | # Integrity Checks 2 | 3 | To check a document hierarchy for consistency, run the main command: 4 | 5 | ```sh 6 | $ doorstop 7 | building tree... 8 | loading documents... 9 | validating items... 10 | 11 | REQ 12 | │ 13 | ├── TUT 14 | │ │ 15 | │ └── HLT 16 | │ 17 | └── LLT 18 | ``` 19 | 20 | By default, the validation run displays messages with a `WARNING` and `ERROR` level. 21 | In case verbose output is enabled, also messages with an `INFO` level 22 | are shown. 23 | 24 | `INFO` level messages are generated under the following conditions: 25 | 26 | * Skipped levels within the items of a document. 27 | * No initial review done for an item. 28 | * The prefix of an UID is not equal to the document prefix. 29 | * The prefix of a link UID is not equal to the parent document prefix. 30 | 31 | `WARNING` level messages are generated under the following conditions: 32 | 33 | * A document contains no items. 34 | * Duplicated levels within the items of a document. 35 | * An item has an empty text attribute. 36 | * An item has unreviewed changes. 37 | * An item's child link is an inactive item. 38 | * An item is linked to a non-normative item. 39 | * An item is linked to itself. 40 | * An item has a suspect linked to an item those fingerprint is not equal to the 41 | one recorded in the link. 42 | * An item in a document with child documents has no links from an item in one of its child documents. 43 | * A normative, non-derived item in a child document has no links. 44 | * A non-normative items has links. 45 | * There is a cycle of item links. 46 | 47 | `ERROR` level messages are generated under the following conditions: 48 | 49 | * An item's parent link is an inactive item. 50 | * An item's link is an invalid or unknown UID. 51 | * An external reference cannot be found. 52 | 53 | ## Links 54 | 55 | To confirm that every item in a document links to its parents: 56 | 57 | ```sh 58 | $ doorstop --strict-child-check 59 | building tree... 60 | loading documents... 61 | validating items... 62 | WARNING: REQ: REQ001: no links from document: TUT 63 | WARNING: REQ: REQ016: no links from document: LLT 64 | WARNING: REQ: REQ017: no links from document: LLT 65 | WARNING: REQ: REQ008: no links from document: TUT 66 | WARNING: REQ: REQ009: no links from document: TUT 67 | WARNING: REQ: REQ014: no links from document: TUT 68 | WARNING: REQ: REQ015: no links from document: TUT 69 | 70 | REQ 71 | │ 72 | ├── TUT 73 | │ │ 74 | │ └── HLT 75 | │ 76 | └── LLT 77 | ``` 78 | 79 | ## Clear Suspect Links 80 | 81 | Each link consists of the parent item UID and the 82 | [fingerprint](../reference/item.md#reviewed) of the parent item. When the 83 | fingerprint of a parent item changes, the link is reported as suspect during 84 | validation. 85 | 86 | ```sh 87 | doorstop 88 | building tree... 89 | loading documents... 90 | validating items... 91 | WARNING: LLT: LLT005: suspect link: REQ001 92 | 93 | REQ 94 | │ 95 | ├── TUT 96 | │ │ 97 | │ └── HLT 98 | │ 99 | └── LLT 100 | ``` 101 | 102 | You can clear suspect links with the `doorstop clear` command. 103 | 104 | ```sh 105 | $ doorstop clear LLT005 106 | building tree... 107 | clearing item LLT005's suspect links... 108 | ``` 109 | 110 | Optionally, you can clear only suspect links to specific parent items. 111 | 112 | ```sh 113 | $ doorstop clear LLT005 REQ002 REQ003 114 | building tree... 115 | clearing item LLT005's suspect links to REQ002, REQ003... 116 | ``` 117 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Advanced Setup 2 | 3 | Running `doorstop` in Docker: [tlwt/doorstop-docker](https://github.com/tlwt/doorstop-docker) 4 | 5 | # Sample Projects 6 | 7 | [(add your open source project here)](https://github.com/doorstop-dev/doorstop/edit/develop/docs/examples.md) 8 | 9 | - **[ros-safety/requirements-playground](https://github.com/ros-safety/requirements-playground)**: This is a small repo for demonstrating how Doorstop can be used to manage requirements in a C++ project. 10 | - **[urbasus/literate-programming-in-doorstop](https://github.com/urbasus/literate-programming-in-doorstop)**: Proof-of-concept for combining Doorstop and Literate Programming. 11 | 12 | # 3rd-Party Clients 13 | 14 | - **[doorframe](https://doorframe.io/)**: This is (closed source) GitHub App that uses Doorstop as backend. 15 | - **[sevendays/doorhole](https://github.com/sevendays/doorhole)**: This is standalone GUI for Doorstop. 16 | - **[ownbee/doorstop-edit](https://github.com/ownbee/doorstop-edit)**: Cross-platform Doorstop GUI editor. 17 | -------------------------------------------------------------------------------- /docs/generate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from doorstop.core import publisher, builder 4 | from doorstop.cli import utilities 5 | 6 | 7 | def on_pre_build(config): 8 | cwd = os.getcwd() 9 | path = os.path.abspath(os.path.join(cwd, "docs/gen")) 10 | tree = builder.build(cwd=cwd) 11 | 12 | published_path = publisher.publish(tree, path, ".md", index=True) 13 | 14 | if published_path: 15 | utilities.show("published: {}".format(published_path)) 16 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 |

Installation

2 | 3 | Doorstop requires [Python](https://www.python.org/) and [Git](https://git-scm.com/) or another version control system. 4 | 5 | Once Python is installed on your platform, install Doorstop using pip. 6 | 7 | ```sh 8 | $ pip install doorstop 9 | ``` 10 | 11 | !!! note "Installing Pre-releases" 12 | By default, pip only installs stable [releases of Doorstop from PyPi](https://pypi.org/project/doorstop/#history). 13 | 14 | To tell pip to install a pre-release version, [use the `--pre` option](https://pip.pypa.io/en/stable/cli/pip_install/#pre-release-versions): 15 | 16 | ``` 17 | $ pip install --pre doorstop 18 | ``` 19 | 20 | Alternatively, add it to your [Poetry](https://python-poetry.org/) project: 21 | 22 | ```sh 23 | $ poetry add doorstop 24 | ``` 25 | 26 | After installation, Doorstop is available on the command-line: 27 | 28 | ```sh 29 | $ doorstop --help 30 | ``` 31 | 32 | And the package is available under the name 'doorstop': 33 | 34 | ```sh 35 | $ python 36 | >>> import doorstop 37 | >>> doorstop.__version__ 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/getting-started/quickstart.md: -------------------------------------------------------------------------------- 1 |

Quickstart

2 | 3 | Help with Doorstop: 4 | 5 | - `doorstop --help` 6 | - `doorstop --help` 7 | 8 | Set up a repository for controlling documents: 9 | 10 | - `mkdir /path/to/docs` 11 | - `cd /path/to/docs` 12 | - `git init` 13 | 14 | Create a root parent requirements document: 15 | 16 | - `doorstop create REQ ./reqs/req` 17 | 18 | Add items to a document, will be automatically numbered: 19 | 20 | - `doorstop add REQ` 21 | 22 | Create a child document (low level requirements) to link to the parent: 23 | 24 | - `doorstop create LLR ./reqs/llr --parent REQ` 25 | - `doorstop add LLR` 26 | 27 | Link low level items to requirements (separators like '-' are optional and ignored): 28 | 29 | - `doorstop link LLR001 REQ001` 30 | 31 | Check integrity of the document tree and validate links: 32 | 33 | - `doorstop` 34 | 35 | Mark an unreviewed item, document, or all documents as reviewed: 36 | 37 | - `doorstop review REQ-001 # Marks item REQ-001 as reviewed` 38 | - `doorstop review REQ # Marks all items in document REQ as reviewed` 39 | - `doorstop review all # Marks all documents as reviewed` 40 | 41 | Mark suspect links in an item, document, or all documents, as cleared: 42 | 43 | - `doorstop clear LLR-001 # Marks all links originating in item LLR-001 as cleared` 44 | - `doorstop clear LLR-001 REQ # Marks links in item LLR-001 to document REQ as cleared` 45 | - `doorstop clear LLR REQ # Marks all links from LLR that target REQ as cleared` 46 | - `doorstop clear LLR all # Marks all links originating in document LLR as cleared` 47 | - `doorstop clear all # Marks all links in all documents as cleared` 48 | 49 | Create an HTML document in `publish`: 50 | 51 | - `doorstop publish all ./publish` 52 | 53 | View in the graphical user interface (GUI): 54 | 55 | - `doorstop-gui` 56 | 57 | Browse the doc tree on a local web server: 58 | 59 | - `doorstop-server` 60 | - Point browser to: http://127.0.0.1:7867/ 61 | - Ctrl+C to quit the server. 62 | 63 | Round-trip to Excel: 64 | 65 | - `doorstop export -x REQ REQ.xslx` 66 | - Make edits in Excel. Do not edit UIDs. Leave UID cells blank for new items. 67 | - `doorstop import REQ.xslx REQ` 68 | - `rm REQ.xlsx` 69 | -------------------------------------------------------------------------------- /docs/getting-started/setup.md: -------------------------------------------------------------------------------- 1 | ## Editor 2 | 3 | Doorstop will open files using the editor specified by the `$EDITOR` environment variable. If that is unset, it will attempt to open files in the default editor for each file type. 4 | 5 | ## Git 6 | 7 | **Linux / macOS** 8 | 9 | No additional configuration should be necessary. 10 | 11 | **Windows** 12 | 13 | Windows deals with line-endings differently to most systems, favoring `CRLF` (`\r\n`) over the more traditional `LF` (`\n`). 14 | The `YAML` files saved and revision-controlled by Doorstop have `LF` 15 | line-endings, which can cause the following warnings: 16 | 17 | ``` 18 | (doorstop) C:\temp\doorstop>doorstop reorder --auto TS 19 | building tree... 20 | reordering document TS... 21 | warning: LF will be replaced by CRLF in tests/sys/TS003.yml. 22 | The file will have its original line endings in your working directory. 23 | warning: LF will be replaced by CRLF in tests/sys/TS001.yml. 24 | The file will have its original line endings in your working directory. 25 | warning: LF will be replaced by CRLF in tests/sys/TS002.yml. 26 | The file will have its original line endings in your working directory. 27 | warning: LF will be replaced by CRLF in tests/sys/TS004.yml. 28 | The file will have its original line endings in your working directory. 29 | reordered document: TS 30 | ``` 31 | 32 | These warnings come from Git as a sub-process of the main Doorstop processes, 33 | so the solution is to add the following to your `.gitattributes` file: 34 | 35 | ``` 36 | *.yml text eol=lf 37 | ``` 38 | 39 | From [Git's documentation](https://git-scm.com/docs/gitattributes): 40 | 41 | > This setting forces Git to normalize line endings [for \*.yml files] to LF on checkin and prevents conversion to CRLF when the file is checked out. 42 | -------------------------------------------------------------------------------- /docs/gui/desktop-gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/docs/gui/desktop-gui.png -------------------------------------------------------------------------------- /docs/gui/overview.md: -------------------------------------------------------------------------------- 1 | # Desktop Client 2 | 3 | An **experimental** desktop graphical interface (GUI) is available as a way to perform basic editing tasks. To launch the program: 4 | 5 | ```sh 6 | $ doorstop-gui 7 | ``` 8 | 9 | A window similar to the following should appear: 10 | 11 | ![Desktop GUI](desktop-gui.png) 12 | 13 | If you're interested in helping finish this UI, please check out the [relevant open issues](https://github.com/doorstop-dev/doorstop/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+gui) on GitHub. 14 | 15 | NOTE: You may need to run `brew install python-tk` first on macOS. 16 | -------------------------------------------------------------------------------- /docs/images/logo-black-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/docs/images/logo-black-white.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /docs/reference/document.md: -------------------------------------------------------------------------------- 1 |

Document Reference

2 | 3 | Doorstop documents are folders, with metadata stored in a configuration YAML 4 | file `.doorstop.yml` directly below the folder. 5 | 6 | # Standard Document Attributes 7 | 8 | Upon [creation](../cli/creation.md), `.doorstop.yml` contains default settings. 9 | 10 | ```sh 11 | $ doorstop create REQ ./reqs 12 | created document: REQ (@/reqs) 13 | ``` 14 | 15 | Then open `REQ/.doorstop.yml` in any text editor: 16 | 17 | ```yaml 18 | settings: 19 | digits: 3 20 | prefix: REQ 21 | itemformat: yaml 22 | sep: '' 23 | ``` 24 | 25 | ## `settings` 26 | 27 | Parameters that affect the entire document fall under here. 28 | 29 | ### `digits` 30 | 31 | Defines the number of digits in an item UID. The default value is 3. Optionally, 32 | you can set it through the `-d` command line option of the `doorstop create` 33 | command. It is a mandatory and read-only document setting. 34 | 35 | ### `parent` 36 | 37 | Defines the parent document prefix. You set it through the `-p` command line 38 | option of the `doorstop create` command. It is an optional and read-only 39 | document setting. 40 | 41 | ### `prefix` 42 | 43 | Defines the document prefix. You set it through the prefix of the 44 | `doorstop create` command. It is a mandatory and read-only document setting. 45 | 46 | ### `sep` 47 | 48 | Defines the separator between the document prefix and the number in an item UID. 49 | The default value is the empty string. You have to set it manually before an 50 | item is created. Afterwards, it should be considered as read-only. This 51 | document setting is mandatory. 52 | 53 | ### `itemformat` 54 | 55 | Requirement items can be stored in different file formats. The two types 56 | currently supported are `yaml` and `markdown`. See the [item](item.md) 57 | documentation for more details. The default format is `yaml`. 58 | 59 | # Extended Document Attributes 60 | 61 | In addition to the standard attributes, Doorstop will allow any number of custom 62 | extended attributes (key-value pairs) in the YAML file. The extended attributes 63 | will not be part of a published document, but they can be queried by a 3rd party 64 | application through the REST interface or the Python API. 65 | 66 | ## `attributes` 67 | 68 | Extended document attributes fall under here. 69 | 70 | ### `defaults` 71 | 72 | Defines the [defaults for extended 73 | attributes](item.md#defaults-for-extended-attributes). This is an optional 74 | document configuration option. 75 | 76 | ### `reviewed` 77 | 78 | Defines which [extended attributes contribute to the item 79 | fingerprint](item.md#extended-reviewed-attributes). This is an optional document 80 | configuration option. 81 | 82 | In the document configuration files, you can include other YAML files through a 83 | value tagged with `!include`. The path to the included file is always relative 84 | to the directory of the file with the include tag. Absolute paths are not 85 | supported. Please have a look at this example: 86 | 87 | In `.doorstop.yml`: 88 | 89 | ```yaml 90 | settings: 91 | digits: 3 92 | prefix: REQ 93 | sep: '' 94 | attributes: 95 | defaults: 96 | text: !include path/to/file.yml 97 | ``` 98 | 99 | In `path/to/file.yml`: 100 | 101 | ```yaml 102 | | 103 | Some template text, which may 104 | have several 105 | lines. 106 | ``` -------------------------------------------------------------------------------- /docs/reference/tree.md: -------------------------------------------------------------------------------- 1 |

Tree Reference

2 | 3 | A Doorstop project structure follows a hierarchal [tree 4 | structure](https://en.wikipedia.org/wiki/Tree_structure). 5 | 6 | The root of the tree can exist anywhere in your version control working copy. 7 | Consider the sample project structure below. (Browning, p. 191) 8 | 9 | ``` 10 | req/.doorstop.yml 11 | SRD001.yml 12 | SRD001.YML 13 | tests/.doorstop.yml 14 | HLT001.yml 15 | HLT002.yml 16 | src/doc/.doorstop.yml 17 | SDD001.yml 18 | SSD002.yml 19 | main.c 20 | test/doc/.doorstop.yml 21 | LLT001.yml 22 | LLT002.yml 23 | test_main.c 24 | ``` 25 | 26 | In Doorstop each [document](document.md) is a folder with a `.doorstop.yml` 27 | configuration file directly inside it. 28 | 29 | In this sample structure, there are four documents: 30 | 31 | - SRD = Software Requirements Document 32 | - HLT = High Level Test document 33 | - SDD = Software Design Document 34 | - LLT = Low Level Tests document 35 | 36 | A few [item](item.md) files are listed in each document folder that Doorstop has 37 | numbered sequentially. 38 | 39 | # References 40 | 41 | * Browning, Jace, and Robert Adams. 2014. "Doorstop: Text-Based Requirements 42 | Management Using Version Control." Journal of Software Engineering and 43 | Applications 07 (03): 187–94. . 44 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.5.3 2 | Pygments==2.17.2 3 | -------------------------------------------------------------------------------- /docs/web.md: -------------------------------------------------------------------------------- 1 | ## Bottle 2 | 3 | Doorstop can be run as a standalone web server by running 4 | `doorstop-server`. 5 | 6 | It will use the current working directory as the 7 | document source by default. 8 | 9 | ## WSGI 10 | 11 | Doorstop can also be used as a WSGI application by Apache or other web 12 | servers. To configure this, copy `bin/example-adapter.wsgi` from this 13 | repository to an appropriate place in your web data directory, such as 14 | `/var/www/doorstop/adapter.wsgi`. Edit that file to give it the 15 | correct path to your doorstop installation. Now alter your apache 16 | configuration and add something similar to this: 17 | 18 | WSGIDaemonProcess doorstop user=www-data group=www-data processes=1 threads=5 19 | 20 | WSGIScriptAlias /doorstop /var/www/doorstop/adapter.wsgi 21 | 22 | SetEnv DOORSTOP_PROJECT_DIR /path/to/your/document 23 | SetEnv DOORSTOP_BASE_URL /doorstop 24 | WSGIProcessGroup doorstop 25 | WSGIApplicationGroup %{GLOBAL} 26 | Require all granted 27 | 28 | 29 | Change `path/to/your/document` to the path to the Doorstop data you 30 | wish to display. 31 | -------------------------------------------------------------------------------- /doorstop/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Package for doorstop.""" 4 | 5 | from importlib.metadata import PackageNotFoundError, version 6 | 7 | from doorstop.common import DoorstopError, DoorstopInfo, DoorstopWarning 8 | from doorstop.core import ( 9 | Document, 10 | Item, 11 | Tree, 12 | build, 13 | builder, 14 | editor, 15 | exporter, 16 | find_document, 17 | find_item, 18 | importer, 19 | publisher, 20 | ) 21 | 22 | __project__ = "Doorstop" 23 | 24 | try: 25 | __version__ = version(__project__) 26 | except PackageNotFoundError: 27 | __version__ = "(local)" 28 | 29 | CLI = "doorstop" 30 | GUI = "doorstop-gui" 31 | SERVER = "doorstop-server" 32 | VERSION = "{0} v{1}".format(__project__, __version__) 33 | DESCRIPTION = "Requirements management using version control." 34 | -------------------------------------------------------------------------------- /doorstop/cli/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Command-line interface for Doorstop.""" 4 | -------------------------------------------------------------------------------- /doorstop/cli/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Package for the doorstop.cli tests.""" 4 | 5 | import os 6 | import unittest 7 | 8 | from doorstop import settings 9 | from doorstop.cli.main import main 10 | 11 | ROOT = os.path.join(os.path.dirname(__file__), "..", "..", "..") 12 | REQS = os.path.join(ROOT, "reqs") 13 | TUTORIAL = os.path.join(REQS, "tutorial") 14 | FILES = os.path.join(os.path.dirname(__file__), "files") 15 | 16 | ENV = "TEST_INTEGRATION" # environment variable to enable integration tests 17 | REASON = "'{0}' variable not set".format(ENV) 18 | 19 | 20 | class SettingsTestCase(unittest.TestCase): 21 | """Base test case class that backs up settings.""" 22 | 23 | def setUp(self): 24 | self.backup = ( 25 | settings.REFORMAT, 26 | settings.CHECK_REF, 27 | settings.CHECK_CHILD_LINKS, 28 | settings.REORDER, 29 | settings.CHECK_LEVELS, 30 | settings.PUBLISH_CHILD_LINKS, 31 | settings.CHECK_SUSPECT_LINKS, 32 | settings.CHECK_REVIEW_STATUS, 33 | settings.PUBLISH_BODY_LEVELS, 34 | settings.CACHE_DOCUMENTS, 35 | settings.CACHE_ITEMS, 36 | settings.CACHE_PATHS, 37 | settings.WARN_ALL, 38 | settings.ERROR_ALL, 39 | settings.SERVER_HOST, 40 | settings.SERVER_PORT, 41 | ) 42 | 43 | def tearDown(self): 44 | ( 45 | settings.REFORMAT, 46 | settings.CHECK_REF, 47 | settings.CHECK_CHILD_LINKS, 48 | settings.REORDER, 49 | settings.CHECK_LEVELS, 50 | settings.PUBLISH_CHILD_LINKS, 51 | settings.CHECK_SUSPECT_LINKS, 52 | settings.CHECK_REVIEW_STATUS, 53 | settings.PUBLISH_BODY_LEVELS, 54 | settings.CACHE_DOCUMENTS, 55 | settings.CACHE_ITEMS, 56 | settings.CACHE_PATHS, 57 | settings.WARN_ALL, 58 | settings.ERROR_ALL, 59 | settings.SERVER_HOST, 60 | settings.SERVER_PORT, 61 | ) = self.backup 62 | -------------------------------------------------------------------------------- /doorstop/cli/tests/docs/.doorstop.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | digits: 3 3 | parent: TUT 4 | prefix: HLT 5 | sep: '' 6 | -------------------------------------------------------------------------------- /doorstop/cli/tests/docs/HLT001.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.1 5 | links: 6 | - TUT001: UdIKq3W6wFQh-0c_VH39gf_h_ylXOAhobQ-ULSgiH-Y= 7 | - TUT002: yJH1DjAtUch02wejY1yhbglJskFa9mf2VgusvEF03vU= 8 | - TUT004: I36aGjE75oB5vbPvUlM_gXuyLh_56CfVmYuAcEzsa6U= 9 | - TUT008: z3Xivwhj4Ii69-OvVs7lj1qt4owdivQLvMi7wMGxL6Y= 10 | - TUT017: JvHYGJBTVYk--HFVudf5PMOvTHZuikZf5trmX1TKikE= 11 | - TUT019: Wg_m37OqgVhFw9YvjZkxv9mFdrg04ZIy8u1NLg4Pvh0= 12 | normative: true 13 | ref: test_tutorial_section_1 14 | reviewed: LuZMejVVbNoVI-fZff7mrEqiORKLfSvW0IYGX1CTafA= 15 | text: | 16 | Tutorial Section 1.0: 17 | -------------------------------------------------------------------------------- /doorstop/cli/tests/docs/HLT002.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.2 5 | links: 6 | - TUT009: A_rTJjsI_7B9QRUOWaqSBxquBdz1t9h_HwNn3cQXtQQ= 7 | - TUT010: zgtZEugq0wM6J06MTFhb_kGQtUdus1Qdi_HTc-ZiReI= 8 | normative: true 9 | ref: test_tutorial_section_2 10 | reviewed: zXSVvXJUdWg5mMc78SyoUg7iFagz82_GKz1uDSK0tAA= 11 | text: | 12 | Tutorial Section 2.0: 13 | -------------------------------------------------------------------------------- /doorstop/cli/tests/docs/HLT003.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.3 5 | links: 6 | - TUT012: VkQ9YnIdcmX4-9b9WpMwi2Ghqd4Tg5qMn8pqrGGmXas= 7 | - TUT013: 7lqiX5VtcvclAHuCzl7mqVr17Xu1PEQNuquw80DNDSQ= 8 | - TUT016: ZA0D9hs_RcGQvBR3EjlVdAYRp1lonVCBBq29AVU-vPE= 9 | normative: true 10 | ref: test_tutorial_section_3 11 | reviewed: aME0_SIctRosiaSzFJLZSugR1lZfDkgnSXyPiMUQd-A= 12 | text: | 13 | Tutorial Section 3.0: 14 | -------------------------------------------------------------------------------- /doorstop/cli/tests/docs/HLT004.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.4 5 | links: 6 | - TUT015: ClzVApdcWd-VQIuz7XN_SRyRvlFUVD9A6XONsF9ZNT4= 7 | normative: true 8 | ref: test_tutorial_section_4 9 | reviewed: uqurqigZ0qa92QvdApGpiaV4htLBKHLOZT0xoOOe6-8= 10 | text: | 11 | Tutorial Section 4.0: 12 | -------------------------------------------------------------------------------- /doorstop/cli/tests/docs/HLT005.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: b15hY4KHGmxUEWzRPFSCWv5ToVl0lbsdIgTv8O5xVA0= 9 | text: | 10 | Automated Tests 11 | -------------------------------------------------------------------------------- /doorstop/cli/tests/files/A001.txt: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.0 5 | links: 6 | - B001: xaO5x-ZxaHLy2knS_DD73aXtVRV3hxqCmJInnCh2PZY= 7 | normative: true 8 | ref: '' 9 | reviewed: JEp17AY60onnnNJH7zLSYmM5O2d3heNVB96iEGFkX48= 10 | text: | 11 | A001 12 | -------------------------------------------------------------------------------- /doorstop/cli/tests/files/A002.txt: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.1 5 | links: 6 | - A002: xwOeM6ISel7jEK2qfGlKduG-u5zjhv0uybQrC61UbDQ= 7 | normative: true 8 | ref: '' 9 | reviewed: 4jsCT-Eq-WsIKbMoEV-tzo4AJ_I2_Bs0laFUehmJETg= 10 | text: | 11 | A002 12 | -------------------------------------------------------------------------------- /doorstop/cli/tests/files/B001.txt: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.0 5 | links: 6 | - B002: gDTkEwZSPXcHS-l4h25uOvA8kPtZ6kF0srVr6KbD2Zo= 7 | normative: true 8 | ref: '' 9 | reviewed: GZSV6eLVc5kAfY6bwMSAT1oTHouyBLJHLOUDJu5E-J8= 10 | text: | 11 | B001 12 | -------------------------------------------------------------------------------- /doorstop/cli/tests/files/B002.txt: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.1 5 | links: 6 | - A001: tVIIYUpx8ypqwRceNq8fPnXtMRBkuVJEcJHF-0oVya8= 7 | - A002: xwOeM6ISel7jEK2qfGlKduG-u5zjhv0uybQrC61UbDQ= 8 | normative: true 9 | ref: '' 10 | reviewed: 64iJUOaTCc2Ufto1KD7_B5Rj2mthKNbuElJvZI52n9k= 11 | text: | 12 | B002 13 | -------------------------------------------------------------------------------- /doorstop/cli/tests/files/C001.txt: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.0 5 | links: 6 | - C002: ThisIsTheFingerprintOfASuspectLinkABCDEFGHI= 7 | - C003: ThisIsTheFingerprintOfASuspectLinkABCDEFGHI= 8 | normative: true 9 | ref: '' 10 | reviewed: czv54TPcUklkenTWIS96RvaEKb_vYY9wKgwD5TJcKsA= 11 | text: | 12 | C001 13 | -------------------------------------------------------------------------------- /doorstop/cli/tests/files/C002.txt: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.1 5 | links: [] 6 | normative: true 7 | ref: '' 8 | reviewed: d05UOdrvVooFi8X_3XHRFPrHG3d5DE5yH0sc1dIaZ64= 9 | text: | 10 | C002 11 | -------------------------------------------------------------------------------- /doorstop/cli/tests/files/C003.txt: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.2 5 | links: [] 6 | normative: true 7 | ref: '' 8 | reviewed: QERl1YZnr5RYggS6Pkf5Sibzh9FJ0CfDGGFe6lDpY5U= 9 | text: | 10 | C003 11 | -------------------------------------------------------------------------------- /doorstop/cli/tests/files/exported-map.csv: -------------------------------------------------------------------------------- 1 | uid,mylevel,text,ref,links,active,derived,normative 2 | REQ001,1.2.3,"Hello, world! 3 | ",,"SYS001, 4 | SYS002",True,False,True 5 | REQ003,1.4,"Hello, world! 6 | ",REF123,REQ001,True,False,True 7 | REQ004,1.6,"Hello, world! 8 | ",,,True,False,True 9 | REQ002,2.1,"Hello, world! 10 | ",,,True,False,True 11 | invalid,2.1,"Hello, world! 12 | ",,REQ001,True,False,True 13 | REQ2-001,2.1,"Hello, world! 14 | ",,REQ001,True,False,True 15 | -------------------------------------------------------------------------------- /doorstop/cli/tests/files/exported.csv: -------------------------------------------------------------------------------- 1 | uid,level,text,ref,links,active,derived,normative 2 | REQ001,1.2.3,"Hello, world! 3 | ",,"SYS001, 4 | SYS002",True,False,True 5 | REQ003,1.4,"Hello, world! 6 | ",REF123,REQ001,True,False,True 7 | REQ004,1.6,"Hello, world! 8 | ",,,True,False,True 9 | REQ002,2.1,"Hello, world! 10 | ",,,True,False,True 11 | invalid,2.1,"Hello, world! 12 | ",,REQ001,True,False,True 13 | REQ2-001,2.1,"Hello, world! 14 | ",,REQ001,True,False,True 15 | -------------------------------------------------------------------------------- /doorstop/cli/tests/files/exported.tsv: -------------------------------------------------------------------------------- 1 | uid level text ref links active derived normative 2 | REQ001 1.2.3 "Hello, world! 3 | " "SYS001, 4 | SYS002" True False True 5 | REQ003 1.4 "Hello, world! 6 | " REF123 REQ001 True False True 7 | REQ004 1.6 "Hello, world! 8 | " True False True 9 | REQ002 2.1 "Hello, world! 10 | " True False True 11 | REQ2-001 2.1 "Hello, world! 12 | " REQ001 True False True 13 | -------------------------------------------------------------------------------- /doorstop/cli/tests/files/exported.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/cli/tests/files/exported.xlsx -------------------------------------------------------------------------------- /doorstop/cli/tests/files/template.yml: -------------------------------------------------------------------------------- 1 | text: | 2 | Some text 3 | with more than 4 | one line. 5 | -------------------------------------------------------------------------------- /doorstop/cli/tests/test_main.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Unit tests for the doorstop.cli.main module.""" 4 | 5 | import importlib.util 6 | import sys 7 | from os import sep 8 | from unittest.mock import Mock, patch 9 | 10 | from doorstop import settings 11 | from doorstop.cli import main 12 | from doorstop.cli.tests import SettingsTestCase 13 | 14 | 15 | class TestMain(SettingsTestCase): 16 | """Unit tests for the `main` function.""" 17 | 18 | @patch("doorstop.cli.commands.get") 19 | def test_run(self, mock_get): 20 | """Verify the main CLI function can be called.""" 21 | main.main(args=[]) 22 | mock_get.assert_called_once_with(None) 23 | 24 | @patch("doorstop.cli.commands.run", Mock(side_effect=KeyboardInterrupt)) 25 | def test_interrupt(self): 26 | """Verify the CLI can be interrupted.""" 27 | self.assertRaises(SystemExit, main.main, []) 28 | 29 | @patch("doorstop.cli.commands.run", Mock()) 30 | def test_empty(self): 31 | """Verify 'doorstop' can be run in a working copy with no docs.""" 32 | self.assertIs(None, main.main([])) 33 | self.assertTrue(settings.REFORMAT) 34 | self.assertTrue(settings.CHECK_REF) 35 | self.assertTrue(settings.CHECK_CHILD_LINKS) 36 | self.assertFalse(settings.REORDER) 37 | self.assertTrue(settings.CHECK_LEVELS) 38 | self.assertTrue(settings.CHECK_SUSPECT_LINKS) 39 | self.assertTrue(settings.CHECK_REVIEW_STATUS) 40 | self.assertTrue(settings.CACHE_DOCUMENTS) 41 | self.assertTrue(settings.CACHE_ITEMS) 42 | self.assertTrue(settings.CACHE_PATHS) 43 | self.assertFalse(settings.WARN_ALL) 44 | self.assertFalse(settings.ERROR_ALL) 45 | 46 | @patch("doorstop.cli.commands.run", Mock()) 47 | def test_options(self): 48 | """Verify 'doorstop' can be run with options.""" 49 | self.assertIs( 50 | None, 51 | main.main( 52 | [ 53 | "--no-reformat", 54 | "--no-ref-check", 55 | "--no-child-check", 56 | "--reorder", 57 | "--no-level-check", 58 | "--no-suspect-check", 59 | "--no-review-check", 60 | "--no-cache", 61 | "--warn-all", 62 | "--error-all", 63 | ] 64 | ), 65 | ) 66 | self.assertFalse(settings.REFORMAT) 67 | self.assertFalse(settings.CHECK_REF) 68 | self.assertFalse(settings.CHECK_CHILD_LINKS) 69 | self.assertTrue(settings.REORDER) 70 | self.assertFalse(settings.CHECK_LEVELS) 71 | self.assertFalse(settings.CHECK_SUSPECT_LINKS) 72 | self.assertFalse(settings.CHECK_REVIEW_STATUS) 73 | self.assertFalse(settings.CACHE_DOCUMENTS) 74 | self.assertFalse(settings.CACHE_ITEMS) 75 | self.assertFalse(settings.CACHE_PATHS) 76 | self.assertTrue(settings.WARN_ALL) 77 | self.assertTrue(settings.ERROR_ALL) 78 | 79 | def test_main(self): 80 | testargs = [sep.join(["doorstop", "cli", "main.py"])] 81 | with patch.object(sys, "argv", testargs): 82 | spec = importlib.util.spec_from_file_location("__main__", testargs[0]) 83 | runpy = importlib.util.module_from_spec(spec) 84 | spec.loader.exec_module(runpy) 85 | # Assert 86 | self.assertIsNotNone(runpy) 87 | -------------------------------------------------------------------------------- /doorstop/core/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Core package for Doorstop.""" 4 | 5 | from doorstop.core.builder import build, find_document, find_item 6 | from doorstop.core.document import Document 7 | from doorstop.core.item import Item 8 | from doorstop.core.tree import Tree 9 | -------------------------------------------------------------------------------- /doorstop/core/editor.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Functions to edit documents and items.""" 4 | 5 | import os 6 | import shutil 7 | import subprocess 8 | import sys 9 | import tempfile 10 | import time 11 | 12 | from doorstop import common 13 | from doorstop.common import DoorstopError 14 | 15 | LAUNCH_DELAY = 0.5 # number of seconds to let a program try to launch 16 | 17 | log = common.logger(__name__) 18 | 19 | 20 | def edit(path, tool=None): 21 | """Open a file and wait for the default editor to exit. 22 | 23 | :param path: path of file to open 24 | :param tool: path of alternate editor 25 | 26 | :return: launched process 27 | 28 | """ 29 | process = launch(path, tool=tool) 30 | if process: 31 | try: 32 | process.wait() 33 | except KeyboardInterrupt: 34 | log.debug("user cancelled") 35 | finally: 36 | if process.returncode is None: 37 | process.terminate() 38 | log.warning("force closed editor") 39 | log.debug("process exited: {}".format(process.returncode)) 40 | 41 | 42 | def edit_tmp_content(title=None, original_content=None, tool=None): 43 | """Edit content in a temporary file and return the saved content. 44 | 45 | :param title: text that will appear in the name of the temporary file. 46 | If not given, name is only random characters. 47 | :param original_content: content to insert in the temporary file before 48 | opening it with the editor. If not given, file is empty. 49 | Must be a string object. 50 | :param tool: path of alternate editor 51 | 52 | :return: content of the temporary file after user closes the editor. 53 | 54 | """ 55 | # Create a temporary file to edit the text 56 | tmp_fd, tmp_path = tempfile.mkstemp(prefix="{}_".format(title), text=True) 57 | os.close(tmp_fd) # release the file descriptor because it is not needed 58 | with open(tmp_path, "w") as tmp_f: 59 | tmp_f.write(original_content) 60 | 61 | # Open the editor to edit the temporary file with the original text 62 | edit(tmp_path, tool=tool) 63 | 64 | # Read the edited text and remove the tmp file 65 | with open(tmp_path, "r") as tmp_f: 66 | edited_content = tmp_f.read() 67 | os.remove(tmp_path) 68 | 69 | return edited_content 70 | 71 | 72 | def launch(path, tool=None): 73 | """Open a file using the default editor. 74 | 75 | :param path: path of file to open 76 | :param tool: path of alternate editor 77 | 78 | :raises: :class:`~doorstop.common.DoorstopError` no default editor 79 | or editor unavailable 80 | 81 | :return: launched process if long-running, else None 82 | 83 | """ 84 | # Determine how to launch the editor 85 | if tool: 86 | args = [tool, path] 87 | elif sys.platform.startswith("darwin"): 88 | args = ["open", path] 89 | elif os.name == "nt": 90 | cygstart = shutil.which("cygstart") 91 | if cygstart: 92 | args = [cygstart, path] 93 | else: 94 | args = ["start", path] 95 | elif os.name == "posix": 96 | args = ["xdg-open", path] 97 | else: 98 | raise DoorstopError( 99 | "unknown operating system, unable to determine launch command" 100 | ) 101 | 102 | # Launch the editor 103 | try: 104 | log.info("opening '{}'...".format(path)) 105 | process = _call(args) 106 | except FileNotFoundError: 107 | raise DoorstopError("editor not found: {}".format(args[0])) 108 | 109 | # Wait for the editor to launch 110 | time.sleep(LAUNCH_DELAY) 111 | if process.poll() is None: 112 | log.debug("process is running...") 113 | else: 114 | log.debug("process exited: {}".format(process.returncode)) 115 | if process.returncode != 0: 116 | raise DoorstopError("no default editor for: {}".format(path)) 117 | 118 | # Return the process if it's still running 119 | return process if process.returncode is None else None 120 | 121 | 122 | def _call(args): 123 | """Call a program with arguments and return the process.""" 124 | log.debug("$ {}".format(" ".join(args))) 125 | process = subprocess.Popen(args) 126 | return process 127 | -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/doorstop.css: -------------------------------------------------------------------------------- 1 | /* Doorstop.css file. */ 2 | .caption { 3 | text-align: center; 4 | font-size:15px; 5 | } 6 | 7 | #img {width: 100%} -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/general.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-left: 0px; 3 | padding-top: 0px; 4 | padding-right: 0px; 5 | } 6 | 7 | /* indent everything within a section */ 8 | section > * { 9 | margin-left: 30px; 10 | } 11 | 12 | /* don't indent the section header */ 13 | section > :first-child { 14 | margin-left: 0px; 15 | } 16 | -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/logo-black-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/logo-black-white.png -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Zero.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/html/output/chtml/fonts/woff-v2/MathJax_Zero.woff -------------------------------------------------------------------------------- /doorstop/core/files/templates/latex/doorstop.yml: -------------------------------------------------------------------------------- 1 | # Define all options to class. 2 | documentclass: 3 | - a4paper 4 | - twoside 5 | # Define all 'usepackage' commands. 6 | usepackage: 7 | inputenc: 8 | - utf8 9 | amsmath: 10 | ulem: 11 | longtable: 12 | fancyvrb: 13 | xr-hyper: 14 | hyperref: 15 | # Options are defined as sub-lists. 16 | - unicode 17 | - colorlinks 18 | zref-user: 19 | zref-xr: 20 | # Define custom lines before \begin{document} 21 | before_begin_document: 22 | - \def\docname{Doorstop - \doccategory{}} 23 | # Define custom lines after \begin{document} 24 | after_begin_document: 25 | - \maketitle 26 | - \maketoc 27 | -------------------------------------------------------------------------------- /doorstop/core/files/templates/latex/logo-black-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/files/templates/latex/logo-black-white.png -------------------------------------------------------------------------------- /doorstop/core/publishers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/publishers/__init__.py -------------------------------------------------------------------------------- /doorstop/core/publishers/tests/helpers.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Unit test helper functions to reduce code duplication.""" 4 | 5 | # pylint: disable=unused-argument,protected-access 6 | 7 | import os 8 | 9 | LINES = """ 10 | initial: 1.2.3 11 | outline: 12 | - REQ001: # Lorem ipsum d... 13 | - REQ003: # Unicode: -40° ±1% 14 | - REQ004: # Hello, world! !['.. 15 | - REQ002: # Hello, world! !["... 16 | - REQ2-001: # Hello, world! 17 | """ 18 | YAML_CUSTOM_ATTRIBUTES = """ 19 | settings: 20 | digits: 3 21 | prefix: REQ 22 | sep: '-' 23 | attributes: 24 | defaults: 25 | publish: 26 | - CUSTOM-ATTRIB 27 | - invented-by 28 | """ 29 | HTML_TEMPLATE_WALK = """ 30 | template/ 31 | bootstrap.bundle.min.js 32 | bootstrap.min.css 33 | doorstop.css 34 | general.css 35 | jquery.min.js 36 | logo-black-white.png 37 | tex-mml-chtml.js 38 | output/ 39 | chtml.js 40 | svg.js 41 | chtml/ 42 | fonts/ 43 | tex.js 44 | woff-v2/ 45 | MathJax_AMS-Regular.woff 46 | MathJax_Calligraphic-Bold.woff 47 | MathJax_Calligraphic-Regular.woff 48 | MathJax_Fraktur-Bold.woff 49 | MathJax_Fraktur-Regular.woff 50 | MathJax_Main-Bold.woff 51 | MathJax_Main-Italic.woff 52 | MathJax_Main-Regular.woff 53 | MathJax_Math-BoldItalic.woff 54 | MathJax_Math-Italic.woff 55 | MathJax_Math-Regular.woff 56 | MathJax_SansSerif-Bold.woff 57 | MathJax_SansSerif-Italic.woff 58 | MathJax_SansSerif-Regular.woff 59 | MathJax_Script-Regular.woff 60 | MathJax_Size1-Regular.woff 61 | MathJax_Size2-Regular.woff 62 | MathJax_Size3-Regular.woff 63 | MathJax_Size4-Regular.woff 64 | MathJax_Typewriter-Regular.woff 65 | MathJax_Vector-Bold.woff 66 | MathJax_Vector-Regular.woff 67 | MathJax_Zero.woff 68 | svg/ 69 | fonts/ 70 | tex.js 71 | views/ 72 | base.tpl 73 | document_list.tpl 74 | doorstop.tpl 75 | item_list.tpl 76 | """ 77 | 78 | 79 | def getWalk(walk_path): 80 | # Get the exported tree. 81 | walk = [] 82 | for root, _, files in sorted(os.walk(walk_path)): 83 | level = root.replace(walk_path, "").count(os.sep) 84 | indent = " " * 4 * (level) 85 | walk.append("{}{}/\n".format(indent, os.path.basename(root))) 86 | subindent = " " * 4 * (level + 1) 87 | for f in sorted(files): 88 | walk.append("{}{}\n".format(subindent, f)) 89 | return "".join(line + "" for line in walk) 90 | 91 | 92 | def getLines(gen): 93 | # Get the generated lines. 94 | result = "" 95 | for line in gen: 96 | result = result + line + "\n" 97 | return result 98 | 99 | 100 | def getFileContents(file): 101 | """Return the contents of a file.""" 102 | data = [] 103 | with open(file, "r") as file_stream: 104 | data = file_stream.readlines() 105 | return data 106 | -------------------------------------------------------------------------------- /doorstop/core/publishers/tests/helpers_latex.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Unit test helper functions to reduce code duplication.""" 4 | 5 | # pylint: disable=unused-argument,protected-access 6 | 7 | YAML_LATEX_DOC = """ 8 | settings: 9 | digits: 3 10 | prefix: REQ 11 | sep: '-' 12 | attributes: 13 | defaults: 14 | doc: 15 | name: 'Tutorial' 16 | title: 'Development test document' 17 | ref: 'TUT-DS-22' 18 | by: 'Jng' 19 | major: '1' 20 | minor: 'A' 21 | copyright: 'Whatever Inc.' 22 | publish: 23 | - CUSTOM-ATTRIB 24 | - invented-by 25 | """ 26 | 27 | YAML_LATEX_EMPTY_DOC = """ 28 | settings: 29 | digits: 3 30 | prefix: TST 31 | sep: '-' 32 | attributes: 33 | defaults: 34 | doc: 35 | name: '' 36 | title: '' 37 | ref: '' 38 | by: '' 39 | major: '' 40 | minor: '' 41 | copyright: '' 42 | publish: 43 | - CUSTOM-ATTRIB 44 | """ 45 | 46 | YAML_LATEX_NO_DOC = """ 47 | settings: 48 | digits: 3 49 | prefix: TST 50 | sep: '-' 51 | attributes: 52 | defaults: 53 | publish: 54 | - CUSTOM-ATTRIB 55 | """ 56 | 57 | YAML_LATEX_NO_REF = """ 58 | settings: 59 | digits: 3 60 | prefix: TST 61 | sep: '-' 62 | attributes: 63 | defaults: 64 | doc: 65 | name: 'Tutorial' 66 | title: 'Development test document' 67 | by: 'Jng' 68 | major: '1' 69 | minor: 'A' 70 | copyright: 'Whatever Inc.' 71 | publish: 72 | - CUSTOM-ATTRIB 73 | - invented-by 74 | """ 75 | 76 | YAML_LATEX_ONLY_REF = """ 77 | settings: 78 | digits: 3 79 | prefix: TST 80 | sep: '-' 81 | attributes: 82 | defaults: 83 | doc: 84 | ref: 'TUT-DS-22' 85 | publish: 86 | - CUSTOM-ATTRIB 87 | - invented-by 88 | """ 89 | -------------------------------------------------------------------------------- /doorstop/core/publishers/tests/test_publisher_html_doc.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Unit tests for the doorstop.core.publishers.html module.""" 4 | 5 | # pylint: disable=unused-argument,protected-access 6 | 7 | import os 8 | import stat 9 | import unittest 10 | from secrets import token_hex 11 | from shutil import rmtree 12 | 13 | from doorstop.common import DoorstopError 14 | from doorstop.core import publisher 15 | from doorstop.core.builder import build 16 | from doorstop.core.publishers.tests.helpers import HTML_TEMPLATE_WALK, getWalk 17 | from doorstop.core.tests import ROOT, MockDataMixIn 18 | from doorstop.core.tests.helpers import on_error_with_retry 19 | 20 | 21 | class TestPublisherFullDocument(MockDataMixIn, unittest.TestCase): 22 | """Unit tests for the doorstop.core.publishers.html module by publishing a full document tree.""" 23 | 24 | # pylint: disable=no-value-for-parameter 25 | def setUp(self): 26 | """Setup test folder.""" 27 | # Build a tree. 28 | self.mock_tree = build(cwd=ROOT, root=ROOT, request_next_number=None) 29 | self.hex = token_hex() 30 | self.dirpath = os.path.abspath(os.path.join("mock_%s" % __name__, self.hex)) 31 | os.makedirs(self.dirpath) 32 | self.expected_walk = """{n}/ 33 | index.html 34 | traceability.csv 35 | traceability.html 36 | documents/ 37 | EXT.html 38 | HLT.html 39 | LLT.html 40 | REQ.html 41 | TUT.html 42 | assets/ 43 | logo-black-white.png{w}""".format( 44 | n=self.hex, w=HTML_TEMPLATE_WALK 45 | ) 46 | 47 | @classmethod 48 | def tearDownClass(cls): 49 | """Remove test folder.""" 50 | rmtree("mock_%s" % __name__, onerror=on_error_with_retry) 51 | 52 | def test_publish_html_tree_copies_assets(self): 53 | """Verify that html assets are published when publishing a tree.""" 54 | # Act 55 | path2 = publisher.publish(self.mock_tree, self.dirpath, ext=".html") 56 | # Assert 57 | self.assertIs(self.dirpath, path2) 58 | # Get the exported tree. 59 | walk = getWalk(self.dirpath) 60 | self.assertEqual(self.expected_walk, walk) 61 | 62 | def test_bad_html_template(self): 63 | """Verify a bad HTML template raises an error.""" 64 | # Act 65 | with self.assertRaises(DoorstopError): 66 | publisher.publish(self.mock_tree, ".html", template="DOES_NOT_EXIST") 67 | -------------------------------------------------------------------------------- /doorstop/core/publishers/tests/test_publisher_markdown_doc.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Unit tests for the doorstop.core.publishers.markdown module.""" 4 | 5 | # pylint: disable=unused-argument,protected-access 6 | 7 | import os 8 | import stat 9 | import unittest 10 | from secrets import token_hex 11 | from shutil import rmtree 12 | 13 | from doorstop.core import publisher 14 | from doorstop.core.builder import build 15 | from doorstop.core.publishers.tests.helpers import getWalk 16 | from doorstop.core.tests import ROOT, MockDataMixIn, MockDocument 17 | from doorstop.core.tests.helpers import on_error_with_retry 18 | 19 | 20 | class TestPublisherFullDocument(MockDataMixIn, unittest.TestCase): 21 | """Unit tests for the doorstop.core.publishers.markdown module by publishing a full document tree.""" 22 | 23 | # pylint: disable=no-value-for-parameter 24 | def setUp(self): 25 | """Setup test folder.""" 26 | # Build a tree. 27 | self.mock_tree = build(cwd=ROOT, root=ROOT, request_next_number=None) 28 | self.hex = token_hex() 29 | self.dirpath = os.path.abspath(os.path.join("mock_%s" % __name__, self.hex)) 30 | os.makedirs(self.dirpath) 31 | self.expected_walk = """{n}/ 32 | EXT.md 33 | HLT.md 34 | LLT.md 35 | REQ.md 36 | TUT.md 37 | assets/ 38 | logo-black-white.png 39 | """.format( 40 | n=self.hex 41 | ) 42 | 43 | @classmethod 44 | def tearDownClass(cls): 45 | """Remove test folder.""" 46 | rmtree("mock_%s" % __name__, onerror=on_error_with_retry) 47 | 48 | def test_publish_markdown_tree_copies_assets(self): 49 | """Verify that markdown assets are published when publishing a tree.""" 50 | # Act 51 | path2 = publisher.publish(self.mock_tree, self.dirpath, ext=".md") 52 | # Assert 53 | self.assertIs(self.dirpath, path2) 54 | # Get the exported tree. 55 | walk = getWalk(self.dirpath) 56 | self.assertEqual(self.expected_walk, walk) 57 | 58 | def test_publish_markdown_document_to_path(self): 59 | """Verify that single document export to path works.""" 60 | expected_walk = """{n}/ 61 | REQ.md 62 | """.format( 63 | n=self.hex 64 | ) 65 | dirpath = self.dirpath 66 | doc = MockDocument(dirpath) 67 | # Act 68 | path2 = publisher.publish(doc, dirpath, ".md") 69 | # Assert 70 | self.assertIs(dirpath, path2) 71 | # Get the exported tree. 72 | walk = getWalk(self.dirpath) 73 | self.assertEqual(expected_walk, walk) 74 | -------------------------------------------------------------------------------- /doorstop/core/reference_finder.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Finding external references.""" 4 | 5 | import linecache 6 | import os 7 | import re 8 | 9 | from doorstop import common, settings 10 | from doorstop.common import DoorstopError 11 | 12 | log = common.logger(__name__) 13 | 14 | 15 | class ReferenceFinder: 16 | """Finds files referenced from an Item.""" 17 | 18 | @staticmethod 19 | def find_ref(ref, tree, item_path): 20 | """Get the external file reference and line number. 21 | 22 | :raises: :class:`~doorstop.common.DoorstopError` when no 23 | reference is found 24 | 25 | :return: relative path to file or None (when no reference 26 | set), 27 | line number (when found in file) or None (when found as 28 | filename) or None (when no reference set) 29 | 30 | """ 31 | 32 | # Search for the external reference 33 | log.debug("searching for ref '{}'...".format(ref)) 34 | pattern = r"(\b|\W){}(\b|\W)".format(re.escape(ref)) 35 | log.trace("regex: {}".format(pattern)) # type: ignore 36 | regex = re.compile(pattern) 37 | for path, filename, relpath in tree.vcs.paths: 38 | # Skip the item's file while searching 39 | if path == item_path: 40 | continue 41 | # Check for a matching filename 42 | if filename == ref: 43 | return relpath, None 44 | # Skip extensions that should not be considered text 45 | if os.path.splitext(filename)[-1] in settings.SKIP_EXTS: 46 | continue 47 | # Search for the reference in the file 48 | try: 49 | lines = linecache.getlines(path) 50 | except (SyntaxError, UnicodeDecodeError): 51 | log.trace("unable to read lines from: {}".format(path)) # type: ignore 52 | continue 53 | for lineno, line in enumerate(lines, start=1): 54 | if regex.search(line): 55 | log.debug("found ref: {}".format(relpath)) 56 | return relpath, lineno 57 | 58 | msg = "external reference not found: {}".format(ref) 59 | raise DoorstopError(msg) 60 | 61 | @staticmethod 62 | def find_file_reference(ref_path, root, tree, item_path, keyword=None): 63 | """Find the external file reference. 64 | 65 | :raises: :class:`~doorstop.common.DoorstopError` when no 66 | reference is found 67 | 68 | :return: Tuple (ref_path, line) when reference is found 69 | 70 | """ 71 | 72 | log.debug("searching for ref '{}'...".format(ref_path)) 73 | ref_full_path = os.path.normpath(os.path.join(root, ref_path)) 74 | 75 | for path, _filename, relpath in tree.vcs.paths: 76 | # Skip the item's file while searching 77 | if path == item_path: 78 | continue 79 | if path == ref_full_path: 80 | if keyword is None: 81 | return relpath, None 82 | 83 | # Search for the reference in the file 84 | try: 85 | lines = linecache.getlines(path) 86 | except SyntaxError: 87 | log.trace("unable to read lines from: {}".format(path)) # type: ignore 88 | continue 89 | 90 | log.debug("searching for ref '{}'...".format(keyword)) 91 | pattern = r"(\b|\W){}(\b|\W)".format(re.escape(keyword)) 92 | log.trace("regex: {}".format(pattern)) # type: ignore 93 | regex = re.compile(pattern) 94 | for lineno, line in enumerate(lines, start=1): 95 | if regex.search(line): 96 | log.debug("found ref: {}".format(relpath)) 97 | return relpath, lineno 98 | 99 | msg = "external reference not found: {}".format(ref_path) 100 | raise DoorstopError(msg) 101 | -------------------------------------------------------------------------------- /doorstop/core/tests/docs/.doorstop.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | digits: 3 3 | parent: REQ 4 | prefix: LLT 5 | sep: '' 6 | -------------------------------------------------------------------------------- /doorstop/core/tests/docs/LLT001.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.1 5 | links: 6 | - REQ003: 9TcFUzsQWUHhoh5wsqnhL7VRtSqMaIhrCXg7mfIkxKM= 7 | normative: true 8 | ref: Verify an item can be added to a document. 9 | reviewed: 3cWsswJTpxB9WHng7lXLeM4jQW6zbPTZtt1vL1mZFMI= 10 | text: | 11 | Test adding items: 12 | -------------------------------------------------------------------------------- /doorstop/core/tests/docs/LLT002.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.2 5 | links: 6 | - REQ004: T2tSkn27DO3GXvagwOgNNLvhW4FPNg9gyLfru-l9hWQ= 7 | normative: true 8 | ref: Verify Markdown can be published from a document. 9 | reviewed: ly_FQiijvn6dMCKTGW5gzw3zovTXRmB5g_WFv39j0nA= 10 | text: | 11 | Test publishing Markdown: 12 | -------------------------------------------------------------------------------- /doorstop/core/tests/docs/LLT003.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.3 5 | links: 6 | - REQ007: N4qTPlDi0z6kClsYAWlTsYPYWPylyr5KscMlxyYlzbA= 7 | normative: true 8 | ref: Verify text can be published from a document. 9 | reviewed: nZXA_TD_MfctNFPxpkuwjeucmfGh3588iDeLsaExROA= 10 | text: | 11 | Test publishing text: 12 | -------------------------------------------------------------------------------- /doorstop/core/tests/docs/LLT004.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.4 5 | links: 6 | - REQ008: Y9QwGNJVzJSHbW9sHzBqswqAtF5v8OJup8HEH7E2qHU= 7 | normative: true 8 | ref: Verify the items in a document can be accessed. 9 | reviewed: vD17pIGCoBclRlwG71wnHHt_Ng1jkCo4uq2h6hOYBWw= 10 | text: | 11 | Test getting items from a document: 12 | -------------------------------------------------------------------------------- /doorstop/core/tests/docs/LLT005.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.5 5 | links: 6 | - REQ001: avwblqPimDJ2OgTrRCXxRPN8FQhUBWqPIXm7kSR95C4= 7 | normative: true 8 | ref: Verify an item's reference can also be a filename. 9 | reviewed: G96xqQH4CjV1i3ZwiWtUKbbDisOK28E7ik8aItcy07A= 10 | text: | 11 | Test referencing an external file by name: 12 | -------------------------------------------------------------------------------- /doorstop/core/tests/docs/LLT007.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 2.1 5 | links: 6 | - REQ009: uuTiyRLtSfUnneuE71WqML8D_X8YtiKj02Ehlb8b2bg= 7 | - REQ011: 3N-eJ3o7Va-Vwx8F4VCE1eYw4WCwK1DleYM95RfsaEQ= 8 | - REQ012: aDEVUTCV4yqDY99sfjch7otkgidiyG0We8tGSTWD9Hs= 9 | - REQ013: ch1ilz7OJnQhSuKbpcN81Z0ml2z6lBTQqV6rWanW_ZA= 10 | - REQ014: r_QHE6crBVcD_cEeXXtztbaeU7PoTnvpQ6uSpR3M63w= 11 | - REQ015: DxAA240XYSKeWMmaKCR3OymyabO52_T7dQGRVfF7yKw= 12 | normative: true 13 | ref: '' 14 | reviewed: yFFQ523SPsaP7IgGKaQ1f-guLTZ0-8msYsaqK-zuxAI= 15 | text: | 16 | These checks ensure the version control system (VCS) meets the needs of 17 | requirements management: 18 | 19 | - Verify the VCS includes a 'tag' feature. 20 | - Verify the VCS stores files in a permanent and secure manner. 21 | - Verify the VCS handles change management of files. 22 | - Verify the VCS associates changes to existing developer acccounts. 23 | - Verify the VCS can manage changes to thousands of files. 24 | -------------------------------------------------------------------------------- /doorstop/core/tests/docs/LLT008.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 2.2 5 | links: 6 | - REQ015: DxAA240XYSKeWMmaKCR3OymyabO52_T7dQGRVfF7yKw= 7 | normative: true 8 | ref: '' 9 | reviewed: 5HmM-ABqS824omdludZzfX5gPwIH2gEq4l3EYf9wXkY= 10 | text: | 11 | These checks ensure the Python package is distributed properly: 12 | 13 | - Verify the installation can be performed on a new computer in fewer than 10 14 | seconds. 15 | -------------------------------------------------------------------------------- /doorstop/core/tests/docs/LLT009.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: kSJLszT3Mu5O-OdoMVqiniUEc1w4voBPiZZJ4bmfBBs= 9 | text: | 10 | Automated Tests 11 | -------------------------------------------------------------------------------- /doorstop/core/tests/docs/LLT010.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 2.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: vAqWkWng0CMNOxNTMWZGUC-l3YfBOIpl1dIxJdUh3wQ= 9 | text: | 10 | Inspection Tests 11 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/.doorstop.skip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/tests/files/.doorstop.skip -------------------------------------------------------------------------------- /doorstop/core/tests/files/.doorstop.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | digits: 2 3 | parent: SYS 4 | prefix: REQ 5 | sep: '' 6 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/.venv/doorstop/reqs/.doorstop.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | digits: 2 3 | prefix: REQ 4 | sep: '' 5 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/REQ001.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.2.3 5 | links: 6 | - SYS001: null 7 | - SYS002: abc123 8 | normative: true 9 | ref: '' 10 | reviewed: null 11 | text: | 12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 13 | tempor incididunt ut labore et dolore magna aliqua. 14 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 15 | aliquip ex ea commodo consequat. 16 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 17 | eu fugiat nulla pariatur. 18 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 19 | deserunt mollit anim id est laborum. 20 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/REQ002.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Plantuml 5 | level: 2.1 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: 1PvBLmy0xmdK_zLKrLu1au0wlIw_zsD6A8Oc5F4zWxU= 10 | text: | 11 | Hello, world! 12 | 13 | ```plantuml format="svg_inline" alt="Use Cases of Doorstop" title="Use Cases of Doorstop" 14 | @startuml 15 | Author --> (Create Document) 16 | Author --> (Create Item) 17 | Author --> (Link Item to Document) 18 | Author --> (Link Item to other Item) 19 | Author --> (Edit Item) 20 | Author --> (Review Item) 21 | Author -> (Delete Item) 22 | Author -> (Delete Document) 23 | (Export) <- (Author) 24 | (Import) <- (Author) 25 | Reviewer --> (Review Item) 26 | System --> (Suspect Changes) 27 | System --> (Integrity) 28 | @enduml 29 | ``` 30 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/REQ003.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.4 5 | links: 6 | - REQ001: null 7 | normative: true 8 | ref: REF123 9 | reviewed: null 10 | text: | 11 | Unicode: -40° ±1% 12 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/REQ006.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.5 5 | links: 6 | - REQ001: 35ed54323e3054c33ae5545fffdbbbf5 7 | normative: true 8 | ref: '' 9 | references: 10 | - keyword: REF123 11 | path: external/text.txt 12 | type: file 13 | - path: external/text2.txt 14 | type: file 15 | reviewed: c442316131ca0225595ae257f3b4583d 16 | text: | 17 | Hello, world! 18 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/REQ2-001.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 2.1 5 | links: 6 | - REQ001: null 7 | normative: true 8 | ref: '' 9 | reviewed: null 10 | text: | 11 | Hello, world! 12 | 13 | Test Math Expressions in Latex Style: 14 | 15 | Inline Style 1: $a \ne 0$ 16 | Inline Style 2: \(ax^2 + bx + c = 0\) 17 | Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ 18 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/a/b/template.yml: -------------------------------------------------------------------------------- 1 | text: 'Some text' 2 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/a/template.yml: -------------------------------------------------------------------------------- 1 | defaults: !include b/template.yml 2 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/child/.doorstop.skip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/tests/files/child/.doorstop.skip -------------------------------------------------------------------------------- /doorstop/core/tests/files/child/.doorstop.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | digits: 3 3 | prefix: TST 4 | parent: REQ 5 | sep: '' 6 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/child/TST001.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1 5 | links: 6 | - REQ002: null 7 | - REQ2-001: null 8 | normative: true 9 | ref: '' 10 | reviewed: null 11 | text: | 12 | Hello, world! 13 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/child/TST002.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 2 5 | links: 6 | - REQ002: null 7 | normative: true 8 | ref: '' 9 | reviewed: null 10 | text: | 11 | Hello, world! 12 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/exported-huge.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/tests/files/exported-huge.xlsx -------------------------------------------------------------------------------- /doorstop/core/tests/files/exported-modified.csv: -------------------------------------------------------------------------------- 1 | id,level,text,ref,links,active,derived,normative,additional 2 | REQ0555,1.2.3,"Hello, world! 3 | ",,"SYS001, 4 | SYS002",True,"False",FALSE, 5 | REQ003,1.4,"Hello, world! 6 | ",REF123,REQ001,false,"FALSE",True,"Some ""quoted"" text 'here'." 7 | REQ004,1.6,"Hello, world! 8 | ",,,FAlSe,tRue,"TRUE", 9 | REQ002,2.1,"Hello, world! 10 | ",,,"true",False,"True","" 11 | REQ2-001,2.1,"Hello, world! 12 | ",,REQ001,True,"false",True,"" 13 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/exported.csv: -------------------------------------------------------------------------------- 1 | uid,level,text,ref,references,links,active,derived,header,normative,reviewed 2 | REQ001,1.2.3,"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 3 | tempor incididunt ut labore et dolore magna aliqua. 4 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 5 | aliquip ex ea commodo consequat. 6 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 7 | eu fugiat nulla pariatur. 8 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 9 | deserunt mollit anim id est laborum.",,,"SYS001 10 | SYS002:abc123",True,False,,True, 11 | REQ003,1.4,Unicode: -40° ±1%,REF123,,REQ001,True,False,,True, 12 | REQ006,1.5,"Hello, world!",,"type:file,path:external/text.txt,keyword:REF123 13 | type:file,path:external/text2.txt",REQ001:35ed54323e3054c33ae5545fffdbbbf5,True,False,,True,c442316131ca0225595ae257f3b4583d 14 | REQ004,1.6,"Hello, world!",,,,True,False,,True, 15 | REQ002,2.1,"Hello, world! 16 | 17 | ```plantuml format=""svg_inline"" alt=""Use Cases of Doorstop"" title=""Use Cases of Doorstop"" 18 | @startuml 19 | Author --> (Create Document) 20 | Author --> (Create Item) 21 | Author --> (Link Item to Document) 22 | Author --> (Link Item to other Item) 23 | Author --> (Edit Item) 24 | Author --> (Review Item) 25 | Author -> (Delete Item) 26 | Author -> (Delete Document) 27 | (Export) <- (Author) 28 | (Import) <- (Author) 29 | Reviewer --> (Review Item) 30 | System --> (Suspect Changes) 31 | System --> (Integrity) 32 | @enduml 33 | ```",,,,True,False,Plantuml,True,1PvBLmy0xmdK_zLKrLu1au0wlIw_zsD6A8Oc5F4zWxU= 34 | REQ2-001,2.1,"Hello, world! 35 | 36 | Test Math Expressions in Latex Style: 37 | 38 | Inline Style 1: $a \ne 0$ 39 | Inline Style 2: \(ax^2 + bx + c = 0\) 40 | Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$",,,REQ001,True,False,,True, 41 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/exported.tsv: -------------------------------------------------------------------------------- 1 | uid level text ref references links active derived header normative reviewed 2 | REQ001 1.2.3 "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 3 | tempor incididunt ut labore et dolore magna aliqua. 4 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 5 | aliquip ex ea commodo consequat. 6 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 7 | eu fugiat nulla pariatur. 8 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 9 | deserunt mollit anim id est laborum." "SYS001 10 | SYS002:abc123" True False True 11 | REQ003 1.4 Unicode: -40° ±1% REF123 REQ001 True False True 12 | REQ006 1.5 Hello, world! "type:file,path:external/text.txt,keyword:REF123 13 | type:file,path:external/text2.txt" REQ001:35ed54323e3054c33ae5545fffdbbbf5 True False True c442316131ca0225595ae257f3b4583d 14 | REQ004 1.6 Hello, world! True False True 15 | REQ002 2.1 "Hello, world! 16 | 17 | ```plantuml format=""svg_inline"" alt=""Use Cases of Doorstop"" title=""Use Cases of Doorstop"" 18 | @startuml 19 | Author --> (Create Document) 20 | Author --> (Create Item) 21 | Author --> (Link Item to Document) 22 | Author --> (Link Item to other Item) 23 | Author --> (Edit Item) 24 | Author --> (Review Item) 25 | Author -> (Delete Item) 26 | Author -> (Delete Document) 27 | (Export) <- (Author) 28 | (Import) <- (Author) 29 | Reviewer --> (Review Item) 30 | System --> (Suspect Changes) 31 | System --> (Integrity) 32 | @enduml 33 | ```" True False Plantuml True 1PvBLmy0xmdK_zLKrLu1au0wlIw_zsD6A8Oc5F4zWxU= 34 | REQ2-001 2.1 "Hello, world! 35 | 36 | Test Math Expressions in Latex Style: 37 | 38 | Inline Style 1: $a \ne 0$ 39 | Inline Style 2: \(ax^2 + bx + c = 0\) 40 | Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$" REQ001 True False True 41 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/exported.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/tests/files/exported.xlsx -------------------------------------------------------------------------------- /doorstop/core/tests/files/exported.yml: -------------------------------------------------------------------------------- 1 | REQ001: 2 | active: true 3 | derived: false 4 | header: '' 5 | level: 1.2.3 6 | links: 7 | - SYS001: null 8 | - SYS002: abc123 9 | normative: true 10 | ref: '' 11 | reviewed: null 12 | text: | 13 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 14 | tempor incididunt ut labore et dolore magna aliqua. 15 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 16 | aliquip ex ea commodo consequat. 17 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 18 | eu fugiat nulla pariatur. 19 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 20 | deserunt mollit anim id est laborum. 21 | 22 | REQ003: 23 | active: true 24 | derived: false 25 | header: '' 26 | level: 1.4 27 | links: 28 | - REQ001: null 29 | normative: true 30 | ref: REF123 31 | reviewed: null 32 | text: | 33 | Unicode: -40° ±1% 34 | 35 | REQ006: 36 | active: true 37 | derived: false 38 | header: '' 39 | level: 1.5 40 | links: 41 | - REQ001: 35ed54323e3054c33ae5545fffdbbbf5 42 | normative: true 43 | ref: '' 44 | references: 45 | - keyword: REF123 46 | path: external/text.txt 47 | type: file 48 | - path: external/text2.txt 49 | type: file 50 | reviewed: c442316131ca0225595ae257f3b4583d 51 | text: | 52 | Hello, world! 53 | 54 | REQ004: 55 | active: true 56 | derived: false 57 | header: '' 58 | level: 1.6 59 | links: [] 60 | normative: true 61 | ref: '' 62 | reviewed: null 63 | text: | 64 | Hello, world! 65 | 66 | REQ002: 67 | active: true 68 | derived: false 69 | header: | 70 | Plantuml 71 | level: 2.1 72 | links: [] 73 | normative: true 74 | ref: '' 75 | reviewed: 1PvBLmy0xmdK_zLKrLu1au0wlIw_zsD6A8Oc5F4zWxU= 76 | text: | 77 | Hello, world! 78 | 79 | ```plantuml format="svg_inline" alt="Use Cases of Doorstop" title="Use Cases of Doorstop" 80 | @startuml 81 | Author --> (Create Document) 82 | Author --> (Create Item) 83 | Author --> (Link Item to Document) 84 | Author --> (Link Item to other Item) 85 | Author --> (Edit Item) 86 | Author --> (Review Item) 87 | Author -> (Delete Item) 88 | Author -> (Delete Document) 89 | (Export) <- (Author) 90 | (Import) <- (Author) 91 | Reviewer --> (Review Item) 92 | System --> (Suspect Changes) 93 | System --> (Integrity) 94 | @enduml 95 | ``` 96 | 97 | REQ2-001: 98 | active: true 99 | derived: false 100 | header: '' 101 | level: 2.1 102 | links: 103 | - REQ001: null 104 | normative: true 105 | ref: '' 106 | reviewed: null 107 | text: | 108 | Hello, world! 109 | 110 | Test Math Expressions in Latex Style: 111 | 112 | Inline Style 1: $a \ne 0$ 113 | Inline Style 2: \(ax^2 + bx + c = 0\) 114 | Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ 115 | 116 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/external/text.txt: -------------------------------------------------------------------------------- 1 | REF122 2 | 3 | REF123 4 | 5 | REF124 6 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/external/text2.txt: -------------------------------------------------------------------------------- 1 | CONTENT HERE IS NOT RELEVANT 2 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/formula.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/tests/files/formula.xlsx -------------------------------------------------------------------------------- /doorstop/core/tests/files/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 126 | 127 | 128 | 129 |

Published Documents:

130 |

131 |

136 |

137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/index.md: -------------------------------------------------------------------------------- 1 | # Requirements index 2 | 3 | ### Published Documents: 4 | * [index2](index2.md) 5 | * [published](published.md) 6 | * [published2](published2.md) -------------------------------------------------------------------------------- /doorstop/core/tests/files/index2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 126 | 127 | 128 | 129 |

Tree Structure:

130 |
(mock tree structure)
131 | 132 |
133 | 134 |

Published Documents:

135 |

136 |

141 |

142 | 143 |
144 | 145 |

Item Traceability:

146 |

147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 |
SYS HLR LLR HLT LLT
KNOWN-001
UNKNOWN-002
182 |

183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/index2.md: -------------------------------------------------------------------------------- 1 | # Requirements index 2 | * [SYS](SYS.md) - The SYS document for Doorstop 3 | * [HLR](HLR.md) - The HLR document for Doorstop 4 | * [LLR](LLR.md) - The LLR document for Doorstop 5 | * [HLT](HLT.md) - The HLT document for Doorstop 6 | * [LLT](LLT.md) - The LLT document for Doorstop 7 | 8 | ### Published Documents: 9 | * [index2](index2.md) 10 | * [published](published.md) 11 | * [published2](published2.md) -------------------------------------------------------------------------------- /doorstop/core/tests/files/new/.doorstop.skip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/tests/files/new/.doorstop.skip -------------------------------------------------------------------------------- /doorstop/core/tests/files/new/.doorstop.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | digits: 5 3 | parent: REQ 4 | prefix: NEW 5 | sep: '' 6 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/parent/.doorstop.skip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/tests/files/parent/.doorstop.skip -------------------------------------------------------------------------------- /doorstop/core/tests/files/parent/.doorstop.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | digits: 3 3 | prefix: SYS 4 | sep: '' 5 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/parent/SYS001.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.0 5 | links: [] 6 | normative: true 7 | ref: '' 8 | reviewed: null 9 | text: | 10 | Hello, world! 11 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/parent/SYS002.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 2.0 5 | links: [] 6 | normative: true 7 | ref: '' 8 | reviewed: null 9 | text: | 10 | Hello, world! 11 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/published.md: -------------------------------------------------------------------------------- 1 | ### 1.2.3 REQ001 {#REQ001} 2 | 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 4 | tempor incididunt ut labore et dolore magna aliqua. 5 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 6 | aliquip ex ea commodo consequat. 7 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 8 | eu fugiat nulla pariatur. 9 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 10 | deserunt mollit anim id est laborum. 11 | 12 | *Parent links: SYS001, SYS002* 13 | 14 | ## 1.4 REQ003 {#REQ003} 15 | 16 | Unicode: -40° ±1% 17 | 18 | > `external/text.txt` (line 3) 19 | 20 | *Parent links: REQ001* 21 | 22 | ## 1.5 REQ006 {#REQ006} 23 | 24 | Hello, world! 25 | 26 | > `external/text.txt` (line 3) 27 | > `external/text2.txt` 28 | 29 | *Parent links: REQ001* 30 | 31 | ## 1.6 REQ004 {#REQ004} 32 | 33 | Hello, world! 34 | 35 | ## 2.1 Plantuml _REQ002_ {#REQ002} 36 | 37 | Hello, world! 38 | 39 | ```plantuml format="svg_inline" alt="Use Cases of Doorstop" title="Use Cases of Doorstop" 40 | @startuml 41 | Author --> (Create Document) 42 | Author --> (Create Item) 43 | Author --> (Link Item to Document) 44 | Author --> (Link Item to other Item) 45 | Author --> (Edit Item) 46 | Author --> (Review Item) 47 | Author -> (Delete Item) 48 | Author -> (Delete Document) 49 | (Export) <- (Author) 50 | (Import) <- (Author) 51 | Reviewer --> (Review Item) 52 | System --> (Suspect Changes) 53 | System --> (Integrity) 54 | @enduml 55 | ``` 56 | 57 | *Child links: TST001, TST002* 58 | 59 | ## 2.1 REQ2-001 {#REQ2-001} 60 | 61 | Hello, world! 62 | 63 | Test Math Expressions in Latex Style: 64 | 65 | Inline Style 1: $a \ne 0$ 66 | Inline Style 2: \(ax^2 + bx + c = 0\) 67 | Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ 68 | 69 | *Parent links: REQ001* 70 | 71 | *Child links: TST001* 72 | 73 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/published.txt: -------------------------------------------------------------------------------- 1 | 1.2.3 REQ001 2 | 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 4 | tempor incididunt ut labore et dolore magna aliqua. 5 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi 6 | ut 7 | aliquip ex ea commodo consequat. 8 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum 9 | dolore 10 | eu fugiat nulla pariatur. 11 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui 12 | officia 13 | deserunt mollit anim id est laborum. 14 | 15 | Parent links: SYS001, SYS002 16 | 17 | 1.4 REQ003 18 | 19 | Unicode: -40° ±1% 20 | 21 | Reference: external/text.txt (line 3) 22 | 23 | Parent links: REQ001 24 | 25 | 1.5 REQ006 26 | 27 | Hello, world! 28 | 29 | Reference: external/text.txt (line 3), external/text2.txt 30 | 31 | Parent links: REQ001 32 | 33 | 1.6 REQ004 34 | 35 | Hello, world! 36 | 37 | 2.1 REQ002 Plantuml 38 | 39 | Hello, world! 40 | 41 | ```plantuml format="svg_inline" alt="Use Cases of Doorstop" title="Use 42 | Cases of Doorstop" 43 | @startuml 44 | Author --> (Create Document) 45 | Author --> (Create Item) 46 | Author --> (Link Item to Document) 47 | Author --> (Link Item to other Item) 48 | Author --> (Edit Item) 49 | Author --> (Review Item) 50 | Author -> (Delete Item) 51 | Author -> (Delete Document) 52 | (Export) <- (Author) 53 | (Import) <- (Author) 54 | Reviewer --> (Review Item) 55 | System --> (Suspect Changes) 56 | System --> (Integrity) 57 | @enduml 58 | ``` 59 | 60 | Child links: TST001, TST002 61 | 62 | 2.1 REQ2-001 63 | 64 | Hello, world! 65 | 66 | Test Math Expressions in Latex Style: 67 | 68 | Inline Style 1: $a \ne 0$ 69 | Inline Style 2: \(ax^2 + bx + c = 0\) 70 | Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ 71 | 72 | Parent links: REQ001 73 | 74 | Child links: TST001 75 | 76 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/published2.md: -------------------------------------------------------------------------------- 1 | ### 1.2.3 REQ001 {#REQ001} 2 | 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 4 | tempor incididunt ut labore et dolore magna aliqua. 5 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 6 | aliquip ex ea commodo consequat. 7 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 8 | eu fugiat nulla pariatur. 9 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 10 | deserunt mollit anim id est laborum. 11 | 12 | *Links: SYS001, SYS002* 13 | 14 | ## 1.4 REQ003 {#REQ003} 15 | 16 | Unicode: -40° ±1% 17 | 18 | > `external/text.txt` (line 3) 19 | 20 | *Links: REQ001* 21 | 22 | ## 1.5 REQ006 {#REQ006} 23 | 24 | Hello, world! 25 | 26 | > `external/text.txt` (line 3) 27 | > `external/text2.txt` 28 | 29 | *Links: REQ001* 30 | 31 | ## 1.6 REQ004 {#REQ004} 32 | 33 | Hello, world! 34 | 35 | ## 2.1 Plantuml _REQ002_ {#REQ002} 36 | 37 | Hello, world! 38 | 39 | ```plantuml format="svg_inline" alt="Use Cases of Doorstop" title="Use Cases of Doorstop" 40 | @startuml 41 | Author --> (Create Document) 42 | Author --> (Create Item) 43 | Author --> (Link Item to Document) 44 | Author --> (Link Item to other Item) 45 | Author --> (Edit Item) 46 | Author --> (Review Item) 47 | Author -> (Delete Item) 48 | Author -> (Delete Document) 49 | (Export) <- (Author) 50 | (Import) <- (Author) 51 | Reviewer --> (Review Item) 52 | System --> (Suspect Changes) 53 | System --> (Integrity) 54 | @enduml 55 | ``` 56 | 57 | ## 2.1 REQ2-001 {#REQ2-001} 58 | 59 | Hello, world! 60 | 61 | Test Math Expressions in Latex Style: 62 | 63 | Inline Style 1: $a \ne 0$ 64 | Inline Style 2: \(ax^2 + bx + c = 0\) 65 | Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ 66 | 67 | *Links: REQ001* 68 | 69 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/published2.txt: -------------------------------------------------------------------------------- 1 | 1.2.3 REQ001 2 | 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 4 | tempor incididunt ut labore et dolore magna aliqua. 5 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi 6 | ut 7 | aliquip ex ea commodo consequat. 8 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum 9 | dolore 10 | eu fugiat nulla pariatur. 11 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui 12 | officia 13 | deserunt mollit anim id est laborum. 14 | 15 | Links: SYS001, SYS002 16 | 17 | 1.4 REQ003 18 | 19 | Unicode: -40° ±1% 20 | 21 | Reference: external/text.txt (line 3) 22 | 23 | Links: REQ001 24 | 25 | 1.5 REQ006 26 | 27 | Hello, world! 28 | 29 | Reference: external/text.txt (line 3), external/text2.txt 30 | 31 | Links: REQ001 32 | 33 | 1.6 REQ004 34 | 35 | Hello, world! 36 | 37 | 2.1 REQ002 Plantuml 38 | 39 | Hello, world! 40 | 41 | ```plantuml format="svg_inline" alt="Use Cases of Doorstop" title="Use 42 | Cases of Doorstop" 43 | @startuml 44 | Author --> (Create Document) 45 | Author --> (Create Item) 46 | Author --> (Link Item to Document) 47 | Author --> (Link Item to other Item) 48 | Author --> (Edit Item) 49 | Author --> (Review Item) 50 | Author -> (Delete Item) 51 | Author -> (Delete Document) 52 | (Export) <- (Author) 53 | (Import) <- (Author) 54 | Reviewer --> (Review Item) 55 | System --> (Suspect Changes) 56 | System --> (Integrity) 57 | @enduml 58 | ``` 59 | 60 | 2.1 REQ2-001 61 | 62 | Hello, world! 63 | 64 | Test Math Expressions in Latex Style: 65 | 66 | Inline Style 1: $a \ne 0$ 67 | Inline Style 2: \(ax^2 + bx + c = 0\) 68 | Multiline: $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ 69 | 70 | Links: REQ001 71 | 72 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/skipped.txt: -------------------------------------------------------------------------------- 1 | This file should not be considered a requirement because it is not *.yml. -------------------------------------------------------------------------------- /doorstop/core/tests/files/subfolder/REQ004.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.6 5 | links: [] 6 | normative: true 7 | ref: '' 8 | reviewed: null 9 | text: | 10 | Hello, world! 11 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/subfolder/REQ005.yml: -------------------------------------------------------------------------------- 1 | active: false 2 | derived: false 3 | level: 9.9 4 | links: [] 5 | normative: true 6 | ref: '' 7 | reviewed: null 8 | text: | 9 | An inactive requirement. 10 | -------------------------------------------------------------------------------- /doorstop/core/tests/files/testmatrix.csv: -------------------------------------------------------------------------------- 1 | SYS,HLR,LLR,HLT,LLT 2 | ,KNOWN-001,,, 3 | ,,,UNKNOWN-002, 4 | ,,,, 5 | -------------------------------------------------------------------------------- /doorstop/core/tests/files_md/.doorstop.skip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorstop-dev/doorstop/96ebab9689bb906a88190305aad82f715f71af49/doorstop/core/tests/files_md/.doorstop.skip -------------------------------------------------------------------------------- /doorstop/core/tests/files_md/.doorstop.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | digits: 2 3 | parent: SYSMD 4 | itemformat: markdown 5 | prefix: REQMD 6 | sep: '' 7 | -------------------------------------------------------------------------------- /doorstop/core/tests/files_md/REQ001.md: -------------------------------------------------------------------------------- 1 | --- 2 | active: true 3 | derived: false 4 | header: '' 5 | level: 1.2.3 6 | links: 7 | - SYS001: null 8 | - SYS002: abc123 9 | normative: true 10 | ref: '' 11 | reviewed: null 12 | --- 13 | 14 | # Markdown Header 15 | 16 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 17 | tempor incididunt ut labore et dolore magna aliqua. 18 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 19 | aliquip ex ea commodo consequat. 20 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 21 | eu fugiat nulla pariatur. 22 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 23 | deserunt mollit anim id est laborum. 24 | -------------------------------------------------------------------------------- /doorstop/core/tests/files_md/REQ002.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.5 5 | links: 6 | - REQ001: 35ed54323e3054c33ae5545fffdbbbf5 7 | normative: true 8 | ref: '' 9 | references: 10 | - keyword: REF123 11 | path: external/text.txt 12 | type: file 13 | - path: external/text2.txt 14 | type: file 15 | reviewed: c442316131ca0225595ae257f3b4583d 16 | text: | 17 | Hello, world! 18 | -------------------------------------------------------------------------------- /doorstop/core/tests/helpers.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | # pylint: disable=unused-argument,protected-access 3 | 4 | """Unit test helper functions to reduce code duplication.""" 5 | from logging import NullHandler 6 | from os import chmod 7 | from shutil import copytree 8 | from stat import S_IWRITE 9 | from time import sleep 10 | from typing import List 11 | 12 | from doorstop.core.builder import build 13 | from doorstop.core.tests import ROOT 14 | 15 | 16 | class ListLogHandler(NullHandler): 17 | """Create a log handler for asserting log calls.""" 18 | 19 | def __init__(self, log): 20 | super().__init__() 21 | self.records: List[str] = [] 22 | self.log = log 23 | 24 | def __enter__(self): 25 | self.log.addHandler(self) 26 | return self 27 | 28 | def __exit__(self, kind, value, traceback): 29 | self.log.removeHandler(self) 30 | 31 | def handle(self, record): 32 | self.records.append(str(record.msg)) 33 | 34 | 35 | def build_expensive_tree(obj): 36 | # Build a tree. 37 | copytree(ROOT, obj.datapath) 38 | obj.mock_tree = build(cwd=obj.datapath, root=obj.datapath, request_next_number=None) 39 | 40 | 41 | def on_error_with_retry(func, path, exc_info, retries=60): 42 | """Define a separate function to handle errors for rmtree. 43 | 44 | This callback function is used to retry rmtree operations 45 | that fail for up to 60 seconds. This is necessary because 46 | Windows does not always release file handles immediately, 47 | and the rmtree operation can fail. 48 | """ 49 | if retries > 0: 50 | # Change the file permissions 51 | chmod(path, S_IWRITE) 52 | # Sleep for 1 seconds to wait for the race condition to resolve 53 | sleep(1) 54 | try: 55 | # Try the operation again 56 | func(path) 57 | # pylint: disable=broad-except 58 | except Exception as e: 59 | # If an error occurs, recurse with one fewer retry available 60 | print(f"Retry {5 - retries} failed for {path}, error: {e}. Retrying...") 61 | on_error_with_retry(func, path, exc_info, retries - 1) 62 | else: 63 | # If no retries are left, raise an exception or handle the final failure case 64 | print(f"Failed to remove {path} after multiple retries.") 65 | -------------------------------------------------------------------------------- /doorstop/core/tests/test_common.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Unit tests for the doorstop.common module""" 4 | 5 | import unittest 6 | 7 | from doorstop import common 8 | 9 | MARKDOW_DEFAULT = """ 10 | --- 11 | active: true 12 | derived: false 13 | header: '' 14 | level: 1.0 15 | links: [] 16 | normative: true 17 | ref: '' 18 | reviewed: null 19 | --- 20 | 21 | # Markdown Header 22 | 23 | text text text 24 | 25 | * one 26 | * two 27 | 28 | text text text 29 | """.lstrip() 30 | 31 | MARKDOW_INVALID = """ 32 | --- 33 | invalid 34 | something 35 | --- 36 | 37 | # Markdown Header 38 | 39 | text text text 40 | """.lstrip() 41 | 42 | MARKDOW_MISSING_YAML = """ 43 | # Markdown Header 44 | 45 | text text text 46 | """.lstrip() 47 | 48 | 49 | class TestMarkdownIO(unittest.TestCase): 50 | """Unit tests for the Markdown IO operations.""" 51 | 52 | def test_load_markdown_without_header(self): 53 | # Arrange 54 | keys = ["text"] 55 | text = MARKDOW_DEFAULT 56 | path = "path/to/req.md" 57 | 58 | # Act 59 | data = common.load_markdown(text, path, keys) 60 | 61 | # Assert 62 | 63 | self.assertIn("header", data) 64 | self.assertEqual(data["header"], "") 65 | self.assertEqual( 66 | data["text"], 67 | """# Markdown Header 68 | 69 | text text text 70 | 71 | * one 72 | * two 73 | 74 | text text text""", 75 | ) 76 | 77 | def test_load_markdown_with_header(self): 78 | # Arrange 79 | keys = ["text", "header"] 80 | text = MARKDOW_DEFAULT 81 | path = "path/to/req.md" 82 | 83 | # Act 84 | data = common.load_markdown(text, path, keys) 85 | 86 | # Assert 87 | 88 | self.assertIn("header", data) 89 | self.assertEqual(data["header"], "Markdown Header") 90 | self.assertEqual( 91 | data["text"], 92 | """text text text 93 | 94 | * one 95 | * two 96 | 97 | text text text""", 98 | ) 99 | 100 | def test_load_markdown_invalid_yaml(self): 101 | # Arrange 102 | keys = [] 103 | text = MARKDOW_INVALID 104 | path = "path/to/req.md" 105 | 106 | # Act 107 | data = common.load_markdown(text, path, keys) 108 | 109 | # Assert 110 | self.assertEqual(data, {}) 111 | 112 | def test_load_markdown_missing_yaml(self): 113 | # Arrange 114 | keys = [] 115 | text = MARKDOW_MISSING_YAML 116 | path = "path/to/req.md" 117 | 118 | # Act 119 | data = common.load_markdown(text, path, keys) 120 | 121 | # Assert 122 | self.assertEqual(data, {}) 123 | 124 | def test_dump_markdown(self): 125 | # Arrange 126 | data = { 127 | "key1": "text1", 128 | "key2": "text2", 129 | "list": ["a", "b", "c"], 130 | } 131 | textattr = { 132 | "text": """ 133 | text text text 134 | """.lstrip(), 135 | "header": "header", 136 | } 137 | # Act 138 | text = common.dump_markdown(data, textattr) 139 | 140 | # Assert 141 | self.assertEqual( 142 | text, 143 | """ 144 | --- 145 | key1: text1 146 | key2: text2 147 | list: 148 | - a 149 | - b 150 | - c 151 | --- 152 | 153 | # header 154 | 155 | text text text""".lstrip(), 156 | ) 157 | -------------------------------------------------------------------------------- /doorstop/core/tests/test_fixtures/001-item-references-utf8-keyword/REQ-UTF8.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.5 5 | links: 6 | - REQ001: 35ed54323e3054c33ae5545fffdbbbf5 7 | normative: true 8 | ref: '' 9 | references: 10 | - keyword: français 11 | path: NOT-RELEVANT-FOR-THIS-TEST.txt 12 | type: file 13 | reviewed: c442316131ca0225595ae257f3b4583d 14 | text: | 15 | Hello, world! 16 | -------------------------------------------------------------------------------- /doorstop/core/tests/test_fixtures/001-item-references-utf8-keyword/files/text-utf8.txt: -------------------------------------------------------------------------------- 1 | français 2 | -------------------------------------------------------------------------------- /doorstop/core/tests/test_fixtures/002-utf8-characters/REQ-CYRILLIC.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | UTF-8 5 | level: 1.7.0 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: null 10 | text: | 11 | UTF-8 is supported. Verified by this file. 12 | 13 | première is first 14 | première is slightly different 15 | Кириллица is Cyrillic 16 | 𐐀 am Deseret 17 | -------------------------------------------------------------------------------- /doorstop/core/tests/test_fixtures/002-utf8-characters/REQ-CYRILLIC_crlf.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | UTF-8 5 | level: 1.7.0 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: null 10 | text: | 11 | UTF-8 is supported. Verified by this file. 12 | 13 | première is first 14 | première is slightly different 15 | Кириллица is Cyrillic 16 | 𐐀 am Deseret 17 | -------------------------------------------------------------------------------- /doorstop/core/tests/test_fixtures/002-utf8-characters/REQ-MIT.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | MIT licence 5 | level: 1.7.1 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: null 10 | text: | 11 | Just verify that the MIT licence is typeset correctly. 12 | 13 | Copyright © 2022 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /doorstop/core/tests/test_item_extensions.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | # pylint: disable=C0302 3 | 4 | """Unit tests for the doorstop.core.item with extensions enabled.""" 5 | 6 | import os 7 | import unittest 8 | from types import ModuleType 9 | from unittest.mock import patch 10 | 11 | from doorstop.common import import_path_as_module 12 | from doorstop.core.tests import TESTS_ROOT, MockItem, MockSimpleDocumentExtensions 13 | 14 | 15 | class TestItem(unittest.TestCase): 16 | """Unit tests for the Item class.""" 17 | 18 | # pylint: disable=protected-access,no-value-for-parameter 19 | 20 | def setUp(self): 21 | path = os.path.join("path", "to", "RQ001.yml") 22 | self.item = MockItem(MockSimpleDocumentExtensions(), path) 23 | 24 | @patch("doorstop.settings.CACHE_PATHS", False) 25 | def test_load_custom_validator_per_folder(self): 26 | """Load a valid custom validator per folder.""" 27 | path = os.path.join("path", "to", "RQ001.yml") 28 | self.item = MockItem( 29 | MockSimpleDocumentExtensions( 30 | item_validator=f"{TESTS_ROOT}/validators/validator_dummy.py" 31 | ), 32 | path, 33 | ) 34 | document = self.item.document 35 | validator = import_path_as_module(document.extensions["item_validator"]) 36 | 37 | self.assertEqual(isinstance(validator, ModuleType), True) 38 | 39 | @patch("doorstop.settings.CACHE_PATHS", False) 40 | def test_load_custom_validator_per_folder_and_fails(self): 41 | """Load a invalid custom validator per folder and fails with FileNotFoundError.""" 42 | path = os.path.join("path", "to", "RQ001.yml") 43 | self.item = MockItem( 44 | MockSimpleDocumentExtensions( 45 | item_validator=f"{TESTS_ROOT}/files/validator_dummy2.py" 46 | ), 47 | path, 48 | ) 49 | document = self.item.document 50 | try: 51 | validator = import_path_as_module(document.extensions["item_validator"]) 52 | except FileNotFoundError: 53 | validator = FileNotFoundError 54 | 55 | self.assertEqual(FileNotFoundError, validator) 56 | 57 | @patch("doorstop.settings.CACHE_PATHS", False) 58 | def test_find_references_and_get_sha(self): 59 | """Verify an item's references can be found.""" 60 | path = os.path.join("path", "to", "RQ001.yml") 61 | self.item = MockItem(MockSimpleDocumentExtensions(item_sha_required=True), path) 62 | self.item.root = TESTS_ROOT 63 | 64 | self.item.references = [ 65 | {"path": "files/REQ001.yml", "type": "file"}, 66 | {"path": "files/REQ002.yml", "type": "file"}, 67 | ] 68 | # Generate sha through review 69 | self.item.review() 70 | refs = self.item.references 71 | 72 | self.assertIn("sha", refs[0]) 73 | self.assertIn("sha", refs[1]) 74 | 75 | @patch("doorstop.settings.CACHE_PATHS", False) 76 | def test_no_sha_ref(self): 77 | """Verify sha is not obtained if extension is not enabled.""" 78 | path = os.path.join("path", "to", "RQ001.yml") 79 | self.item = MockItem( 80 | MockSimpleDocumentExtensions(), 81 | path, 82 | ) 83 | 84 | self.item.root = TESTS_ROOT 85 | 86 | self.item.references = [ 87 | {"path": "files/REQ001.yml", "type": "file"}, 88 | {"path": "files/REQ002.yml", "type": "file"}, 89 | ] 90 | # without item_sha_required, sha must return None 91 | self.item.review() 92 | refs = self.item.references 93 | sha = self.item._hash_reference(refs[0]["path"]) 94 | self.assertNotIn("sha", refs[0].keys()) 95 | self.assertIsNone(sha) 96 | -------------------------------------------------------------------------------- /doorstop/core/tests/test_yaml_validator.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | # pylint: disable=C0302 3 | 4 | """Unit tests for the doorstop.core.item module.""" 5 | 6 | import unittest 7 | from typing import Any, Dict 8 | 9 | from doorstop.core.yaml_validator import YamlValidator 10 | 11 | 12 | class TestItem(unittest.TestCase): 13 | """Unit tests for the Item class.""" 14 | 15 | # pylint: disable=protected-access,no-value-for-parameter 16 | 17 | def setUp(self): 18 | self.validator = YamlValidator() 19 | 20 | def test_empty_item(self): 21 | item_dict: Dict[str, Any] = {} 22 | 23 | valid = self.validator.validate_item_yaml(item_dict) 24 | 25 | self.assertTrue(valid) 26 | 27 | def test_references_are_none(self): 28 | item_dict: Dict[str, Any] = {"references": None} 29 | 30 | with self.assertRaises(AttributeError) as context: 31 | self.validator.validate_item_yaml(item_dict) 32 | 33 | self.assertTrue( 34 | "'references' must be an array with at least one reference element" 35 | in str(context.exception) 36 | ) 37 | 38 | def test_references_missing_type(self): 39 | item_dict: Dict[str, Any] = {"references": [{}]} 40 | 41 | with self.assertRaises(AttributeError) as context: 42 | self.validator.validate_item_yaml(item_dict) 43 | 44 | self.assertEqual( 45 | "'references' member must have a 'type' key", str(context.exception) 46 | ) 47 | 48 | def test_reference_with_a_nonfile_type(self): 49 | item_dict = { 50 | "references": [ 51 | { 52 | "type": "incorrect-type", 53 | "path": "some/path", 54 | "keyword": "some keyword", 55 | } 56 | ] 57 | } 58 | 59 | with self.assertRaises(AttributeError) as context: 60 | self.validator.validate_item_yaml(item_dict) 61 | 62 | self.assertEqual( 63 | "'references' member's 'type' value must be a 'file'", 64 | str(context.exception), 65 | ) 66 | 67 | def test_reference_with_a_string_keyword(self): 68 | item_dict = { 69 | "references": [ 70 | {"type": "file", "path": "some/path", "keyword": "some keyword"} 71 | ] 72 | } 73 | 74 | valid = self.validator.validate_item_yaml(item_dict) 75 | 76 | self.assertTrue(valid) 77 | 78 | def test_reference_with_a_non_string_keyword(self): 79 | item_dict = { 80 | "references": [ 81 | { 82 | "type": "file", 83 | "path": "some/path", 84 | "keyword": ["keyword must be a string"], 85 | } 86 | ] 87 | } 88 | 89 | with self.assertRaises(AttributeError) as context: 90 | self.validator.validate_item_yaml(item_dict) 91 | 92 | self.assertTrue( 93 | "'references' member's 'keyword' must be a string value" 94 | in str(context.exception) 95 | ) 96 | -------------------------------------------------------------------------------- /doorstop/core/tests/validators/validator_dummy.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | from doorstop import DoorstopError, DoorstopInfo, DoorstopWarning 4 | 5 | 6 | def item_validator(item): 7 | if item: 8 | yield DoorstopInfo("Loaded") 9 | -------------------------------------------------------------------------------- /doorstop/core/vcs/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Interfaces to version control systems.""" 4 | 5 | import os 6 | 7 | from doorstop import common 8 | from doorstop.common import DoorstopError 9 | from doorstop.core.vcs import git, mercurial, mockvcs, subversion 10 | 11 | DEFAULT = mockvcs.WorkingCopy 12 | DIRECTORIES = { 13 | git.WorkingCopy.DIRECTORY: git.WorkingCopy, 14 | subversion.WorkingCopy.DIRECTORY: subversion.WorkingCopy, 15 | mercurial.WorkingCopy.DIRECTORY: mercurial.WorkingCopy, 16 | DEFAULT.DIRECTORY: DEFAULT, 17 | } 18 | 19 | log = common.logger(__name__) 20 | 21 | 22 | def find_root(cwd): 23 | """Find the root of the working copy. 24 | 25 | :param cwd: current working directory 26 | 27 | :raises: :class:`doorstop.common.DoorstopError` if the root cannot be found 28 | 29 | :return: path to root of working copy 30 | 31 | """ 32 | path = cwd 33 | 34 | log.debug("looking for working copy from {}...".format(path)) 35 | log.debug("options: {}".format(", ".join([d for d in DIRECTORIES]))) 36 | while not any(d in DIRECTORIES for d in os.listdir(path)): 37 | parent = os.path.dirname(path) 38 | if path == parent: 39 | msg = "no working copy found from: {}".format(cwd) 40 | raise DoorstopError(msg) 41 | path = parent 42 | 43 | log.debug("found working copy: {}".format(path)) 44 | return path 45 | 46 | 47 | def load(path): 48 | """Return a working copy for the specified path.""" 49 | for directory in os.listdir(path): 50 | if directory in DIRECTORIES: 51 | return DIRECTORIES[directory](path) # type: ignore 52 | 53 | log.warning("no working copy found at: {}".format(path)) 54 | return DEFAULT(path) 55 | -------------------------------------------------------------------------------- /doorstop/core/vcs/git.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Plug-in module to store requirements in a Git repository.""" 4 | 5 | from doorstop import common 6 | from doorstop.core.vcs.base import BaseWorkingCopy 7 | 8 | log = common.logger(__name__) 9 | 10 | 11 | class WorkingCopy(BaseWorkingCopy): 12 | """Git working copy.""" 13 | 14 | DIRECTORY = ".git" 15 | IGNORES = (".gitignore",) 16 | 17 | def lock(self, path): 18 | log.debug("`git` does not support locking: %s", path) 19 | self.call("git", "pull") 20 | 21 | def edit(self, path): 22 | self.call("git", "add", self.relpath(path)) 23 | 24 | def add(self, path): 25 | self.call("git", "add", self.relpath(path)) 26 | 27 | def delete(self, path): 28 | self.call("git", "rm", self.relpath(path), "--force", "--quiet") 29 | 30 | def commit(self, message=None): 31 | message = message or input("Commit message: ") 32 | self.call("git", "commit", "--all", "--message", message) 33 | self.call("git", "push") 34 | -------------------------------------------------------------------------------- /doorstop/core/vcs/mercurial.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Plug-in module to store requirements in a Mercurial repository.""" 4 | 5 | from doorstop import common 6 | from doorstop.core.vcs.base import BaseWorkingCopy 7 | 8 | log = common.logger(__name__) 9 | 10 | 11 | class WorkingCopy(BaseWorkingCopy): 12 | """Mercurial working copy.""" 13 | 14 | DIRECTORY = ".hg" 15 | IGNORES = (".hgignore",) 16 | 17 | def lock(self, path): 18 | log.debug("`hg` does not support locking: {}".format(path)) 19 | self.call("hg", "pull", "-u") 20 | 21 | def edit(self, path): 22 | self.call("hg", "add", path) 23 | 24 | def add(self, path): 25 | self.call("hg", "add", path) 26 | 27 | def delete(self, path): 28 | self.call("hg", "remove", path, "--force") 29 | 30 | def commit(self, message=None): 31 | message = message or input("Commit message: ") 32 | self.call("hg", "commit", "--message", message) 33 | self.call("hg", "push") 34 | -------------------------------------------------------------------------------- /doorstop/core/vcs/mockvcs.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Plug-in module to simulate the storage of requirements in a repository.""" 4 | import os 5 | 6 | from doorstop import common 7 | from doorstop.core.vcs.base import BaseWorkingCopy 8 | 9 | log = common.logger(__name__) 10 | 11 | 12 | class WorkingCopy(BaseWorkingCopy): 13 | """Simulated working copy.""" 14 | 15 | DIRECTORY = ".mockvcs" 16 | 17 | def __init__(self, path): 18 | super().__init__(path) 19 | self._ignores_cache = ["*/env/*", "*/apidocs/*", "*/build/lib/*"] 20 | 21 | def lock(self, path): 22 | log.debug("$ simulated lock on: {}...".format(path)) 23 | 24 | def edit(self, path): 25 | log.debug("$ simulated edit on: {}...".format(path)) 26 | 27 | def add(self, path): 28 | log.debug("$ simulated add on: {}...".format(path)) 29 | 30 | def delete(self, path): 31 | os.remove(path) 32 | log.debug("$ Deleted {}...".format(path)) 33 | 34 | def commit(self, message=None): 35 | log.debug("$ simulated commit") 36 | -------------------------------------------------------------------------------- /doorstop/core/vcs/subversion.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Plug-in module to store requirements in a Subversion (1.7) repository.""" 4 | 5 | import os 6 | 7 | from doorstop import common 8 | from doorstop.core.vcs.base import BaseWorkingCopy 9 | 10 | log = common.logger(__name__) 11 | 12 | 13 | class WorkingCopy(BaseWorkingCopy): 14 | """Subversion working copy.""" 15 | 16 | DIRECTORY = ".svn" 17 | IGNORES = (".sgignores", ".vvignores") 18 | 19 | def lock(self, path): 20 | self.call("svn", "update") 21 | self.call("svn", "lock", path) 22 | 23 | def edit(self, path): 24 | log.debug("`svn` adds all changes") 25 | 26 | def add(self, path): 27 | self.call("svn", "add", path) 28 | 29 | def delete(self, path): 30 | self.call("svn", "delete", path) 31 | 32 | def commit(self, message=None): 33 | message = message or input("Commit message: ") 34 | self.call("svn", "commit", "--message", message) 35 | 36 | @property 37 | def ignores(self): 38 | if self._ignores_cache is None: 39 | self._ignores_cache = [] 40 | os.chdir(self.path) 41 | for line in self.call( 42 | "svn", "pg", "-R", "svn:ignore", ".", return_stdout=True 43 | ).splitlines(): 44 | self._ignores_cache.append(line) 45 | return self._ignores_cache 46 | -------------------------------------------------------------------------------- /doorstop/core/vcs/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Integration tests for the doorstop.core.vcs package.""" 4 | 5 | import os 6 | import unittest 7 | from unittest.mock import Mock, patch 8 | 9 | from doorstop.common import DoorstopError 10 | from doorstop.core import vcs 11 | 12 | DIR = os.path.dirname(__file__) 13 | ROOT = os.path.join(DIR, "..", "..", "..", "..") 14 | 15 | 16 | class TestFunctions(unittest.TestCase): 17 | """Unit tests for top-level VCS functions.""" 18 | 19 | @patch("os.listdir", Mock(return_value=[".git"])) 20 | def test_find_root(self): 21 | """Verify a root VCS directory can be found.""" 22 | path = vcs.find_root("fake/path") 23 | self.assertEqual("fake/path", path) 24 | 25 | @patch("os.listdir", Mock(return_value=[])) 26 | def test_find_root_error(self): 27 | """Verify an error occurs when no VCS directory can be found.""" 28 | self.assertRaises(DoorstopError, vcs.find_root, "fake") 29 | 30 | def test_load(self): 31 | """Verify a working copy can be created.""" 32 | working = vcs.load(ROOT) 33 | self.assertIsInstance(working, vcs.git.WorkingCopy) 34 | self.assertEqual(ROOT, working.path) 35 | 36 | def test_load_unknown(self): 37 | """Verify a working copy can be created.""" 38 | working = vcs.load(DIR) 39 | self.assertIsInstance(working, vcs.mockvcs.WorkingCopy) 40 | self.assertEqual(DIR, working.path) 41 | -------------------------------------------------------------------------------- /doorstop/core/vcs/tests/test_all.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Integration tests for the doorstop.vcs package.""" 4 | 5 | import unittest 6 | 7 | from doorstop.core.vcs import load 8 | from doorstop.core.vcs.tests import ROOT 9 | 10 | 11 | class TestWorkingCopy(unittest.TestCase): 12 | """Integration tests for a working copy.""" 13 | 14 | @classmethod 15 | def setUpClass(cls): 16 | cls.wc = load(ROOT) 17 | 18 | def test_ignores(self): 19 | """Verify the ignores file is parsed.""" 20 | patterns = list(self.wc.ignores) 21 | for pattern in patterns: 22 | print(pattern) 23 | self.assertIn("*__pycache__*", patterns) 24 | self.assertIn("*build*", patterns) 25 | -------------------------------------------------------------------------------- /doorstop/core/vcs/tests/test_base.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Unit tests for the doorstop.vcs.base module.""" 4 | 5 | import os 6 | import unittest 7 | from unittest.mock import patch 8 | 9 | from doorstop.core.tests import ROOT 10 | from doorstop.core.vcs.base import BaseWorkingCopy 11 | 12 | 13 | class SampleWorkingCopy(BaseWorkingCopy): 14 | """Sample WorkingCopy implementation.""" 15 | 16 | def __init__(self, path): 17 | super().__init__(path) 18 | self._ignores_cache = ["*build*", "ignored.*", "*published*"] 19 | 20 | def lock(self, path): 21 | print(path) 22 | 23 | def edit(self, path): 24 | print(path) 25 | 26 | def add(self, path): 27 | print(path) 28 | 29 | def delete(self, path): 30 | print(path) 31 | 32 | def commit(self, message=None): 33 | print(message) 34 | 35 | 36 | class TestSampleWorkingCopy(unittest.TestCase): 37 | """Tests for the doorstop.vcs.base module.""" 38 | 39 | def setUp(self): 40 | self.wc = SampleWorkingCopy(None) 41 | 42 | @patch("os.environ", {}) 43 | def test_ignored(self): 44 | """Verify ignored paths are detected.""" 45 | self.assertTrue(self.wc.ignored("ignored.txt")) 46 | self.assertFalse(self.wc.ignored("not_ignored.txt")) 47 | self.assertTrue(self.wc.ignored("path/to/published.html")) 48 | self.assertTrue(self.wc.ignored("build/path/to/anything")) 49 | 50 | @patch("os.environ", {}) 51 | def test_paths(self): 52 | """Verify that paths are cached correctly.""" 53 | wc = SampleWorkingCopy(ROOT) 54 | paths = [relpath for path, _, relpath in wc.paths] 55 | self.assertEqual([], [x for x in paths if x.startswith(".git/")]) 56 | self.assertNotEqual( 57 | [], [x for x in paths if x.startswith(os.path.join("doorstop", ""))] 58 | ) 59 | -------------------------------------------------------------------------------- /doorstop/core/yaml_validator.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Validation of an item's YAML representation.""" 4 | 5 | 6 | class YamlValidator: 7 | """Validates the schema of the Item's YAML file.""" 8 | 9 | @staticmethod 10 | def validate_item_yaml(item_dict): 11 | """Validate a dictionary of item's attributes read from YAML.""" 12 | for key, value in item_dict.items(): 13 | if key == "references": 14 | if value is None: 15 | raise AttributeError( 16 | "'references' must be an array with at least one reference element" 17 | ) 18 | 19 | if not isinstance(value, list): 20 | raise AttributeError("'references' must be an array") 21 | 22 | for ref_dict in value: 23 | if not isinstance(ref_dict, dict): 24 | raise AttributeError("'references' member must be a dictionary") 25 | 26 | ref_keys = ref_dict.keys() 27 | if "type" not in ref_keys: 28 | raise AttributeError( 29 | "'references' member must have a 'type' key" 30 | ) 31 | if "path" not in ref_keys: 32 | raise AttributeError( 33 | "'references' member must have a 'path' key" 34 | ) 35 | 36 | ref_type = ref_dict["type"] 37 | if ref_type != "file": 38 | raise AttributeError( 39 | "'references' member's 'type' value must be a 'file'" 40 | ) 41 | 42 | ref_path = ref_dict["path"] 43 | if not isinstance(ref_path, str): 44 | raise AttributeError( 45 | "'references' member's path must be a string value" 46 | ) 47 | 48 | if "keyword" in ref_dict: 49 | keyword = ref_dict["keyword"] 50 | if not isinstance(keyword, str): 51 | raise AttributeError( 52 | "'references' member's 'keyword' must be a string value" 53 | ) 54 | 55 | return True 56 | -------------------------------------------------------------------------------- /doorstop/gui/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Graphical interface for Doorstop.""" 4 | -------------------------------------------------------------------------------- /doorstop/gui/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Package for the doorstop.gui tests.""" 4 | 5 | ENV = "TEST_INTEGRATION" # environment variable to enable integration tests 6 | REASON = "'{0}' variable not set".format(ENV) 7 | -------------------------------------------------------------------------------- /doorstop/gui/tests/test_all.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Integration tests for the doorstop.cli package.""" 4 | 5 | import importlib 6 | import sys 7 | import unittest 8 | from unittest.mock import Mock, patch 9 | 10 | from doorstop.gui import main as gui 11 | from doorstop.gui.main import main # type: ignore 12 | 13 | 14 | class TestMain(unittest.TestCase): 15 | """Integration tests for the 'doorstop-gui' command.""" 16 | 17 | @patch("doorstop.gui.main.run", Mock(return_value=True)) 18 | def test_gui(self): 19 | """Verify 'doorstop-gui' launches the GUI.""" 20 | self.assertIs(0, main([])) 21 | 22 | @patch("doorstop.gui.main.run", Mock(return_value=False)) 23 | def test_exit(self): 24 | """Verify 'doorstop-gui' treats False as an error .""" 25 | self.assertIs(1, main([])) 26 | 27 | @patch("doorstop.gui.main.run", Mock(side_effect=KeyboardInterrupt)) 28 | def test_interrupt(self): 29 | """Verify 'doorstop-gui' treats KeyboardInterrupt as an error.""" 30 | self.assertIs(1, main([])) 31 | 32 | 33 | class TestImport(unittest.TestCase): 34 | """Integration tests for importing the GUI module.""" 35 | 36 | def test_import(self): 37 | """Verify tkinter import errors are handled.""" 38 | sys.modules["tkinter"] = Mock(side_effect=ImportError) 39 | importlib.reload(gui) 40 | self.assertFalse(gui.run(None, None, lambda x: False)) # pylint: disable=W0212 41 | self.assertIsInstance(gui.tk, Mock) 42 | 43 | 44 | @patch("doorstop.gui.main.run", Mock(return_value=True)) 45 | class TestLogging(unittest.TestCase): 46 | """Integration tests for the Doorstop GUI logging.""" 47 | 48 | def test_verbose_1(self): 49 | """Verify verbose level 1 can be set.""" 50 | self.assertIs(0, main(["-v"])) 51 | 52 | def test_verbose_2(self): 53 | """Verify verbose level 2 can be set.""" 54 | self.assertIs(0, main(["-v", "-v"])) 55 | 56 | def test_verbose_3(self): 57 | """Verify verbose level 1 can be set.""" 58 | self.assertIs(0, main(["-v", "-v", "-v"])) 59 | -------------------------------------------------------------------------------- /doorstop/gui/utilTkinter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | # type: ignore 4 | 5 | import sys 6 | from unittest.mock import Mock 7 | 8 | try: 9 | import tkinter as tk 10 | except ImportError as _exc: 11 | sys.stderr.write("WARNING: {}\n".format(_exc)) 12 | tk = Mock() 13 | ttk = Mock() 14 | 15 | 16 | class HyperlinkManager: 17 | def __init__(self, text) -> None: 18 | self.text = text 19 | 20 | self.text.tag_config("hyper", underline=True) 21 | 22 | self.text.tag_bind("hyper", "", self._enter) 23 | self.text.tag_bind("hyper", "", self._leave) 24 | self.text.tag_bind("hyper", "", self._click) 25 | 26 | self.reset() 27 | 28 | def reset(self): 29 | """Remove all hyperlinks.""" 30 | self.links = {} 31 | 32 | def add(self, action, p_id, p_Tags=[]): # pylint: disable=W0102 33 | """ 34 | Add a new hyper link. 35 | 36 | @param action: method that will be called for this hyperlink 37 | @param p_id: the arbitration id that we are associating this action. 38 | """ 39 | # add an action to the manager. returns tags to use in 40 | # associated text widget 41 | thetags = [] 42 | uniquetag = "hyper-%d" % len(self.links) 43 | thetags.extend(p_Tags) 44 | thetags.append("hyper") 45 | thetags.append(uniquetag) 46 | self.links[uniquetag] = [action, p_id] 47 | return tuple(thetags) 48 | 49 | def _enter(self, event): # pylint: disable=W0613 50 | self.text.config(cursor="hand2") 51 | 52 | def _leave(self, event): # pylint: disable=W0613 53 | self.text.config(cursor="") 54 | 55 | def _click(self, event): # pylint: disable=W0613 56 | """If somebody clicks on the link it will find the method to call.""" 57 | for tag in self.text.tag_names(tk.CURRENT): 58 | if tag[:6] == "hyper-": 59 | self.links[tag][0](self.links[tag][1]) 60 | 61 | 62 | def getAllChildren(treeView, item=None): 63 | """Recursive generator of all the children item of the provided ttk.Treeview.""" 64 | for c_currUID in treeView.get_children(item): 65 | yield c_currUID 66 | yield from getAllChildren(treeView, c_currUID) 67 | -------------------------------------------------------------------------------- /doorstop/server/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Web interface for Doorstop.""" 4 | 5 | from .client import check, get_next_number 6 | -------------------------------------------------------------------------------- /doorstop/server/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | """REST client to request item numbers.""" 5 | 6 | import sys 7 | 8 | import requests 9 | 10 | from doorstop import common, settings 11 | from doorstop.common import DoorstopError 12 | from doorstop.server import utilities 13 | 14 | log = common.logger(__name__) 15 | 16 | 17 | def exists(path="/documents"): 18 | """Determine if the server exists.""" 19 | found = False 20 | url = utilities.build_url(path=path) 21 | if url: 22 | log.debug("looking for {}...".format(url)) 23 | try: 24 | response = requests.head(url, timeout=10) 25 | except requests.exceptions.RequestException as exc: 26 | log.debug(exc) 27 | else: 28 | found = response.status_code == 200 29 | if found: 30 | log.info("found: {}".format(url)) 31 | return found 32 | 33 | 34 | def check(): 35 | """Ensure the server exists.""" 36 | log.info("checking for a server...") 37 | if settings.SERVER_HOST is None: 38 | log.info("no server in use") 39 | return 40 | if not settings.SERVER_HOST: 41 | raise DoorstopError("no server specified") 42 | if not exists(): 43 | raise DoorstopError("unknown server: {}".format(settings.SERVER_HOST)) 44 | 45 | 46 | def get_next_number(prefix): 47 | """Get the next number for the given document prefix.""" 48 | number = None 49 | url = utilities.build_url( 50 | path="/documents/{p}/numbers?format=json".format(p=prefix) 51 | ) 52 | if not url: 53 | log.info("no server to get the next number from") 54 | return None 55 | headers = {"content-type": "application/json"} 56 | response = requests.post(url, headers=headers, timeout=10) 57 | if response.status_code == 200: 58 | data = response.json() 59 | number = data.get("next") 60 | if number is None: 61 | raise DoorstopError("bad response from: {}".format(url)) 62 | log.info("next number from the server: {}".format(number)) 63 | return number 64 | 65 | 66 | if __name__ == "__main__": 67 | if len(sys.argv) != 2: 68 | sys.exit("Usage: {} ".format(sys.argv[0])) 69 | print(get_next_number(sys.argv[-1])) 70 | -------------------------------------------------------------------------------- /doorstop/server/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Package for the doorstop.server tests.""" 4 | 5 | ENV = "TEST_INTEGRATION" # environment variable to enable integration tests 6 | REASON = "'{0}' variable not set".format(ENV) 7 | -------------------------------------------------------------------------------- /doorstop/server/utilities.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Shared functions for the `doorstop.server` package.""" 4 | 5 | from doorstop import common, settings 6 | 7 | log = common.logger(__name__) 8 | 9 | 10 | class StripPathMiddleware: # pylint: disable=R0903 11 | """WSGI middleware that strips trailing slashes from all URLs.""" 12 | 13 | def __init__(self, app): 14 | self.app = app 15 | 16 | def __call__(self, e, h): 17 | e["PATH_INFO"] = e["PATH_INFO"].rstrip("/") 18 | return self.app(e, h) 19 | 20 | 21 | def build_url(host=None, port=None, path=None): 22 | """Build the server's URL with optional path.""" 23 | host = host or settings.SERVER_HOST 24 | port = port or settings.SERVER_PORT 25 | log.debug("building URL: {} + {} + {}".format(host, port, path)) 26 | if not host: 27 | return None 28 | url = "http://{}".format(host) 29 | if port != 80: 30 | url += ":{}".format(port) 31 | if path: 32 | url += path 33 | return url 34 | 35 | 36 | def json_response(request): 37 | """Determine if the request's response should be JSON. 38 | 39 | This is done by checking if there is a query parameter named "format" with the value "json", 40 | or if there is a json body in the request with a parameter named "format" with the value "json". 41 | """ 42 | if request.query.get("format") == "json": 43 | return True 44 | if request.json: 45 | if request.json.get("format") == "json": 46 | return True 47 | return False 48 | -------------------------------------------------------------------------------- /doorstop/settings.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Settings for the Doorstop package.""" 4 | 5 | import logging 6 | import os 7 | 8 | # Logging settings 9 | DEFAULT_LOGGING_FORMAT = "%(message)s" 10 | LEVELED_LOGGING_FORMAT = "%(levelname)s: %(message)s" 11 | VERBOSE_LOGGING_FORMAT = "[%(levelname)-8s] %(message)s" 12 | VERBOSE2_LOGGING_FORMAT = "[%(levelname)-8s] (%(name)s @%(lineno)4d) %(message)s" 13 | QUIET_LOGGING_LEVEL = logging.WARNING 14 | TIMED_LOGGING_FORMAT = "%(asctime)s" + " " + VERBOSE_LOGGING_FORMAT 15 | DEFAULT_LOGGING_LEVEL = logging.WARNING 16 | VERBOSE_LOGGING_LEVEL = logging.INFO 17 | VERBOSE2_LOGGING_LEVEL = logging.DEBUG 18 | VERBOSE3_LOGGING_LEVEL = logging.DEBUG - 1 19 | 20 | # Value constants 21 | SEP_CHARS = "-_." # valid prefix/number separators 22 | SKIP_EXTS = [".yml", ".csv", ".tsv"] # extensions skipped in reference search 23 | RESERVED_WORDS = ["all"] # keywords that cannot be used for prefixes 24 | PLACEHOLDER = "..." # placeholder for new item UIDs on export/import 25 | PLACEHOLDER_COUNT = 1 # number of placeholders to include on export 26 | 27 | # Formatting settings 28 | MAX_LINE_LENGTH = 79 # line length to trigger multiline on extended attributes 29 | 30 | # Validation settings 31 | REFORMAT = True # reformat item files during validation 32 | REORDER = False # reorder document levels during validation 33 | CHECK_LEVELS = True # validate document levels during validation 34 | CHECK_REF = True # validate external file references 35 | CHECK_CHILD_LINKS = True # validate reverse links 36 | CHECK_CHILD_LINKS_STRICT = False # require child (reverse) links from every document 37 | CHECK_SUSPECT_LINKS = True # check stamps on links 38 | CHECK_REVIEW_STATUS = True # check stamps on items 39 | WARN_ALL = False # display info-level issues as warnings 40 | ERROR_ALL = False # display warning-level issues as errors 41 | 42 | # Review settings 43 | REVIEW_NEW_ITEMS = True # automatically review new items during validation 44 | 45 | # Stamping settings 46 | STAMP_NEW_LINKS = True # automatically stamp links upon creation 47 | 48 | # Publishing settings 49 | PUBLISH_CHILD_LINKS = True # include child links when publishing 50 | PUBLISH_BODY_LEVELS = True # include levels on non-header items 51 | PUBLISH_HEADING_LEVELS = True # include levels on header items 52 | ENABLE_HEADERS = True # use headers if defined 53 | WRITE_LINESEPERATOR = os.linesep 54 | 55 | # Version control settings 56 | ADDREMOVE_FILES = True # automatically add/remove new/changed files 57 | 58 | # Caching settings 59 | CACHE_ITEMS = True # cache items in documents and trees 60 | CACHE_DOCUMENTS = True # cache documents in trees 61 | CACHE_PATHS = True # cache file/directory paths and contents 62 | 63 | # Server settings 64 | SERVER_HOST = None # '' = server not specified, None = no server in use 65 | SERVER_PORT = 7867 66 | -------------------------------------------------------------------------------- /doorstop/tests/test_init.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Unit tests for the doorstop.__init__ module.""" 4 | import unittest 5 | from importlib import reload 6 | from importlib.metadata import PackageNotFoundError 7 | from unittest.mock import patch 8 | 9 | import doorstop 10 | 11 | 12 | class InitTestCase(unittest.TestCase): 13 | """Init test class for server tests.""" 14 | 15 | @patch("importlib.metadata.version") 16 | def test_import(self, mock_version): 17 | """Verify the doorstop package can be imported as a local version. 18 | 19 | This test is a bit of a hack. It is intended to verify that the 20 | doorstop package can be imported as a local version. This is 21 | necessary because if the doorstop package is not installed in the 22 | test environment, the version shall be set to "(local)". The patch 23 | ensures that the version lookup will fail. 24 | """ 25 | mock_version.side_effect = PackageNotFoundError() 26 | 27 | # Reload the doorstop package to allow import of the patched version. 28 | reload(doorstop) 29 | from doorstop import VERSION 30 | 31 | # Assert that the version number is correct. 32 | self.assertEqual("Doorstop v(local)", VERSION) 33 | -------------------------------------------------------------------------------- /doorstop/views/base.tpl: -------------------------------------------------------------------------------- 1 | %setdefault('stylesheet', None) 2 | %setdefault('navigation', False) 3 | 4 | 5 | {{!doc_attributes["name"]}} 6 | 7 | 8 | % if is_doc: 9 | % tmpRef='../' 10 | % else: 11 | % tmpRef='' 12 | % end 13 | 14 | 15 | {{! ''%(baseurl+tmpRef+'template/'+stylesheet) if stylesheet else "" }} 16 | 17 | 22 | 23 | 24 | {{! '

Navigation: HomeDocuments'.format(baseurl) if navigation else ''}} 25 | {{!base}} 26 | 27 | 28 | -------------------------------------------------------------------------------- /doorstop/views/document_list.tpl: -------------------------------------------------------------------------------- 1 | % rebase('base.tpl') 2 |

Doorstop - List of documents

3 |

4 |

    5 | {{! "".join('
  • {0}
  • '.format(p) for p in prefixes) }} 6 |
7 | 8 | -------------------------------------------------------------------------------- /doorstop/views/doorstop.tpl: -------------------------------------------------------------------------------- 1 | %setdefault('has_index', True) 2 | %setdefault('has_matrix', True) 3 | % rebase('base.tpl', stylesheet='doorstop.css') 4 | 81 |
82 |
83 |
84 |

{{!doc_attributes["title"]}}

85 | {{!body}} 86 |
87 |
88 |
89 | -------------------------------------------------------------------------------- /doorstop/views/item_list.tpl: -------------------------------------------------------------------------------- 1 | % rebase('base.tpl') 2 |

Doorstop - List of items in {{prefix}}

3 |

4 |

    5 | {{! "".join('
  • {0}
  • '.format(i) for i in items) }} 6 |
7 | 8 | -------------------------------------------------------------------------------- /git_hooks/check_unreviewed_requirements.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | output="$(doorstop -W 2>&1 | grep 'unreviewed changes')" 4 | 5 | if [ -n "$output" ]; then 6 | echo "$output" 7 | exit 1 8 | fi 9 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Doorstop 2 | site_description: Requirements management using version control. 3 | site_author: Jace Browning 4 | 5 | repo_url: https://github.com/doorstop-dev/doorstop 6 | edit_uri: https://github.com/doorstop-dev/doorstop/edit/develop/docs 7 | 8 | theme: readthedocs 9 | 10 | markdown_extensions: 11 | - admonition 12 | use_directory_urls: false 13 | hooks: 14 | - docs/generate.py 15 | 16 | nav: 17 | - Home: index.md 18 | - Getting Started: 19 | - Installation: getting-started/installation.md 20 | - Setup: getting-started/setup.md 21 | - Quickstart: getting-started/quickstart.md 22 | - Command-line Interface: 23 | - Creating Documents: cli/creation.md 24 | - Reordering Documents: cli/reordering.md 25 | - Validating Requirements: cli/validation.md 26 | - Publishing Documents: cli/publishing.md 27 | - Importing and Exporting: cli/interchange.md 28 | - Interfaces: 29 | - Desktop Client: gui/overview.md 30 | - Web Server: web.md 31 | - Scripting API: api/scripting.md 32 | - Reference: 33 | - Tree: reference/tree.md 34 | - Document: reference/document.md 35 | - Item: reference/item.md 36 | - Examples: 37 | - 3rd-Party Projects: examples.md 38 | - Doorstop's Requirements: gen/index.md 39 | - About: 40 | - Release Notes: about/changelog.md 41 | - Contributing: about/contributing.md 42 | - License: about/license.md 43 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | 3 | name = "doorstop" 4 | version = "3.0.1" 5 | description = "Requirements management using version control." 6 | 7 | license = "LGPLv3" 8 | 9 | authors = ["Jace Browning "] 10 | 11 | readme = "README.md" 12 | exclude = ["**/tests/**"] 13 | 14 | homepage = "https://pypi.org/project/Doorstop" 15 | documentation = "https://doorstop.readthedocs.io" 16 | repository = "https://github.com/doorstop-dev/doorstop" 17 | 18 | keywords = [ 19 | "requirements-management", 20 | "version-control", 21 | "documentation", 22 | "traceability", 23 | "markdown", 24 | "certification", 25 | "html", 26 | ] 27 | 28 | classifiers = [ 29 | "Development Status :: 5 - Production/Stable", 30 | "Environment :: Console", 31 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 32 | "Operating System :: OS Independent", 33 | "Programming Language :: Python", 34 | "Programming Language :: Python :: 3", 35 | "Programming Language :: Python :: 3.9", 36 | "Programming Language :: Python :: 3.10", 37 | "Programming Language :: Python :: 3.11", 38 | "Programming Language :: Python :: 3.12", 39 | "Programming Language :: Python :: 3.13", 40 | "Topic :: Software Development :: Documentation", 41 | "Topic :: Text Editors :: Documentation", 42 | "Topic :: Text Processing :: Markup", 43 | ] 44 | 45 | [tool.poetry.dependencies] 46 | 47 | python = "<3.14,>=3.9" 48 | 49 | pyyaml = "^6.0" 50 | markdown = "^3.3.3" 51 | bottle = "~0.12.25" 52 | requests = "^2.0" 53 | python-frontmatter = "^1.0" 54 | python-markdown-math = "~0.6" 55 | plantuml-markdown = "^3.4.2" 56 | six = "*" # fixes https://github.com/dougn/python-plantuml/issues/11 57 | openpyxl = ">=3.1.2" 58 | verchew = "^3.4.2" 59 | 60 | [tool.poetry.dev-dependencies] 61 | 62 | # Formatters 63 | black = "^24.3" 64 | isort = "^5.12" 65 | 66 | # Linters 67 | mypy = ">=1.1.1" 68 | pydocstyle = "*" 69 | pylint = "~3.2.0" 70 | types-markdown = "*" 71 | types-pyyaml = "*" 72 | types-requests = "*" 73 | types-setuptools = "*" 74 | 75 | # Testing 76 | pytest = "^6.2.5" 77 | pytest-cov = "*" 78 | pytest-expecter = "*" 79 | pytest-sugar = "*" 80 | WebTest = "^3.0.0" 81 | diff-cover = "^8.0.3" 82 | 83 | # Reports 84 | coveragespace = "^6.1" 85 | 86 | # Documentation 87 | mkdocs = "^1.5" 88 | pygments = "*" 89 | 90 | # Tooling 91 | pyinstaller = "*" 92 | sniffer = "*" 93 | macfsevents = { version = "*", platform = "darwin", markers = "python_version < '3.13'" } 94 | pync = { version = "*", platform = "darwin" } 95 | rope = "*" 96 | 97 | [tool.poetry.scripts] 98 | 99 | doorstop = "doorstop.cli.main:main" 100 | doorstop-gui = "doorstop.gui.main:main" 101 | doorstop-server = "doorstop.server.main:main" 102 | 103 | [tool.black] 104 | 105 | quiet = true 106 | 107 | [tool.isort] 108 | 109 | profile = "black" 110 | 111 | [build-system] 112 | 113 | requires = ["poetry-core"] 114 | build-backend = "poetry.core.masonry.api" 115 | -------------------------------------------------------------------------------- /reqs/.doorstop.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | digits: 3 3 | prefix: REQ 4 | sep: '' 5 | attributes: 6 | defaults: 7 | doc: 8 | name: 'Requirements' 9 | title: 'Requirements for _Doorstop_' 10 | ref: 'REQ-DS-2024' 11 | by: 'Wfg' 12 | major: '1' 13 | minor: 'A' 14 | copyright: 'Doorstop' 15 | -------------------------------------------------------------------------------- /reqs/REQ001.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Assets 5 | level: 2.3 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: avwblqPimDJ2OgTrRCXxRPN8FQhUBWqPIXm7kSR95C4= 10 | text: | 11 | Doorstop **shall** support the storage of external requirements assets. 12 | -------------------------------------------------------------------------------- /reqs/REQ002.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 2.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: XQwVjcTIdnbfgDZLM1ZyqMZa6aO4DrSahsK8Eji4Wzs= 9 | text: | 10 | Composition Features 11 | -------------------------------------------------------------------------------- /reqs/REQ003.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Identifiers 5 | level: 2.1 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: 9TcFUzsQWUHhoh5wsqnhL7VRtSqMaIhrCXg7mfIkxKM= 10 | text: | 11 | Doorstop **shall** provide unique and permanent identifiers to linkable 12 | sections of text. 13 | -------------------------------------------------------------------------------- /reqs/REQ004.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Formatting 5 | level: 2.2 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: T2tSkn27DO3GXvagwOgNNLvhW4FPNg9gyLfru-l9hWQ= 10 | text: | 11 | Doorstop **shall** support formatting within linkable text. 12 | -------------------------------------------------------------------------------- /reqs/REQ006.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 3.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: kJ7kN6wsDHYywfmsDUWTgyw91Ce2yM03vpzC5w9Y93g= 9 | text: | 10 | Presentation Features 11 | -------------------------------------------------------------------------------- /reqs/REQ007.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Viewing documents 5 | level: 3.1 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: N4qTPlDi0z6kClsYAWlTsYPYWPylyr5KscMlxyYlzbA= 10 | text: | 11 | Doorstop **shall** provide a way to view requirements as a document. 12 | -------------------------------------------------------------------------------- /reqs/REQ008.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Interactive viewing 5 | level: 3.2 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: Y9QwGNJVzJSHbW9sHzBqswqAtF5v8OJup8HEH7E2qHU= 10 | text: | 11 | Doorstop **shall** provide a way to view document items interactively. 12 | -------------------------------------------------------------------------------- /reqs/REQ009.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Baseline versions 5 | level: 3.3 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: uuTiyRLtSfUnneuE71WqML8D_X8YtiKj02Ehlb8b2bg= 10 | text: | 11 | Doorstop **shall** provide the capability to baseline versions of 12 | requirements. 13 | -------------------------------------------------------------------------------- /reqs/REQ010.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 4.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: w3QVQhmViSZ_9hlxTy89hOLiiO3zQP6juQC4hYewX7s= 9 | text: | 10 | Administration Features 11 | -------------------------------------------------------------------------------- /reqs/REQ011.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Storing requirements 5 | level: 4.1 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: 3N-eJ3o7Va-Vwx8F4VCE1eYw4WCwK1DleYM95RfsaEQ= 10 | text: | 11 | Doorstop **shall** store requirements in a permanent and secure manner. 12 | -------------------------------------------------------------------------------- /reqs/REQ012.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Change management 5 | level: 4.2 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: aDEVUTCV4yqDY99sfjch7otkgidiyG0We8tGSTWD9Hs= 10 | text: | 11 | Doorstop **shall** handle change management of the requirements. 12 | -------------------------------------------------------------------------------- /reqs/REQ013.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Author information 5 | level: 4.3 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: ch1ilz7OJnQhSuKbpcN81Z0ml2z6lBTQqV6rWanW_ZA= 10 | text: | 11 | Doorstop **shall** associate requirements changes to existing developer 12 | identification. 13 | -------------------------------------------------------------------------------- /reqs/REQ014.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Scalability 5 | level: 4.4 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: r_QHE6crBVcD_cEeXXtztbaeU7PoTnvpQ6uSpR3M63w= 10 | text: | 11 | Doorstop **shall** be scalable to support thousands of requirements. 12 | -------------------------------------------------------------------------------- /reqs/REQ015.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Installation 5 | level: 4.5 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: DxAA240XYSKeWMmaKCR3OymyabO52_T7dQGRVfF7yKw= 10 | text: | 11 | Doorstop **shall** provide a lightweight installation method. 12 | -------------------------------------------------------------------------------- /reqs/REQ016.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Importing content 5 | level: 2.4 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: y2OYEsfH2SeMgox69qRaoF8vq2C9JtR5eJ-0wXXIBJI= 10 | text: | 11 | Doorstop **shall** be able to import content from other requirments 12 | management tools. 13 | -------------------------------------------------------------------------------- /reqs/REQ017.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Exporting content 5 | level: 2.5 6 | links: [] 7 | normative: true 8 | ref: '' 9 | reviewed: swor_1mByeIN71VfyIFD2i06PT2nX47zDKbrxDYWR-0= 10 | text: | 11 | Doorstop **shall** be able to export content to interact with common 12 | text/document tools. 13 | -------------------------------------------------------------------------------- /reqs/REQ018.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: T7URqeO_PWK6KoVq43mGLtmc_rM7Ytw7nRDIqo7FPYE= 9 | text: | 10 | Overview 11 | -------------------------------------------------------------------------------- /reqs/REQ019.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Introduction 5 | level: 1.1 6 | links: [] 7 | normative: false 8 | ref: '' 9 | reviewed: bb-HpMnSXh4pZIm4RHCARfJa0ifqGam0Fc5vjHEldgs= 10 | text: | 11 | Doorstop is a requirements management tool that leverages version control to 12 | store and manage a project's documentation traced from specification through 13 | implementation. 14 | 15 | ![Doorstop Logo](assets/logo-black-white.png "Doorstop Logo") 16 | -------------------------------------------------------------------------------- /reqs/assets/logo-black-white.png: -------------------------------------------------------------------------------- 1 | ../../docs/images/logo-black-white.png -------------------------------------------------------------------------------- /reqs/ext/.doorstop.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | digits: 3 3 | itemformat: yaml 4 | parent: REQ 5 | prefix: EXT 6 | sep: "" 7 | extensions: 8 | item_validator: .req_sha_item_validator.py 9 | item_sha_required: true 10 | -------------------------------------------------------------------------------- /reqs/ext/.req_sha_item_validator.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | from doorstop import DoorstopInfo, DoorstopWarning, DoorstopError 3 | from subprocess import check_output 4 | from copy import copy 5 | from random import random 6 | 7 | 8 | def item_validator(item): 9 | if getattr(item, "references") == None: 10 | return [] 11 | 12 | for ref in item.references: 13 | if ref['sha'] != item._hash_reference(ref['path']): 14 | yield DoorstopWarning("Hash has changed and it was not reviewed properly") 15 | 16 | if 'modified' in ref['path']: 17 | temp_item = copy(item) 18 | current_value = item.is_reviewed() 19 | check_output( 20 | "echo '1111' > $(git rev-parse --show-toplevel)/reqs/ext/test-modified.file", shell=True) 21 | temp_item.review() 22 | next_value = item.is_reviewed() 23 | check_output( 24 | "echo '0000' > $(git rev-parse --show-toplevel)/reqs/ext/test-modified.file", shell=True) 25 | 26 | yield DoorstopWarning(f"""This is a demonstration of a validator per folder identifying a external ref modified 27 | without a proper review current SHA {current_value} modified SHA {next_value }. 28 | Result: { next_value == current_value} """) 29 | -------------------------------------------------------------------------------- /reqs/ext/EXT001.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.0 5 | links: [] 6 | normative: true 7 | ref: '' 8 | references: 9 | - path: reqs/ext/test.file 10 | sha: 24d9b35c727e6c78676c2ad378a8c3c47cfb539f3583d3cd9e1eafee51d5679d 11 | type: file 12 | reviewed: n0xxAj0z-SqNcebKEu8p9HDE8jAs5I8Vz4kX-5ZieA4= 13 | text: | 14 | Test where we calculate the SHA 15 | -------------------------------------------------------------------------------- /reqs/ext/EXT002.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 2.0 5 | links: [] 6 | normative: true 7 | ref: '' 8 | references: 9 | - path: reqs/ext/test-modified.file 10 | sha: 49ca5d81054fdd20572294b9350b605d05e0df91da09a46fb8bde7fd6c1c172d 11 | type: file 12 | reviewed: arLWz1tqET94t2j7FG0ncvKpwGe5twDi-jPbBnikxho= 13 | text: | 14 | Test where we calculate the SHA, file modified during evaluation 15 | -------------------------------------------------------------------------------- /reqs/ext/test-modified.file: -------------------------------------------------------------------------------- 1 | 0000 2 | -------------------------------------------------------------------------------- /reqs/ext/test.file: -------------------------------------------------------------------------------- 1 | 0001 2 | -------------------------------------------------------------------------------- /reqs/tutorial/.doorstop.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | digits: 3 3 | parent: REQ 4 | prefix: TUT 5 | sep: '' 6 | attributes: 7 | defaults: 8 | doc: 9 | name: 'Tutorial' 10 | title: 'Tutorial for _Doorstop_ requirements management' 11 | ref: 'TUT-DS-2024' 12 | by: 'Wfg' 13 | major: '1' 14 | minor: 'A' 15 | copyright: 'Doorstop' 16 | publish: 17 | - CUSTOM-ATTRIB 18 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT001.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.1 5 | links: 6 | - REQ003: 9TcFUzsQWUHhoh5wsqnhL7VRtSqMaIhrCXg7mfIkxKM= 7 | - REQ004: T2tSkn27DO3GXvagwOgNNLvhW4FPNg9gyLfru-l9hWQ= 8 | normative: true 9 | ref: '' 10 | reviewed: 4sVZFR60cL0w9FttKN20oBQNhdF12ApSwmOnM35ddRY= 11 | text: | 12 | **Creating a New Document and Adding Items** 13 | 14 | Enter a VCS working copy: 15 | 16 | $ cd /tmp/doorstop 17 | 18 | Create a new document: 19 | 20 | $ doorstop create REQ ./reqs 21 | 22 | Add items: 23 | 24 | $ doorstop add REQ 25 | $ doorstop add REQ 26 | $ doorstop add REQ 27 | 28 | Edit the new items in the default text editor: 29 | 30 | $ doorstop edit REQ1 31 | $ doorstop edit REQ2 32 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT002.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.2 5 | links: 6 | - REQ003: 9TcFUzsQWUHhoh5wsqnhL7VRtSqMaIhrCXg7mfIkxKM= 7 | - REQ004: T2tSkn27DO3GXvagwOgNNLvhW4FPNg9gyLfru-l9hWQ= 8 | - REQ011: 3N-eJ3o7Va-Vwx8F4VCE1eYw4WCwK1DleYM95RfsaEQ= 9 | - REQ012: aDEVUTCV4yqDY99sfjch7otkgidiyG0We8tGSTWD9Hs= 10 | - REQ013: ch1ilz7OJnQhSuKbpcN81Z0ml2z6lBTQqV6rWanW_ZA= 11 | normative: true 12 | ref: '' 13 | reviewed: 21EAe6LYhAzOvGgzXcid08kyNt3aXdRjf64b24cf-AA= 14 | text: | 15 | **Creating a Child Document with Links to the Parent Document** 16 | 17 | Enter a VCS working copy: 18 | 19 | $ cd /tmp/doorstop 20 | 21 | Create a new child document: 22 | 23 | $ doorstop create TST ./reqs/tests --parent REQ 24 | 25 | Add new items: 26 | 27 | $ doorstop add TST 28 | $ doorstop add TST 29 | 30 | Edit the new items in the default text editor: 31 | 32 | $ doorstop edit TST1 33 | $ doorstop edit TST2 34 | 35 | Add links to item's in the parent document: 36 | 37 | $ doorstop link TST1 REQ1 38 | $ doorstop link TST1 REQ3 39 | $ doorstop link TST2 REQ1 40 | $ doorstop link TST2 REQ2 41 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT003.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1 5 | links: [] 6 | normative: true 7 | ref: '' 8 | reviewed: CFQ_a9SPDWwyv_Sw-k8Vkm4XnC3tDkwRXAYZ9bk_IQ8= 9 | text: '' 10 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT004.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.3 5 | links: 6 | - REQ003: 9TcFUzsQWUHhoh5wsqnhL7VRtSqMaIhrCXg7mfIkxKM= 7 | - REQ011: 3N-eJ3o7Va-Vwx8F4VCE1eYw4WCwK1DleYM95RfsaEQ= 8 | - REQ012: aDEVUTCV4yqDY99sfjch7otkgidiyG0We8tGSTWD9Hs= 9 | - REQ013: ch1ilz7OJnQhSuKbpcN81Z0ml2z6lBTQqV6rWanW_ZA= 10 | normative: true 11 | ref: '' 12 | reviewed: 3KoKMtZljDE7t4mzJIY_s13kMfAiDUgV_Ig5WMoWZ2U= 13 | text: | 14 | **Removing Items and Links** 15 | 16 | Enter a VCS working copy: 17 | 18 | $ cd /tmp/doorstop 19 | 20 | Remove a link between two document items: 21 | 22 | $ doorstop unlink TST1 REQ3 23 | 24 | Remove a document's item: 25 | 26 | $ doorstop remove REQ3 27 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT005.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 2.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: HmjDKBW1DjmixkzdADInyZN3lRQXsJjFFr4W8PFMGOo= 9 | text: | 10 | Publishing Documents 11 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT008.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.4 5 | links: 6 | - REQ003: 9TcFUzsQWUHhoh5wsqnhL7VRtSqMaIhrCXg7mfIkxKM= 7 | normative: true 8 | ref: '' 9 | reviewed: vNBr4d83a8SIKz4mOaDu2_eLZlXcgDoKSW09sThlb9U= 10 | text: | 11 | **Validating the Tree** 12 | 13 | Enter a VCS working copy: 14 | 15 | $ cd /tmp/doorstop 16 | 17 | Build and validate the tree: 18 | 19 | $ doorstop 20 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT009.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 2.1 5 | links: 6 | - REQ007: N4qTPlDi0z6kClsYAWlTsYPYWPylyr5KscMlxyYlzbA= 7 | normative: true 8 | ref: '' 9 | reviewed: c5B5iK7ImS2RzInDT-2lGLxphNQbn_kiXBAZnUYs-to= 10 | text: | 11 | **Publishing a Document as Text** 12 | 13 | Enter a VCS working copy: 14 | 15 | $ cd /tmp/doorstop 16 | 17 | Display the documents on standard output: 18 | 19 | $ doorstop publish req 20 | $ doorstop publish tst 21 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT010.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 2.2 5 | links: 6 | - REQ007: N4qTPlDi0z6kClsYAWlTsYPYWPylyr5KscMlxyYlzbA= 7 | normative: true 8 | ref: '' 9 | reviewed: ngBTlPb2ceCpLCYx6k8-dubKS0mHe-5rWRzx0JxmmCk= 10 | text: | 11 | **Publishing All Documents as an HTML Directory** 12 | 13 | Enter VCS working copy: 14 | 15 | $ cd /tmp/doorstop 16 | 17 | Create an HTML directory for all documents: 18 | 19 | $ doorstop publish all path/to/htmldir 20 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT011.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 3.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: kjxGvPAHLrgJljFuuKToVfM1Z90a216bOizMsLq8sPw= 9 | text: | 10 | Importing Content 11 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT012.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 3.2 5 | links: 6 | - REQ016: y2OYEsfH2SeMgox69qRaoF8vq2C9JtR5eJ-0wXXIBJI= 7 | normative: true 8 | ref: '' 9 | reviewed: EZkHT1vOOvOHIbaKJ9rpVB6_3WAbYK7c9dip4RMf4zU= 10 | text: | 11 | **Importing a Document** 12 | 13 | Enter a VCS working copy: 14 | 15 | $ cd /tmp/doorstop 16 | 17 | Import a document: 18 | 19 | $ doorstop import --document HLR reqs/hlr 20 | 21 | Import a document with a parent: 22 | 23 | $ doorstop import --document LLR reqs/llr --parent HLR 24 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT013.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 3.3 5 | links: 6 | - REQ016: y2OYEsfH2SeMgox69qRaoF8vq2C9JtR5eJ-0wXXIBJI= 7 | normative: true 8 | ref: '' 9 | reviewed: 6Zfa7fYSteAA535QNMs2ro1RU-g72XXKrm9tOffQT2Q= 10 | text: | 11 | **Importing an Item** 12 | 13 | Enter a VCS working copy: 14 | 15 | $ cd /tmp/doorstop 16 | 17 | Import an item: 18 | 19 | $ doorstop import --item HLR HLR001 20 | 21 | Import an item with attributes: 22 | 23 | $ doorstop import --item LLR LLR001 --attr "{'text': 'The item text.'}" 24 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT014.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 4.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: MJIh2AD06WwsFv7VWQ52BCL8Eu4SqFHwJMhw0g3uT1k= 9 | text: | 10 | Exporting Content 11 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT015.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 4.1 5 | links: 6 | - REQ017: swor_1mByeIN71VfyIFD2i06PT2nX47zDKbrxDYWR-0= 7 | normative: true 8 | ref: '' 9 | reviewed: bWhqqNInBaWMU4MafipoqwNjzTCPHm568-Vs5B-QaCk= 10 | text: | 11 | **Exporting a Document** 12 | 13 | Enter a VCS working copy: 14 | 15 | $ cd /tmp/doorstop 16 | 17 | Export a document to standard outout: 18 | 19 | $ doorstop export LLR 20 | 21 | Export all documents to a directory: 22 | 23 | $ doorstop export all dirpath/to/exports 24 | 25 | Export documents using specific formats: 26 | 27 | $ doorstop export REQ path/to/req.xlsx 28 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT016.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 3.1 5 | links: 6 | - REQ016: y2OYEsfH2SeMgox69qRaoF8vq2C9JtR5eJ-0wXXIBJI= 7 | normative: true 8 | ref: '' 9 | reviewed: -urC-KEzvImhVzQizAOXbwhTVYZN6zr3wD2a4_088mk= 10 | text: | 11 | **Importing a File** 12 | 13 | Enter a VCS working copy: 14 | 15 | cd /tmp/doorstop 16 | 17 | Create a document for the import: 18 | 19 | doorstop create HLR 20 | 21 | Import from an exported document: 22 | 23 | doorstop import path/to/exported.xlsx HLR 24 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT017.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Lot's of different little examples in a single heading which is very long 5 | level: 1.5 6 | links: 7 | - REQ004: T2tSkn27DO3GXvagwOgNNLvhW4FPNg9gyLfru-l9hWQ= 8 | normative: true 9 | ref: '' 10 | reviewed: uPOK_klxAHJRhHZmfgD1HJSs1Gs39jKv2uvlInyl1Ec= 11 | text: | 12 | ### Headings 3 13 | 14 | Markdown can be used to format text. 15 | 16 | #### Heading 4 17 | 18 | ##### Heading 5 19 | 20 | ### Emphasis 21 | Emphasis, aka italics, with *asterisks* or _underscores_. 22 | 23 | Strong emphasis, aka bold, with **asterisks** or __underscores__. 24 | 25 | Combined emphasis with **asterisks and _underscores_**. 26 | 27 | Strikethrough uses two tildes. ~~Scratch this.~~ 28 | 29 | ### Paragraphs 30 | When exporting to html using doorstop newlines 31 | are converted to line breaks. 32 | 33 | An empty line is required to start a new paragraph. 34 | 35 | ### Lists 36 | 1. First ordered list item 37 | 2. Another item 38 | 1. Actual numbers don't matter, 39 | just that it's a numbers 40 | 41 | * Unordered list can use asterisks 42 | - Or minuses 43 | + Or pluses 44 | 45 | ### Tables 46 | Colons can be used to align columns. 47 | 48 | | Tables | Are | Cool | 49 | | ------------- |:-------------:| -----:| 50 | | col 3 is | right-aligned | $1600 | 51 | | col 2 is | centered | $12 | 52 | | zebra stripes | are neat | $1 | 53 | 54 | There must be at least 3 dashes separating each header cell. 55 | The outer pipes (|) are optional, and you don't need to make the 56 | raw Markdown line up prettily. You can also use inline Markdown. 57 | 58 | Markdown | Less | Pretty 59 | --- | --- | --- 60 | *Still* | `renders` | **nicely** 61 | 1 | 2 | 3 62 | 63 | ### UML Diagrams 64 | 65 | [PlantUML-Guide](http://plantuml.com/guide) explains the syntax for 66 | all supported diagram types. Here you see an exemplary state chart diagram: 67 | 68 | ```plantuml format="png" alt="State Diagram Loading" title="State Diagram" 69 | @startuml 70 | scale 600 width 71 | 72 | [*] -> State1 73 | State1 --> State2 : Succeeded 74 | State1 --> [*] : Aborted 75 | State2 --> State3 : Succeeded 76 | State2 --> [*] : Aborted 77 | state State3 { 78 | state "Accumulate Enough Data\nLong State Name" as long1 79 | long1 : Just a test 80 | [*] --> long1 81 | long1 --> long1 : New Data 82 | long1 --> ProcessData : Enough Data 83 | } 84 | State3 --> State3 : Failed 85 | State3 --> [*] : Succeeded / Save Result 86 | State3 --> [*] : Aborted 87 | 88 | @enduml 89 | ``` 90 | 91 | ### Math LaTex 92 | 93 | You can use Math LaTex expressions as `$$k_{n+1} = n^2 + k_n^2 - k_{n-1}$$` 94 | which is rendered like this: 95 | 96 | $$k_{n+1} = n^2 + k_n^2 - k_{n-1}$$ 97 | 98 | ### Code blocks 99 | 100 | Code blocks are either fenced by lines with three back-ticks \`\`\`. Optionally, you can add an 101 | identifier for the language to use for syntax highlighting. For example, this snippet 102 | shows a Python code block and thus starts with \`\`\`python: 103 | 104 | ```python 105 | def fibonacci(n): 106 | a, b = 0, 1 107 | while a < n: 108 | print(a, end=' ') 109 | a, b = b, a+b 110 | print() 111 | fibonacci(1000) 112 | ``` 113 | 114 | They can also be inline `code` by a single back-tick \`. 115 | 116 | C-code can be rendered with syntax highlighting by using the `c` identifier (\`\`\`c): 117 | 118 | ```c 119 | #include 120 | int main() { 121 | printf("Hello, World!"); 122 | return 0; 123 | } 124 | ``` 125 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT018.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.6.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: PuN-p68SLzbHje_SZ5OrEFicycXQHvuHSO-2caAnaqo= 9 | text: | 10 | Sub headings 11 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT019.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 1.6.1 5 | links: 6 | - REQ004: T2tSkn27DO3GXvagwOgNNLvhW4FPNg9gyLfru-l9hWQ= 7 | normative: true 8 | ref: '' 9 | reviewed: sOcI4__o2goMqOAECNTgCLlW19__Yhf7Amq9xKa8ZEA= 10 | text: | 11 | Sub headings can be created by a level that ends in .0 12 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT020.yml: -------------------------------------------------------------------------------- 1 | CUSTOM-ATTRIB: true 2 | active: true 3 | derived: false 4 | header: '' 5 | level: 2.3 6 | links: 7 | - REQ007: N4qTPlDi0z6kClsYAWlTsYPYWPylyr5KscMlxyYlzbA= 8 | normative: true 9 | ref: '' 10 | reviewed: eXhaivJwaU26FVviGRdvG0i0a_YV5y6BKSxf4t55umY= 11 | text: | 12 | **Include custom attributes in published output** 13 | 14 | Include the custom attribute that should be published in `.doorstop.yml`, 15 | e.g. 16 | 17 | settings: 18 | digits: 3 19 | prefix: TUT 20 | sep: '' 21 | attributes: 22 | publish: 23 | - CUSTOM-ATTRIB 24 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT021.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: '' 4 | level: 5.0 5 | links: [] 6 | normative: false 7 | ref: '' 8 | reviewed: 2skYLv7WRiIExnVl29fSuJImDCj1_M8s4UpOTPGxbXY= 9 | text: | 10 | Detailed examples 11 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT022.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Lists 5 | level: 5.1 6 | links: 7 | - REQ017: swor_1mByeIN71VfyIFD2i06PT2nX47zDKbrxDYWR-0= 8 | normative: false 9 | ref: '' 10 | reviewed: JiNddGPoyo_4zpIiEwvkwGGAR9iDZA0QJ_dW9kvNeYo= 11 | text: '' 12 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT023.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Nested list 5 | level: 5.1.1 6 | links: [] 7 | normative: false 8 | ref: '' 9 | reviewed: -jqMsO-XC6Xh4AhhKKWecewXvZao5nDtoUU6rpiL8dM= 10 | text: | 11 | This is an example on how to create a list: 12 | * Item 1 13 | * Item 2 14 | * Nested list 15 | * Item 2.1 16 | * Item 2.2 17 | * Deeper nesting 18 | * Item 2.2.1 19 | * Item 2.2.2 20 | * Item 2.2.3 21 | + Empty 22 | + Nesting 23 | * Should work 24 | - too! 25 | 26 | _Note: The list above is not numbered, and it works with both `*`, `+` and `-`. It also works with two (2) or four (4) space indentation, although you should keep the same indentation in the list._ 27 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT024.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Ordered list with empty items 5 | level: 5.1.2 6 | links: [] 7 | normative: false 8 | ref: '' 9 | reviewed: cGjgXyXJ935uKtAauBIH2yo8QMpe_WJNfEJ7UrNOOVg= 10 | text: | 11 | This is an example with nesting and empty list items: 12 | 1. Item 1 13 | 1. Item 2 14 | 1. Item 2.1 15 | 1. Item 2.1.1 16 | 1. Item 3 17 | 18 | _Notes_: 19 | 20 | Numered (or **ordered**) lists in Markdown are a bit special. 21 | 22 | They are identified with a number followed immediatly by a . (dot). 23 | 24 | **What** number does not really matter! 25 | 26 | Therefore, the easiest way to make sure **all** differenct markdown renderers 27 | handles the list correctly is to use the same number for all items in the list, 28 | i.e., 1. 29 | 30 | The indentation is also important. Whereas the unordered lists can handle 31 | different indentation, the ordered lists must have four (4) spaces 32 | indentation for each level! 33 | 34 | _**IMPORTANT**_ 35 | 36 | _Ordered lists that are nested in Markdown cannot automatically handle nested numbering!_ 37 | In other words, each nested level **will** start with the number 1! 38 | -------------------------------------------------------------------------------- /reqs/tutorial/TUT025.yml: -------------------------------------------------------------------------------- 1 | active: true 2 | derived: false 3 | header: | 4 | Another list example 5 | level: 5.1.3 6 | links: [] 7 | normative: false 8 | ref: '' 9 | reviewed: 0XHgkdYzXl4GGrraMcRMp1UkWrr6pbaEd94_RHLgyF0= 10 | text: | 11 | Another list example: 12 | 1. Item 1 13 | 1. Item 1.1 14 | 1. Item 1.2 15 | 1. Item 2 16 | 1. Item 2.1 17 | -------------------------------------------------------------------------------- /scent.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-only 2 | 3 | """Configuration file for sniffer.""" 4 | # pylint: disable=superfluous-parens,bad-continuation 5 | 6 | import time 7 | import subprocess 8 | 9 | from sniffer.api import select_runnable, file_validator, runnable 10 | 11 | try: 12 | from pync import Notifier 13 | except ImportError: 14 | notify = None 15 | else: 16 | notify = Notifier.notify 17 | 18 | 19 | watch_paths = ["doorstop", "tests"] 20 | 21 | 22 | class Options: 23 | group = int(time.time()) # unique per run 24 | show_coverage = False 25 | rerun_args = None 26 | 27 | targets = [ 28 | (("make", "test-unit", "DISABLE_COVERAGE=true"), "Unit Tests", True), 29 | (("make", "test-int"), "Integration Tests", False), 30 | (("make", "check"), "Static Analysis", True), 31 | (("make", "demo"), "Run Demo", False), 32 | (("make", "docs"), None, True), 33 | ] 34 | 35 | 36 | @select_runnable("run_targets") 37 | @file_validator 38 | def python_files(filename): 39 | return filename.endswith(".py") 40 | 41 | 42 | @select_runnable("run_targets") 43 | @file_validator 44 | def html_files(filename): 45 | return filename.split(".")[-1] in ["html", "css", "js"] 46 | 47 | 48 | @runnable 49 | def run_targets(*args): 50 | """Run targets for Python.""" 51 | Options.show_coverage = "coverage" in args 52 | 53 | count = 0 54 | for count, (command, title, retry) in enumerate(Options.targets, start=1): 55 | 56 | success = call(command, title, retry) 57 | if not success: 58 | message = "✅ " * (count - 1) + "❌" 59 | show_notification(message, title) 60 | 61 | return False 62 | 63 | message = "✅ " * count 64 | title = "All Targets" 65 | show_notification(message, title) 66 | show_coverage() 67 | 68 | return True 69 | 70 | 71 | def call(command, title, retry): 72 | """Run a command-line program and display the result.""" 73 | if Options.rerun_args: 74 | command, title, retry = Options.rerun_args 75 | Options.rerun_args = None 76 | success = call(command, title, retry) 77 | if not success: 78 | return False 79 | 80 | print("") 81 | print("$ %s" % " ".join(command)) 82 | failure = subprocess.call(command) 83 | 84 | if failure and retry: 85 | Options.rerun_args = command, title, retry 86 | 87 | return not failure 88 | 89 | 90 | def show_notification(message, title): 91 | """Show a user notification.""" 92 | if notify and title: 93 | notify(message, title=title, group=Options.group) 94 | 95 | 96 | def show_coverage(): 97 | """Launch the coverage report.""" 98 | if Options.show_coverage: 99 | subprocess.call(["make", "read-coverage"]) 100 | 101 | Options.show_coverage = False 102 | --------------------------------------------------------------------------------