] # noqa: F821
+show_error_codes = True
+
+warn_unused_ignores = True
+warn_redundant_casts = True
+warn_no_return = True
+warn_return_any = True
+warn_unreachable = True
+strict_optional = True
+disallow_untyped_calls = True
+disallow_untyped_defs = True
+check_untyped_defs = True
+disallow_incomplete_defs = True
+disallow_untyped_decorators = True
+disallow_any_generics = True
+strict_equality = True
+
+[mypy-tests.*]
+disallow_untyped_decorators = False
+disallow_incomplete_defs = False
+disallow_untyped_defs = False
+check_untyped_defs = False
+
+[mypy-smart_open.*]
+ignore_missing_imports = True
+
+[mypy-sqlalchemy.*]
+ignore_missing_imports = True
+
+[mypy-stdlib_utils.*]
+ignore_missing_imports = True
+
+[mypy-urllib3.util.retry.*]
+ignore_missing_imports = True
+
+[mypy-defusedxml.*]
+ignore_missing_imports = True
+
+[mypy-python_git_wrapper.*]
+ignore_missing_imports = True
+
+[mypy-python_git_wrapper.exceptions.*]
+ignore_missing_imports = True
+
+[mypy-mock.*]
+ignore_missing_imports = True
+
+[mypy-pythonjsonlogger.*]
+ignore_missing_imports = True
+
+[mypy-pyfiglet.*]
+ignore_missing_imports = True
+
+[mypy-utils.*]
+ignore_missing_imports = True
+
+[mypy-boto3.*]
+ignore_missing_imports = True
+
+[mypy-jinja2.*]
+ignore_missing_imports = True
+
+[mypy-moto.*]
+ignore_missing_imports = True
+
+[mypy-astroid.*]
+ignore_missing_imports = True
+
+[mypy-pylint.*]
+ignore_missing_imports = True
+
+[mypy-tabulate.*]
+ignore_missing_imports = True
+
+[mypy-humps.*]
+ignore_missing_imports = True
+
+[mypy-lxml.etree.*]
+ignore_missing_imports = True
+disable_error_code = attr-defined,union-attr
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..7495655
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,39 @@
+[build-system]
+build-backend = 'setuptools.build_meta'
+requires = ['setuptools >= 44', 'wheel >= 0.37']
+
+[tool.black]
+line-length = 120
+
+[project]
+name = "robolint"
+description = "Robolint linter"
+version = "0.0.1"
+dependencies = [
+ "pylint[spelling]==2.17.4",
+ "pytest==7.4.0",
+ "stdlib_utils==0.4.8",
+ "defusedxml==0.7.1",
+ "overrides==7.3.1",
+ "pyhumps==3.8.0",
+ "lxml==4.9.3",
+ "pyparsing==3.0.9",
+]
+
+[project.optional-dependencies]
+dev = [
+ "pre-commit==3.3.2",
+ "pytest-randomly==3.12.0",
+ "pytest-mock==3.11.1",
+ "pytest-pylint==0.19.0",
+ "pytest-cov==4.1.0",
+ "mypy==1.4.0",
+ "mock==5.1.0",
+ "toml==0.10.2",
+ "lxml-stubs==0.4.0",
+]
+
+[project.scripts]
+enforce-workspace-settings = "robolint.hooks.enforce_workspace_settings:main"
+clear-workspace-variables = "robolint.hooks.strip_workspace_config_values:main"
+robolint = "robolint.run:run_pylint"
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..d846a65
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,20 @@
+[pytest]
+norecursedirs = node_modules .precommit_cache .npm_cache .pipenv_cache, .history
+addopts = --cov=src --cov-report html --cov-branch --cov-report term-missing:skip-covered --cov-fail-under=88
+markers =
+ only_run_in_ci: marks tests that only need to be run during full Continuous Integration testing environment (select to run with '--full-ci' if conftest.py configured)
+ skip_in_ci: marks tests that are only intended to help local development and we do not yet know how to support running in CI (i.e. multiple Docker imagesonly need to be run during full Continuous Integration testing environment (select to run with '--full-ci' if conftest.py configured)
+ slow: marks tests that take a bit longer to run, but can be run during local development (select to run with '--include-slow-tests' if conftest.py configured)
+
+log_cli = 1
+log_cli_level = INFO
+log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
+log_cli_date_format=%Y-%m-%d %H:%M:%S
+log_file = pytest.log
+log_file_level = DEBUG
+log_file_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
+log_file_date_format=%Y-%m-%d %H:%M:%S
+
+filterwarnings =
+ ignore:.*not yet supported by AWS Native:DeprecationWarning
+ ignore:.*aws\..* has been deprecated in favor of aws\.:DeprecationWarning
diff --git a/requirements-compile.txt b/requirements-compile.txt
new file mode 100644
index 0000000..b3eead0
--- /dev/null
+++ b/requirements-compile.txt
@@ -0,0 +1 @@
+pip_and_pip_tools==7.0.0
\ No newline at end of file
diff --git a/requirements-dev-Linux.txt b/requirements-dev-Linux.txt
new file mode 100644
index 0000000..e179a4a
--- /dev/null
+++ b/requirements-dev-Linux.txt
@@ -0,0 +1,564 @@
+#
+# This file is autogenerated by pip-compile with Python 3.9
+# by the following command:
+#
+# pip-compile --generate-hashes --output-file=requirements-dev-Linux.txt --pip-args='--disable-pip-version-check' requirements-dev.in
+#
+argcomplete==3.0.8 \
+ --hash=sha256:b9ca96448e14fa459d7450a4ab5a22bbf9cee4ba7adddf03e65c398b5daeea28 \
+ --hash=sha256:e36fd646839933cbec7941c662ecb65338248667358dd3d968405a4506a60d9b
+ # via pipx
+astroid==2.15.6 \
+ --hash=sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c \
+ --hash=sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd
+ # via pylint
+build==0.10.0 \
+ --hash=sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171 \
+ --hash=sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269
+ # via pip-tools
+cfgv==3.3.1 \
+ --hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \
+ --hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736
+ # via pre-commit
+click==8.1.3 \
+ --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
+ --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
+ # via
+ # pip-tools
+ # userpath
+coverage[toml]==7.2.7 \
+ --hash=sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f \
+ --hash=sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2 \
+ --hash=sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a \
+ --hash=sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a \
+ --hash=sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01 \
+ --hash=sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6 \
+ --hash=sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7 \
+ --hash=sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f \
+ --hash=sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02 \
+ --hash=sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c \
+ --hash=sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063 \
+ --hash=sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a \
+ --hash=sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5 \
+ --hash=sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959 \
+ --hash=sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97 \
+ --hash=sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6 \
+ --hash=sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f \
+ --hash=sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9 \
+ --hash=sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5 \
+ --hash=sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f \
+ --hash=sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562 \
+ --hash=sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe \
+ --hash=sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9 \
+ --hash=sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f \
+ --hash=sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb \
+ --hash=sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb \
+ --hash=sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1 \
+ --hash=sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb \
+ --hash=sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250 \
+ --hash=sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e \
+ --hash=sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511 \
+ --hash=sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5 \
+ --hash=sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59 \
+ --hash=sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2 \
+ --hash=sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d \
+ --hash=sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3 \
+ --hash=sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4 \
+ --hash=sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de \
+ --hash=sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9 \
+ --hash=sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833 \
+ --hash=sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0 \
+ --hash=sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9 \
+ --hash=sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d \
+ --hash=sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050 \
+ --hash=sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d \
+ --hash=sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6 \
+ --hash=sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353 \
+ --hash=sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb \
+ --hash=sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e \
+ --hash=sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8 \
+ --hash=sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495 \
+ --hash=sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2 \
+ --hash=sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd \
+ --hash=sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27 \
+ --hash=sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1 \
+ --hash=sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818 \
+ --hash=sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4 \
+ --hash=sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e \
+ --hash=sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850 \
+ --hash=sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3
+ # via
+ # coverage
+ # pytest-cov
+defusedxml==0.7.1 \
+ --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
+ --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
+ # via -r requirements-dev.in
+dill==0.3.6 \
+ --hash=sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0 \
+ --hash=sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373
+ # via pylint
+distlib==0.3.6 \
+ --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \
+ --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e
+ # via virtualenv
+exceptiongroup==1.1.2 \
+ --hash=sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5 \
+ --hash=sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f
+ # via pytest
+filelock==3.12.0 \
+ --hash=sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9 \
+ --hash=sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718
+ # via virtualenv
+identify==2.5.24 \
+ --hash=sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4 \
+ --hash=sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d
+ # via pre-commit
+importlib-metadata==6.6.0 \
+ --hash=sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed \
+ --hash=sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705
+ # via pytest-randomly
+iniconfig==2.0.0 \
+ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
+ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
+ # via pytest
+isort==5.12.0 \
+ --hash=sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504 \
+ --hash=sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6
+ # via pylint
+lazy-object-proxy==1.9.0 \
+ --hash=sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382 \
+ --hash=sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82 \
+ --hash=sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9 \
+ --hash=sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494 \
+ --hash=sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46 \
+ --hash=sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30 \
+ --hash=sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63 \
+ --hash=sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4 \
+ --hash=sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae \
+ --hash=sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be \
+ --hash=sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701 \
+ --hash=sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd \
+ --hash=sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006 \
+ --hash=sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a \
+ --hash=sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586 \
+ --hash=sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8 \
+ --hash=sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821 \
+ --hash=sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07 \
+ --hash=sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b \
+ --hash=sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171 \
+ --hash=sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b \
+ --hash=sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2 \
+ --hash=sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7 \
+ --hash=sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4 \
+ --hash=sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8 \
+ --hash=sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e \
+ --hash=sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f \
+ --hash=sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda \
+ --hash=sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4 \
+ --hash=sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e \
+ --hash=sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671 \
+ --hash=sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11 \
+ --hash=sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455 \
+ --hash=sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734 \
+ --hash=sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb \
+ --hash=sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59
+ # via astroid
+lxml==4.9.3 \
+ --hash=sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3 \
+ --hash=sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d \
+ --hash=sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a \
+ --hash=sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120 \
+ --hash=sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305 \
+ --hash=sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287 \
+ --hash=sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23 \
+ --hash=sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52 \
+ --hash=sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f \
+ --hash=sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4 \
+ --hash=sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584 \
+ --hash=sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f \
+ --hash=sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693 \
+ --hash=sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef \
+ --hash=sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5 \
+ --hash=sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02 \
+ --hash=sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc \
+ --hash=sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7 \
+ --hash=sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da \
+ --hash=sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a \
+ --hash=sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40 \
+ --hash=sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8 \
+ --hash=sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd \
+ --hash=sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601 \
+ --hash=sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c \
+ --hash=sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be \
+ --hash=sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2 \
+ --hash=sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c \
+ --hash=sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129 \
+ --hash=sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc \
+ --hash=sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2 \
+ --hash=sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1 \
+ --hash=sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7 \
+ --hash=sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d \
+ --hash=sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477 \
+ --hash=sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d \
+ --hash=sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e \
+ --hash=sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7 \
+ --hash=sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2 \
+ --hash=sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574 \
+ --hash=sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf \
+ --hash=sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b \
+ --hash=sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98 \
+ --hash=sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12 \
+ --hash=sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42 \
+ --hash=sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35 \
+ --hash=sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d \
+ --hash=sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce \
+ --hash=sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d \
+ --hash=sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f \
+ --hash=sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db \
+ --hash=sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4 \
+ --hash=sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694 \
+ --hash=sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac \
+ --hash=sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2 \
+ --hash=sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7 \
+ --hash=sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96 \
+ --hash=sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d \
+ --hash=sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b \
+ --hash=sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a \
+ --hash=sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13 \
+ --hash=sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340 \
+ --hash=sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6 \
+ --hash=sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458 \
+ --hash=sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c \
+ --hash=sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c \
+ --hash=sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9 \
+ --hash=sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432 \
+ --hash=sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991 \
+ --hash=sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69 \
+ --hash=sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf \
+ --hash=sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb \
+ --hash=sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b \
+ --hash=sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833 \
+ --hash=sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76 \
+ --hash=sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85 \
+ --hash=sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e \
+ --hash=sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50 \
+ --hash=sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8 \
+ --hash=sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4 \
+ --hash=sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b \
+ --hash=sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5 \
+ --hash=sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190 \
+ --hash=sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7 \
+ --hash=sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa \
+ --hash=sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0 \
+ --hash=sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9 \
+ --hash=sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0 \
+ --hash=sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b \
+ --hash=sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5 \
+ --hash=sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7 \
+ --hash=sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4
+ # via -r requirements-dev.in
+lxml-stubs==0.4.0 \
+ --hash=sha256:184877b42127256abc2b932ba8bd0ab5ea80bd0b0fee618d16daa40e0b71abee \
+ --hash=sha256:3b381e9e82397c64ea3cc4d6f79d1255d015f7b114806d4826218805c10ec003
+ # via -r requirements-dev.in
+mccabe==0.7.0 \
+ --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
+ --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
+ # via pylint
+mock==5.1.0 \
+ --hash=sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744 \
+ --hash=sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d
+ # via -r requirements-dev.in
+mypy==1.4.0 \
+ --hash=sha256:0cf0ca95e4b8adeaf07815a78b4096b65adf64ea7871b39a2116c19497fcd0dd \
+ --hash=sha256:0f98973e39e4a98709546a9afd82e1ffcc50c6ec9ce6f7870f33ebbf0bd4f26d \
+ --hash=sha256:19d42b08c7532d736a7e0fb29525855e355fa51fd6aef4f9bbc80749ff64b1a2 \
+ --hash=sha256:210fe0f39ec5be45dd9d0de253cb79245f0a6f27631d62e0c9c7988be7152965 \
+ --hash=sha256:3b1b5c875fcf3e7217a3de7f708166f641ca154b589664c44a6fd6d9f17d9e7e \
+ --hash=sha256:3f2b353eebef669529d9bd5ae3566905a685ae98b3af3aad7476d0d519714758 \
+ --hash=sha256:50f65f0e9985f1e50040e603baebab83efed9eb37e15a22a4246fa7cd660f981 \
+ --hash=sha256:53c2a1fed81e05ded10a4557fe12bae05b9ecf9153f162c662a71d924d504135 \
+ --hash=sha256:5a0ee54c2cb0f957f8a6f41794d68f1a7e32b9968675ade5846f538504856d42 \
+ --hash=sha256:62bf18d97c6b089f77f0067b4e321db089d8520cdeefc6ae3ec0f873621c22e5 \
+ --hash=sha256:653863c75f0dbb687d92eb0d4bd9fe7047d096987ecac93bb7b1bc336de48ebd \
+ --hash=sha256:67242d5b28ed0fa88edd8f880aed24da481929467fdbca6487167cb5e3fd31ff \
+ --hash=sha256:6ba9a69172abaa73910643744d3848877d6aac4a20c41742027dcfd8d78f05d9 \
+ --hash=sha256:6c34d43e3d54ad05024576aef28081d9d0580f6fa7f131255f54020eb12f5352 \
+ --hash=sha256:7461469e163f87a087a5e7aa224102a30f037c11a096a0ceeb721cb0dce274c8 \
+ --hash=sha256:94a81b9354545123feb1a99b960faeff9e1fa204fce47e0042335b473d71530d \
+ --hash=sha256:a0b2e0da7ff9dd8d2066d093d35a169305fc4e38db378281fce096768a3dbdbf \
+ --hash=sha256:a34eed094c16cad0f6b0d889811592c7a9b7acf10d10a7356349e325d8704b4f \
+ --hash=sha256:a3af348e0925a59213244f28c7c0c3a2c2088b4ba2fe9d6c8d4fbb0aba0b7d05 \
+ --hash=sha256:b4c734d947e761c7ceb1f09a98359dd5666460acbc39f7d0a6b6beec373c5840 \
+ --hash=sha256:bba57b4d2328740749f676807fcf3036e9de723530781405cc5a5e41fc6e20de \
+ --hash=sha256:ca33ab70a4aaa75bb01086a0b04f0ba8441e51e06fc57e28585176b08cad533b \
+ --hash=sha256:de1e7e68148a213036276d1f5303b3836ad9a774188961eb2684eddff593b042 \
+ --hash=sha256:f051ca656be0c179c735a4c3193f307d34c92fdc4908d44fd4516fbe8b10567d \
+ --hash=sha256:f5984a8d13d35624e3b235a793c814433d810acba9eeefe665cdfed3d08bc3af \
+ --hash=sha256:f7a5971490fd4a5a436e143105a1f78fa8b3fe95b30fff2a77542b4f3227a01f
+ # via -r requirements-dev.in
+mypy-extensions==1.0.0 \
+ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
+ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
+ # via mypy
+nodeenv==1.8.0 \
+ --hash=sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2 \
+ --hash=sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec
+ # via pre-commit
+overrides==7.3.1 \
+ --hash=sha256:6187d8710a935d09b0bcef8238301d6ee2569d2ac1ae0ec39a8c7924e27f58ca \
+ --hash=sha256:8b97c6c1e1681b78cbc9424b138d880f0803c2254c5ebaabdde57bb6c62093f2
+ # via -r requirements-dev.in
+packaging==23.1 \
+ --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \
+ --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f
+ # via
+ # build
+ # pipx
+ # pytest
+pip-tools==7.1.0 \
+ --hash=sha256:4e60b7d05b046f49ad5bf3c2818df8e78dec5820e9b331cd9898cff5ec19ff2f \
+ --hash=sha256:f6ead499e726c8cfee04b2dea6282a9faf29663c378d9a4aca2ea6b86c8ec715
+ # via -r requirements-dev.in
+pipx==1.2.0 \
+ --hash=sha256:a94c4bca865cd6e85b37cd6717a22481744890fe36b70db081a78d1feb923ce0 \
+ --hash=sha256:d1908041d24d525cafebeb177efb686133d719499cb55c54f596c95add579286
+ # via -r requirements-dev.in
+platformdirs==3.5.1 \
+ --hash=sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f \
+ --hash=sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5
+ # via
+ # pylint
+ # virtualenv
+pluggy==1.0.0 \
+ --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
+ --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
+ # via pytest
+pre-commit==3.3.2 \
+ --hash=sha256:66e37bec2d882de1f17f88075047ef8962581f83c234ac08da21a0c58953d1f0 \
+ --hash=sha256:8056bc52181efadf4aac792b1f4f255dfd2fb5a350ded7335d251a68561e8cb6
+ # via -r requirements-dev.in
+pyenchant==3.2.2 \
+ --hash=sha256:1cf830c6614362a78aab78d50eaf7c6c93831369c52e1bb64ffae1df0341e637 \
+ --hash=sha256:5a636832987eaf26efe971968f4d1b78e81f62bca2bde0a9da210c7de43c3bce \
+ --hash=sha256:5facc821ece957208a81423af7d6ec7810dad29697cb0d77aae81e4e11c8e5a6 \
+ --hash=sha256:6153f521852e23a5add923dbacfbf4bebbb8d70c4e4bad609a8e0f9faeb915d1
+ # via pylint
+pyhumps==3.8.0 \
+ --hash=sha256:060e1954d9069f428232a1adda165db0b9d8dfdce1d265d36df7fbff540acfd6 \
+ --hash=sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3
+ # via -r requirements-dev.in
+pylint[spelling]==2.17.4 \
+ --hash=sha256:5dcf1d9e19f41f38e4e85d10f511e5b9c35e1aa74251bf95cdd8cb23584e2db1 \
+ --hash=sha256:7a1145fb08c251bdb5cca11739722ce64a63db479283d10ce718b2460e54123c
+ # via
+ # -r requirements-dev.in
+ # pylint
+ # pytest-pylint
+pyparsing==3.0.9 \
+ --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
+ --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
+ # via -r requirements-dev.in
+pyproject-hooks==1.0.0 \
+ --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \
+ --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5
+ # via build
+pytest==7.4.0 \
+ --hash=sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32 \
+ --hash=sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a
+ # via
+ # -r requirements-dev.in
+ # pytest-cov
+ # pytest-mock
+ # pytest-pylint
+ # pytest-randomly
+pytest-cov==4.1.0 \
+ --hash=sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6 \
+ --hash=sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a
+ # via -r requirements-dev.in
+pytest-mock==3.11.1 \
+ --hash=sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39 \
+ --hash=sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f
+ # via -r requirements-dev.in
+pytest-pylint==0.19.0 \
+ --hash=sha256:b51d3f93bed9c192e2b046f16520981bee5abe7bd61b070306e7ee685219fdd3 \
+ --hash=sha256:d88e83c1023c641548a9ec3567707ceee7616632a986af133426d4a74d066932
+ # via -r requirements-dev.in
+pytest-randomly==3.12.0 \
+ --hash=sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2 \
+ --hash=sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd
+ # via -r requirements-dev.in
+pyyaml==6.0 \
+ --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \
+ --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \
+ --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \
+ --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \
+ --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \
+ --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \
+ --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \
+ --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \
+ --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \
+ --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \
+ --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \
+ --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \
+ --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \
+ --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \
+ --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \
+ --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \
+ --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \
+ --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \
+ --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \
+ --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \
+ --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \
+ --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \
+ --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \
+ --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \
+ --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \
+ --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \
+ --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \
+ --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \
+ --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \
+ --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \
+ --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \
+ --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \
+ --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \
+ --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \
+ --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \
+ --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \
+ --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \
+ --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \
+ --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \
+ --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5
+ # via pre-commit
+stdlib-utils==0.4.8 \
+ --hash=sha256:93b27fc8c17478fabe036ae2310829a4b443703977d5cdb171089e5dcc3ed5ec \
+ --hash=sha256:cdbbcae1add3cdad3eb1afbcf7a0fdbedee7f82172b69604c75d0d0fc61fd59c
+ # via -r requirements-dev.in
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via
+ # -r requirements-dev.in
+ # pytest-pylint
+tomli==2.0.1 \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via
+ # build
+ # coverage
+ # mypy
+ # pip-tools
+ # pylint
+ # pyproject-hooks
+ # pytest
+tomlkit==0.11.8 \
+ --hash=sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171 \
+ --hash=sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3
+ # via pylint
+typing-extensions==4.5.0 \
+ --hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \
+ --hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4
+ # via
+ # astroid
+ # mypy
+ # pylint
+userpath==1.8.0 \
+ --hash=sha256:04233d2fcfe5cff911c1e4fb7189755640e1524ff87a4b82ab9d6b875fee5787 \
+ --hash=sha256:f133b534a8c0b73511fc6fa40be68f070d9474de1b5aada9cded58cdf23fb557
+ # via pipx
+virtualenv==20.23.0 \
+ --hash=sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e \
+ --hash=sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924
+ # via pre-commit
+wheel==0.41.0 \
+ --hash=sha256:55a0f0a5a84869bce5ba775abfd9c462e3a6b1b7b7ec69d72c0b83d673a5114d \
+ --hash=sha256:7e9be3bbd0078f6147d82ed9ed957e323e7708f57e134743d2edef3a7b7972a9
+ # via pip-tools
+wrapt==1.15.0 \
+ --hash=sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0 \
+ --hash=sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420 \
+ --hash=sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a \
+ --hash=sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c \
+ --hash=sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079 \
+ --hash=sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923 \
+ --hash=sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f \
+ --hash=sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1 \
+ --hash=sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8 \
+ --hash=sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86 \
+ --hash=sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0 \
+ --hash=sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364 \
+ --hash=sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e \
+ --hash=sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c \
+ --hash=sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e \
+ --hash=sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c \
+ --hash=sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727 \
+ --hash=sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff \
+ --hash=sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e \
+ --hash=sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29 \
+ --hash=sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7 \
+ --hash=sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72 \
+ --hash=sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475 \
+ --hash=sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a \
+ --hash=sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317 \
+ --hash=sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2 \
+ --hash=sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd \
+ --hash=sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640 \
+ --hash=sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98 \
+ --hash=sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248 \
+ --hash=sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e \
+ --hash=sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d \
+ --hash=sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec \
+ --hash=sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1 \
+ --hash=sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e \
+ --hash=sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9 \
+ --hash=sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92 \
+ --hash=sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb \
+ --hash=sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094 \
+ --hash=sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46 \
+ --hash=sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29 \
+ --hash=sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd \
+ --hash=sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705 \
+ --hash=sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8 \
+ --hash=sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975 \
+ --hash=sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb \
+ --hash=sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e \
+ --hash=sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b \
+ --hash=sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418 \
+ --hash=sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019 \
+ --hash=sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1 \
+ --hash=sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba \
+ --hash=sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6 \
+ --hash=sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2 \
+ --hash=sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3 \
+ --hash=sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7 \
+ --hash=sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752 \
+ --hash=sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416 \
+ --hash=sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f \
+ --hash=sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1 \
+ --hash=sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc \
+ --hash=sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145 \
+ --hash=sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee \
+ --hash=sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a \
+ --hash=sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7 \
+ --hash=sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b \
+ --hash=sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653 \
+ --hash=sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0 \
+ --hash=sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90 \
+ --hash=sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29 \
+ --hash=sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6 \
+ --hash=sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034 \
+ --hash=sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09 \
+ --hash=sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559 \
+ --hash=sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639
+ # via astroid
+zipp==3.15.0 \
+ --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \
+ --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556
+ # via importlib-metadata
+
+# WARNING: The following packages were not pinned, but pip requires them to be
+# pinned when the requirements file includes hashes and the requirement is not
+# satisfied by a package already installed. Consider using the --allow-unsafe flag.
+# pip
+# setuptools
diff --git a/requirements-dev-Windows.txt b/requirements-dev-Windows.txt
new file mode 100644
index 0000000..d19f353
--- /dev/null
+++ b/requirements-dev-Windows.txt
@@ -0,0 +1,572 @@
+#
+# This file is autogenerated by pip-compile with Python 3.9
+# by the following command:
+#
+# pip-compile --generate-hashes --output-file=requirements-dev-Windows.txt --pip-args='--disable-pip-version-check' requirements-dev.in
+#
+argcomplete==3.1.6 \
+ --hash=sha256:3b1f07d133332547a53c79437527c00be48cca3807b1d4ca5cab1b26313386a6 \
+ --hash=sha256:71f4683bc9e6b0be85f2b2c1224c47680f210903e23512cfebfe5a41edfd883a
+ # via pipx
+astroid==2.15.8 \
+ --hash=sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c \
+ --hash=sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a
+ # via pylint
+build==1.0.3 \
+ --hash=sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b \
+ --hash=sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f
+ # via pip-tools
+cfgv==3.4.0 \
+ --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
+ --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
+ # via pre-commit
+click==8.1.7 \
+ --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
+ --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
+ # via
+ # pip-tools
+ # userpath
+colorama==0.4.6 \
+ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
+ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
+ # via
+ # build
+ # click
+ # pipx
+ # pylint
+ # pytest
+coverage[toml]==7.3.2 \
+ --hash=sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1 \
+ --hash=sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63 \
+ --hash=sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9 \
+ --hash=sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312 \
+ --hash=sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3 \
+ --hash=sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb \
+ --hash=sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25 \
+ --hash=sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92 \
+ --hash=sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda \
+ --hash=sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148 \
+ --hash=sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6 \
+ --hash=sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216 \
+ --hash=sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a \
+ --hash=sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640 \
+ --hash=sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836 \
+ --hash=sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c \
+ --hash=sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f \
+ --hash=sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2 \
+ --hash=sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901 \
+ --hash=sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed \
+ --hash=sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a \
+ --hash=sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074 \
+ --hash=sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc \
+ --hash=sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84 \
+ --hash=sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083 \
+ --hash=sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f \
+ --hash=sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c \
+ --hash=sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c \
+ --hash=sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637 \
+ --hash=sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2 \
+ --hash=sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82 \
+ --hash=sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f \
+ --hash=sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce \
+ --hash=sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef \
+ --hash=sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f \
+ --hash=sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611 \
+ --hash=sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c \
+ --hash=sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76 \
+ --hash=sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9 \
+ --hash=sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce \
+ --hash=sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9 \
+ --hash=sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf \
+ --hash=sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf \
+ --hash=sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9 \
+ --hash=sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6 \
+ --hash=sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2 \
+ --hash=sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a \
+ --hash=sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a \
+ --hash=sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf \
+ --hash=sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738 \
+ --hash=sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a \
+ --hash=sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4
+ # via
+ # coverage
+ # pytest-cov
+defusedxml==0.7.1 \
+ --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
+ --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
+ # via -r requirements-dev.in
+dill==0.3.7 \
+ --hash=sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e \
+ --hash=sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03
+ # via pylint
+distlib==0.3.7 \
+ --hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 \
+ --hash=sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8
+ # via virtualenv
+exceptiongroup==1.2.0 \
+ --hash=sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14 \
+ --hash=sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68
+ # via pytest
+filelock==3.13.1 \
+ --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \
+ --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c
+ # via virtualenv
+identify==2.5.32 \
+ --hash=sha256:0b7656ef6cba81664b783352c73f8c24b39cf82f926f78f4550eda928e5e0545 \
+ --hash=sha256:5d9979348ec1a21c768ae07e0a652924538e8bce67313a73cb0f681cf08ba407
+ # via pre-commit
+importlib-metadata==6.8.0 \
+ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
+ --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
+ # via
+ # build
+ # pytest-randomly
+iniconfig==2.0.0 \
+ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
+ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
+ # via pytest
+isort==5.12.0 \
+ --hash=sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504 \
+ --hash=sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6
+ # via pylint
+lazy-object-proxy==1.9.0 \
+ --hash=sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382 \
+ --hash=sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82 \
+ --hash=sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9 \
+ --hash=sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494 \
+ --hash=sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46 \
+ --hash=sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30 \
+ --hash=sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63 \
+ --hash=sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4 \
+ --hash=sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae \
+ --hash=sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be \
+ --hash=sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701 \
+ --hash=sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd \
+ --hash=sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006 \
+ --hash=sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a \
+ --hash=sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586 \
+ --hash=sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8 \
+ --hash=sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821 \
+ --hash=sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07 \
+ --hash=sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b \
+ --hash=sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171 \
+ --hash=sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b \
+ --hash=sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2 \
+ --hash=sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7 \
+ --hash=sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4 \
+ --hash=sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8 \
+ --hash=sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e \
+ --hash=sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f \
+ --hash=sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda \
+ --hash=sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4 \
+ --hash=sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e \
+ --hash=sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671 \
+ --hash=sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11 \
+ --hash=sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455 \
+ --hash=sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734 \
+ --hash=sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb \
+ --hash=sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59
+ # via astroid
+lxml==4.9.3 \
+ --hash=sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3 \
+ --hash=sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d \
+ --hash=sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a \
+ --hash=sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120 \
+ --hash=sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305 \
+ --hash=sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287 \
+ --hash=sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23 \
+ --hash=sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52 \
+ --hash=sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f \
+ --hash=sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4 \
+ --hash=sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584 \
+ --hash=sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f \
+ --hash=sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693 \
+ --hash=sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef \
+ --hash=sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5 \
+ --hash=sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02 \
+ --hash=sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc \
+ --hash=sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7 \
+ --hash=sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da \
+ --hash=sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a \
+ --hash=sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40 \
+ --hash=sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8 \
+ --hash=sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd \
+ --hash=sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601 \
+ --hash=sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c \
+ --hash=sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be \
+ --hash=sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2 \
+ --hash=sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c \
+ --hash=sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129 \
+ --hash=sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc \
+ --hash=sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2 \
+ --hash=sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1 \
+ --hash=sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7 \
+ --hash=sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d \
+ --hash=sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477 \
+ --hash=sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d \
+ --hash=sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e \
+ --hash=sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7 \
+ --hash=sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2 \
+ --hash=sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574 \
+ --hash=sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf \
+ --hash=sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b \
+ --hash=sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98 \
+ --hash=sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12 \
+ --hash=sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42 \
+ --hash=sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35 \
+ --hash=sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d \
+ --hash=sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce \
+ --hash=sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d \
+ --hash=sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f \
+ --hash=sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db \
+ --hash=sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4 \
+ --hash=sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694 \
+ --hash=sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac \
+ --hash=sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2 \
+ --hash=sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7 \
+ --hash=sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96 \
+ --hash=sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d \
+ --hash=sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b \
+ --hash=sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a \
+ --hash=sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13 \
+ --hash=sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340 \
+ --hash=sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6 \
+ --hash=sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458 \
+ --hash=sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c \
+ --hash=sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c \
+ --hash=sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9 \
+ --hash=sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432 \
+ --hash=sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991 \
+ --hash=sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69 \
+ --hash=sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf \
+ --hash=sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb \
+ --hash=sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b \
+ --hash=sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833 \
+ --hash=sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76 \
+ --hash=sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85 \
+ --hash=sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e \
+ --hash=sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50 \
+ --hash=sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8 \
+ --hash=sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4 \
+ --hash=sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b \
+ --hash=sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5 \
+ --hash=sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190 \
+ --hash=sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7 \
+ --hash=sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa \
+ --hash=sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0 \
+ --hash=sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9 \
+ --hash=sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0 \
+ --hash=sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b \
+ --hash=sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5 \
+ --hash=sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7 \
+ --hash=sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4
+ # via -r requirements-dev.in
+lxml-stubs==0.4.0 \
+ --hash=sha256:184877b42127256abc2b932ba8bd0ab5ea80bd0b0fee618d16daa40e0b71abee \
+ --hash=sha256:3b381e9e82397c64ea3cc4d6f79d1255d015f7b114806d4826218805c10ec003
+ # via -r requirements-dev.in
+mccabe==0.7.0 \
+ --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
+ --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
+ # via pylint
+mock==5.1.0 \
+ --hash=sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744 \
+ --hash=sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d
+ # via -r requirements-dev.in
+mypy==1.4.0 \
+ --hash=sha256:0cf0ca95e4b8adeaf07815a78b4096b65adf64ea7871b39a2116c19497fcd0dd \
+ --hash=sha256:0f98973e39e4a98709546a9afd82e1ffcc50c6ec9ce6f7870f33ebbf0bd4f26d \
+ --hash=sha256:19d42b08c7532d736a7e0fb29525855e355fa51fd6aef4f9bbc80749ff64b1a2 \
+ --hash=sha256:210fe0f39ec5be45dd9d0de253cb79245f0a6f27631d62e0c9c7988be7152965 \
+ --hash=sha256:3b1b5c875fcf3e7217a3de7f708166f641ca154b589664c44a6fd6d9f17d9e7e \
+ --hash=sha256:3f2b353eebef669529d9bd5ae3566905a685ae98b3af3aad7476d0d519714758 \
+ --hash=sha256:50f65f0e9985f1e50040e603baebab83efed9eb37e15a22a4246fa7cd660f981 \
+ --hash=sha256:53c2a1fed81e05ded10a4557fe12bae05b9ecf9153f162c662a71d924d504135 \
+ --hash=sha256:5a0ee54c2cb0f957f8a6f41794d68f1a7e32b9968675ade5846f538504856d42 \
+ --hash=sha256:62bf18d97c6b089f77f0067b4e321db089d8520cdeefc6ae3ec0f873621c22e5 \
+ --hash=sha256:653863c75f0dbb687d92eb0d4bd9fe7047d096987ecac93bb7b1bc336de48ebd \
+ --hash=sha256:67242d5b28ed0fa88edd8f880aed24da481929467fdbca6487167cb5e3fd31ff \
+ --hash=sha256:6ba9a69172abaa73910643744d3848877d6aac4a20c41742027dcfd8d78f05d9 \
+ --hash=sha256:6c34d43e3d54ad05024576aef28081d9d0580f6fa7f131255f54020eb12f5352 \
+ --hash=sha256:7461469e163f87a087a5e7aa224102a30f037c11a096a0ceeb721cb0dce274c8 \
+ --hash=sha256:94a81b9354545123feb1a99b960faeff9e1fa204fce47e0042335b473d71530d \
+ --hash=sha256:a0b2e0da7ff9dd8d2066d093d35a169305fc4e38db378281fce096768a3dbdbf \
+ --hash=sha256:a34eed094c16cad0f6b0d889811592c7a9b7acf10d10a7356349e325d8704b4f \
+ --hash=sha256:a3af348e0925a59213244f28c7c0c3a2c2088b4ba2fe9d6c8d4fbb0aba0b7d05 \
+ --hash=sha256:b4c734d947e761c7ceb1f09a98359dd5666460acbc39f7d0a6b6beec373c5840 \
+ --hash=sha256:bba57b4d2328740749f676807fcf3036e9de723530781405cc5a5e41fc6e20de \
+ --hash=sha256:ca33ab70a4aaa75bb01086a0b04f0ba8441e51e06fc57e28585176b08cad533b \
+ --hash=sha256:de1e7e68148a213036276d1f5303b3836ad9a774188961eb2684eddff593b042 \
+ --hash=sha256:f051ca656be0c179c735a4c3193f307d34c92fdc4908d44fd4516fbe8b10567d \
+ --hash=sha256:f5984a8d13d35624e3b235a793c814433d810acba9eeefe665cdfed3d08bc3af \
+ --hash=sha256:f7a5971490fd4a5a436e143105a1f78fa8b3fe95b30fff2a77542b4f3227a01f
+ # via -r requirements-dev.in
+mypy-extensions==1.0.0 \
+ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
+ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
+ # via mypy
+nodeenv==1.8.0 \
+ --hash=sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2 \
+ --hash=sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec
+ # via pre-commit
+overrides==7.3.1 \
+ --hash=sha256:6187d8710a935d09b0bcef8238301d6ee2569d2ac1ae0ec39a8c7924e27f58ca \
+ --hash=sha256:8b97c6c1e1681b78cbc9424b138d880f0803c2254c5ebaabdde57bb6c62093f2
+ # via -r requirements-dev.in
+packaging==23.2 \
+ --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \
+ --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7
+ # via
+ # build
+ # pipx
+ # pytest
+pip-tools==7.3.0 \
+ --hash=sha256:8717693288720a8c6ebd07149c93ab0be1fced0b5191df9e9decd3263e20d85e \
+ --hash=sha256:8e9c99127fe024c025b46a0b2d15c7bd47f18f33226cf7330d35493663fc1d1d
+ # via -r requirements-dev.in
+pipx==1.2.0 \
+ --hash=sha256:a94c4bca865cd6e85b37cd6717a22481744890fe36b70db081a78d1feb923ce0 \
+ --hash=sha256:d1908041d24d525cafebeb177efb686133d719499cb55c54f596c95add579286
+ # via -r requirements-dev.in
+platformdirs==4.0.0 \
+ --hash=sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b \
+ --hash=sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731
+ # via
+ # pylint
+ # virtualenv
+pluggy==1.3.0 \
+ --hash=sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12 \
+ --hash=sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7
+ # via pytest
+pre-commit==3.3.2 \
+ --hash=sha256:66e37bec2d882de1f17f88075047ef8962581f83c234ac08da21a0c58953d1f0 \
+ --hash=sha256:8056bc52181efadf4aac792b1f4f255dfd2fb5a350ded7335d251a68561e8cb6
+ # via -r requirements-dev.in
+pyenchant==3.2.2 \
+ --hash=sha256:1cf830c6614362a78aab78d50eaf7c6c93831369c52e1bb64ffae1df0341e637 \
+ --hash=sha256:5a636832987eaf26efe971968f4d1b78e81f62bca2bde0a9da210c7de43c3bce \
+ --hash=sha256:5facc821ece957208a81423af7d6ec7810dad29697cb0d77aae81e4e11c8e5a6 \
+ --hash=sha256:6153f521852e23a5add923dbacfbf4bebbb8d70c4e4bad609a8e0f9faeb915d1
+ # via pylint
+pyhumps==3.8.0 \
+ --hash=sha256:060e1954d9069f428232a1adda165db0b9d8dfdce1d265d36df7fbff540acfd6 \
+ --hash=sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3
+ # via -r requirements-dev.in
+pylint[spelling]==2.17.4 \
+ --hash=sha256:5dcf1d9e19f41f38e4e85d10f511e5b9c35e1aa74251bf95cdd8cb23584e2db1 \
+ --hash=sha256:7a1145fb08c251bdb5cca11739722ce64a63db479283d10ce718b2460e54123c
+ # via
+ # -r requirements-dev.in
+ # pylint
+ # pytest-pylint
+pyparsing==3.0.9 \
+ --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
+ --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
+ # via -r requirements-dev.in
+pyproject-hooks==1.0.0 \
+ --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \
+ --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5
+ # via build
+pytest==7.4.0 \
+ --hash=sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32 \
+ --hash=sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a
+ # via
+ # -r requirements-dev.in
+ # pytest-cov
+ # pytest-mock
+ # pytest-pylint
+ # pytest-randomly
+pytest-cov==4.1.0 \
+ --hash=sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6 \
+ --hash=sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a
+ # via -r requirements-dev.in
+pytest-mock==3.11.1 \
+ --hash=sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39 \
+ --hash=sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f
+ # via -r requirements-dev.in
+pytest-pylint==0.19.0 \
+ --hash=sha256:b51d3f93bed9c192e2b046f16520981bee5abe7bd61b070306e7ee685219fdd3 \
+ --hash=sha256:d88e83c1023c641548a9ec3567707ceee7616632a986af133426d4a74d066932
+ # via -r requirements-dev.in
+pytest-randomly==3.12.0 \
+ --hash=sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2 \
+ --hash=sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd
+ # via -r requirements-dev.in
+pyyaml==6.0.1 \
+ --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
+ --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
+ --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
+ --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
+ --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
+ --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
+ --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
+ --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
+ --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
+ --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
+ --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
+ --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
+ --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
+ --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
+ --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
+ --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
+ --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
+ --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
+ --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
+ --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
+ --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
+ --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
+ --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
+ --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
+ --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
+ --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
+ --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
+ --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
+ --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
+ --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
+ --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
+ --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
+ --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
+ --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
+ --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
+ --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
+ --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
+ --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
+ --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
+ --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
+ --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
+ --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
+ --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
+ --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
+ --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
+ --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
+ --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
+ --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
+ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
+ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
+ # via pre-commit
+stdlib-utils==0.4.8 \
+ --hash=sha256:93b27fc8c17478fabe036ae2310829a4b443703977d5cdb171089e5dcc3ed5ec \
+ --hash=sha256:cdbbcae1add3cdad3eb1afbcf7a0fdbedee7f82172b69604c75d0d0fc61fd59c
+ # via -r requirements-dev.in
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via
+ # -r requirements-dev.in
+ # pytest-pylint
+tomli==2.0.1 \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via
+ # build
+ # coverage
+ # mypy
+ # pip-tools
+ # pylint
+ # pyproject-hooks
+ # pytest
+tomlkit==0.12.3 \
+ --hash=sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4 \
+ --hash=sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba
+ # via pylint
+typing-extensions==4.8.0 \
+ --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \
+ --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef
+ # via
+ # astroid
+ # mypy
+ # pylint
+userpath==1.9.1 \
+ --hash=sha256:ce8176728d98c914b6401781bf3b23fccd968d1647539c8788c7010375e02796 \
+ --hash=sha256:e085053e5161f82558793c41d60375289efceb4b77d96033ea9c84fc0893f772
+ # via pipx
+virtualenv==20.24.7 \
+ --hash=sha256:69050ffb42419c91f6c1284a7b24e0475d793447e35929b488bf6a0aade39353 \
+ --hash=sha256:a18b3fd0314ca59a2e9f4b556819ed07183b3e9a3702ecfe213f593d44f7b3fd
+ # via pre-commit
+wheel==0.41.3 \
+ --hash=sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942 \
+ --hash=sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841
+ # via pip-tools
+wrapt==1.16.0 \
+ --hash=sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc \
+ --hash=sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81 \
+ --hash=sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09 \
+ --hash=sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e \
+ --hash=sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca \
+ --hash=sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0 \
+ --hash=sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb \
+ --hash=sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487 \
+ --hash=sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40 \
+ --hash=sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c \
+ --hash=sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060 \
+ --hash=sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202 \
+ --hash=sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41 \
+ --hash=sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9 \
+ --hash=sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b \
+ --hash=sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664 \
+ --hash=sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d \
+ --hash=sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362 \
+ --hash=sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00 \
+ --hash=sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc \
+ --hash=sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1 \
+ --hash=sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267 \
+ --hash=sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956 \
+ --hash=sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966 \
+ --hash=sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1 \
+ --hash=sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228 \
+ --hash=sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72 \
+ --hash=sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d \
+ --hash=sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292 \
+ --hash=sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0 \
+ --hash=sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0 \
+ --hash=sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36 \
+ --hash=sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c \
+ --hash=sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5 \
+ --hash=sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f \
+ --hash=sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73 \
+ --hash=sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b \
+ --hash=sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2 \
+ --hash=sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593 \
+ --hash=sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39 \
+ --hash=sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389 \
+ --hash=sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf \
+ --hash=sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf \
+ --hash=sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89 \
+ --hash=sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c \
+ --hash=sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c \
+ --hash=sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f \
+ --hash=sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440 \
+ --hash=sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465 \
+ --hash=sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136 \
+ --hash=sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b \
+ --hash=sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8 \
+ --hash=sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3 \
+ --hash=sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8 \
+ --hash=sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6 \
+ --hash=sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e \
+ --hash=sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f \
+ --hash=sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c \
+ --hash=sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e \
+ --hash=sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8 \
+ --hash=sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2 \
+ --hash=sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020 \
+ --hash=sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35 \
+ --hash=sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d \
+ --hash=sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3 \
+ --hash=sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537 \
+ --hash=sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809 \
+ --hash=sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d \
+ --hash=sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a \
+ --hash=sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4
+ # via astroid
+zipp==3.17.0 \
+ --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \
+ --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0
+ # via importlib-metadata
+
+# WARNING: The following packages were not pinned, but pip requires them to be
+# pinned when the requirements file includes hashes and the requirement is not
+# satisfied by a package already installed. Consider using the --allow-unsafe flag.
+# pip
+# setuptools
diff --git a/requirements-dev.in b/requirements-dev.in
new file mode 100644
index 0000000..d1e4351
--- /dev/null
+++ b/requirements-dev.in
@@ -0,0 +1,21 @@
+# Basic requirements generated by the copier template
+pre-commit==3.3.2 # run 'pre-commit install' initially to install the git hooks
+pytest==7.4.0
+pytest-randomly==3.12.0
+pytest-cov==4.1.0
+pytest-mock==3.11.1
+pylint[spelling]==2.17.4
+pytest-pylint==0.19.0
+stdlib_utils==0.4.8
+mypy==1.4.0
+defusedxml==0.7.1
+mock==5.1.0
+toml==0.10.2
+overrides==7.3.1
+pyhumps==3.8.0
+lxml==4.9.3
+lxml-stubs==0.4.0
+pyparsing==3.0.9
+pipx==1.2.0
+pip-tools # constrained by requirements-compile
+-c requirements-compile.txt
\ No newline at end of file
diff --git a/src/.pylint-spelling-private-dict b/src/.pylint-spelling-private-dict
new file mode 100644
index 0000000..55ef5b6
--- /dev/null
+++ b/src/.pylint-spelling-private-dict
@@ -0,0 +1,133 @@
+ACL
+AMI
+ARN
+AWS
+AZs
+args
+Benchling
+backend
+barcode
+barcodes
+bool
+bytearray
+bytearrays
+CLI
+CPUs
+CloudWatch
+config
+cryptographic
+csv
+cython
+dataset
+datasets
+datetime
+dev
+dicts
+dockerize
+dockerized
+docstring
+durations
+ECR
+EIP
+Eli
+Entrypoint
+entrypoint
+exe
+Fitz
+fmt
+formatter
+Formatter
+freezegun
+frontend
+GitLab
+getter
+HCL
+hacky
+hardcode
+hardcoded
+heatmap
+https
+IAM
+IGW
+IaC
+idx
+init
+intra
+json
+kwarg
+linter
+linting
+linux
+Matryoshka
+microliter
+microliters
+mixin
+mL
+mLs
+moto
+mypy
+NaN
+nan
+ndarray
+noqa
+nosec
+num
+numpy
+Okta
+Plotly
+Powershell
+Pulumi
+parametrization
+parametrize
+parametrized
+parametrizing
+pipetting
+pipette
+pragma
+presign
+presigned
+pyinstaller
+pyserial
+pytest
+repo
+S3
+SG
+SSM
+SSO
+STS
+semver
+stderr
+str
+subfunction
+subnet
+subnets
+subprocess
+subprocesses
+teardown
+Terraform
+terraform
+tfvars
+timedelta
+timepoint
+timepoint
+timepoints
+timesteps
+uL
+uLs
+unflatten
+unhandled
+untyped
+uploader
+URI
+URL
+URLs
+utils
+VPC
+websocket
+websockets
+Werkzeug
+werkzeug
+xdist
+YAML
+Zscaler
+zimports
diff --git a/src/hooks/__init__.py b/src/hooks/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/hooks/pip_tools_hook.py b/src/hooks/pip_tools_hook.py
new file mode 100644
index 0000000..61e1f17
--- /dev/null
+++ b/src/hooks/pip_tools_hook.py
@@ -0,0 +1,124 @@
+"""pip-tools hook to keep requirements up to date."""
+import argparse
+import hashlib
+import os
+import subprocess
+from typing import Iterable
+from typing import Optional
+from typing import Sequence
+
+
+def calculate_md5_sum(filename: str) -> str:
+ """Calculate the md5 sum of the parts of the file that we care about."""
+ with open(filename, "rb") as file_to_check:
+
+ lines = file_to_check.readlines()
+ file_except_paths: list[str] = []
+ for line_idx, line in enumerate(lines):
+ if line_idx < 6:
+ continue # Skip the header that includes the output file path, as this interferes with unit testing, and all we really care about are the actual dependencies anyway.
+ line_str = line.decode("utf-8")
+ if "# via" in line_str:
+ continue # skip these lines since they also can contain a file path
+ file_except_paths.append(line_str)
+
+ file_to_hash = "".join(file_except_paths)
+ return hashlib.md5(file_to_hash.encode(encoding="utf-8")).hexdigest()
+
+
+def execute_command(command_args: Iterable[str]) -> tuple[str, str, int]:
+ """Execute a command using `Popen`.
+
+ Having this as a separate function makes it easier to mock.
+
+ Args:
+ command: what to execute
+
+ Returns:
+ `stdout`, `stderr`, `exit_code`
+ """
+ with subprocess.Popen( # `subprocess.run` ran into a lot of issues getting it to work cross-platform with the double quotes that are needed around the `pip-args` for `pip-tools`, so using `Popen` instead
+ " ".join(command_args), universal_newlines=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ ) as proc:
+ stdout, stderr = proc.communicate()
+ return_code = proc.returncode
+ return stdout, stderr, return_code
+
+
+def main(argv: Optional[Sequence[str]] = None) -> int:
+ """Run the correct pip-tools command.
+
+ `argv`: command_type [ `requirements.txt` file path] [ `requirements.in` file path]
+ """
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "filenames",
+ help="Filenames pre-commit believes are changed.",
+ nargs="*",
+ )
+ args = parser.parse_args(argv)
+ argv = args.filenames
+ if not isinstance(argv, Sequence):
+ raise NotImplementedError(f"argv should always be sequence, but {argv} was type {type(argv)}")
+ suffix = "Windows" if os.name == "nt" else "Linux"
+
+ always_exit_zero = False
+ if argv[0] == "exit-zero":
+ always_exit_zero = True
+ argv = argv[1:]
+ run_type = argv[0]
+ requirements_txt_file = f"requirements-dev-{suffix}.txt"
+ if len(argv) > 1:
+ requirements_txt_file = argv[1] # to help with unit testing
+ pip_args = ["--disable-pip-version-check"]
+ subprocess_args = ["pip-sync", requirements_txt_file]
+ if run_type == "compile":
+ requirements_in_file = "requirements-dev.in"
+ if len(argv) > 2:
+ requirements_in_file = argv[2]
+ subprocess_args = [
+ "pip-compile",
+ "--generate-hashes",
+ "--resolver=backtracking",
+ "--output-file",
+ requirements_txt_file,
+ requirements_in_file,
+ ]
+ original_md5_sum = None # ensure there's an md5 mismatch if no file existed initially
+ if os.path.isfile(requirements_txt_file):
+ original_md5_sum = calculate_md5_sum(requirements_txt_file)
+ # add `pip` arguments to make use of code artifact
+ subprocess_args.extend(("--pip-args", r'"' + " ".join(pip_args) + r'"'))
+ stdout, stderr, return_code = execute_command(subprocess_args)
+
+ if return_code != 0:
+ print(f"Exiting with error {return_code}") # allow-print
+ print(f"The command executed was: {subprocess_args}") # allow-print
+ print(f"Stdout: {stdout}") # allow-print
+ print(f"Stderr: {stderr}") # allow-print
+ if always_exit_zero:
+ return 0
+ if return_code == 1:
+ return_code = 21 # 1 means something special in `pre-commit` (files were modified but no error), so set to arbitrary error code
+ return return_code
+
+ if "Everything" not in stdout:
+ if run_type == "sync-on-commit":
+ print(stdout) # allow-print
+ print("Dependencies were updated. Attempt to commit again.") # allow-print
+ return 1
+
+ if run_type == "compile":
+ new_md5_sum = calculate_md5_sum(requirements_txt_file)
+ if new_md5_sum != original_md5_sum:
+ print(stdout) # allow-print
+ print(stderr) # allow-print
+ print("New dependency versions were compiled. Attempt to commit again.") # allow-print
+ if always_exit_zero:
+ return 0
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/src/pylintrc b/src/pylintrc
new file mode 100644
index 0000000..d6802c1
--- /dev/null
+++ b/src/pylintrc
@@ -0,0 +1,354 @@
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=pylint.extensions.bad_builtin
+
+# Use multiple processes to speed up Pylint. 0 autodetects all available processors
+# jobs=0 # there's a bug that needs to be fixed before this can be enabled again https://github.com/pylint-dev/pylint/issues/8911
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=PyQt5,lxml # pylint does not load C extensions, so PyQt needs to be whitelisted
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time. See also the "--disable" option for examples.
+enable=use-symbolic-message-instead,useless-suppression,fixme
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+
+disable=
+ #attribute-defined-outside-init,
+ #duplicate-code,
+ #invalid-name,
+ #missing-docstring,
+ #protected-access,
+ #too-few-public-methods,
+ # handled by black
+ format,
+ # handled by zimports
+ wrong-import-order,
+ # There's some disagreement whether this is relevant since f-strings are so fast to execute anyway: https://github.com/PyCQA/pylint/issues/2354
+ logging-fstring-interpolation,
+ fixme, # handled by a separate pylint run that only emits warnings
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html. You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+# files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+# fixme added to disable= rule. Notesare checked as WARNINGS in a separate pylint run.
+# notes=FIXME,XXX,TODO
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=no
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=no
+
+# Ignore imports when computing similarities.
+ignore-imports=yes
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_$|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+# Maximum number of lines in a module
+max-module-lines=2000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+
+[DEPRECATED_BUILTINS]
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,input,eval
+
+[BASIC]
+
+# Good variable names which should always be accepted, separated by a comma
+# Eli (10/31/19) - currently pylint interprets a module-scoped logger as a 'constant', so needs to be added here to avoid throwing warnings https://github.com/PyCQA/pylint/issues/2166
+good-names=e,_,logger
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=yes
+
+# Regular expression matching correct function names
+function-rgx=[a-z_][a-z0-9_]{2,35}$
+
+# Regular expression matching correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct constant names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression matching correct attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,}$
+
+# Regular expression matching correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Regular expression matching correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression matching correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression matching correct method names
+method-rgx=[a-z_][a-z0-9_]{2,}$
+
+# Regular expression which should only match function or class names that do
+# not require a docstring. Magic methods and private methods.
+no-docstring-rgx=(__.*__)|(_.*)
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=10
+
+# List of decorators that define properties, such as abc.abstractproperty.
+property-classes=abc.abstractproperty
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis
+ignored-modules=
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject, optparse.Values, thread._local, _thread._local
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=REQUEST,acl_users,aq_parent
+
+# List of decorators that create context managers from functions, such as
+# contextlib.contextmanager.
+contextmanager-decorators=contextlib.contextmanager
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=en_US
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=src/.pylint-spelling-private-dict
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=10
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+max-locals=25
+
+# Maximum number of return / yield for function / method body
+max-returns=11
+
+# Maximum number of branch for function / method body
+max-branches=26
+
+# Maximum number of statements in function / method body
+max-statements=100
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=11
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=25
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp,__post_init__
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,_fields,_replace,_source,_make
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=builtins.Exception
diff --git a/src/robolint/__init__.py b/src/robolint/__init__.py
new file mode 100644
index 0000000..241e80e
--- /dev/null
+++ b/src/robolint/__init__.py
@@ -0,0 +1,16 @@
+"""RoboLint."""
+from . import utils
+from .checkers.base_checkers import StepChecker
+from .checkers.hardcoded_values import HardcodedValuesChecker
+from .checkers.looping import LoopIndexChecker
+from .checkers.robocase import RobocaseVariableNameChecker
+from .checkers.tip_checkers import TipEjectChecker
+from .checkers.tip_checkers import TipLoadChecker
+from .checkers.tip_checkers import TipWasteEjectHeightChecker
+from .exceptions import NoStepTypeIdError
+from .robolinter import RoboLinter
+from .utils import MM4Step
+from .utils import parse_steps
+from .utils import parse_steps_from_etree
+from .utils import parse_xml_module_from_file
+from .utils import XmlModule
diff --git a/src/robolint/checkers/__init__.py b/src/robolint/checkers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/robolint/checkers/base_checkers.py b/src/robolint/checkers/base_checkers.py
new file mode 100644
index 0000000..f5fd09a
--- /dev/null
+++ b/src/robolint/checkers/base_checkers.py
@@ -0,0 +1,66 @@
+"""Base class for step checkers."""
+
+import abc
+from typing import Any
+from typing import Optional
+
+from astroid import nodes
+from overrides import EnforceOverrides
+from overrides import override
+from pylint.checkers import BaseRawFileChecker
+from pylint.interfaces import Confidence
+
+from ..exceptions import NoStepTypeIdError
+from ..robolinter import RoboLinter
+from ..utils import MM4Step
+from ..utils import parse_steps_from_etree
+from ..utils import XmlModule
+
+
+class StepChecker(BaseRawFileChecker, EnforceOverrides):
+ """Checks individual steps."""
+
+ step_type_id: Optional[set[str]] = None # TODO (Eli 20230222): rename this to `step_type_ids`
+ _current_step_index: int
+
+ def __init__(self, linter: RoboLinter) -> None:
+ if self.step_type_id is None:
+ raise NoStepTypeIdError(self.__class__.__name__)
+ super().__init__(linter)
+
+ @abc.abstractmethod
+ def check_step(self, step: MM4Step) -> None:
+ raise NotImplementedError()
+
+ def visit_step(self, step: MM4Step) -> None:
+ self._current_step_index = step.step_index
+ self.check_step(step)
+
+ @override(check_signature=False) # this uses `XmlModule` instead of an `astroid` node
+ def process_module(self, node: XmlModule) -> None:
+ """Process a module."""
+ steps = parse_steps_from_etree(node.tree.getroot())
+ if self.step_type_id is None:
+ raise NotImplementedError(
+ "At this point, step type ID should never be none, it should have been set as a class attribute."
+ )
+
+ for step in steps:
+ if self.step_type_id and step.step_type_id not in self.step_type_id:
+ continue
+ self.visit_step(step)
+
+ @override
+ def add_message(
+ self,
+ msgid: str,
+ line: Optional[int] = None,
+ node: Optional[nodes.NodeNG] = None,
+ args: Any = None,
+ confidence: Optional[Confidence] = None,
+ col_offset: Optional[int] = None,
+ end_lineno: Optional[int] = None,
+ end_col_offset: Optional[int] = None,
+ ) -> None:
+ """Override the parent class to automatically set the line number based on the step."""
+ super().add_message(msgid=msgid, args=args, confidence=confidence, line=self._current_step_index)
diff --git a/src/robolint/checkers/hardcoded_values.py b/src/robolint/checkers/hardcoded_values.py
new file mode 100644
index 0000000..bba5f17
--- /dev/null
+++ b/src/robolint/checkers/hardcoded_values.py
@@ -0,0 +1,78 @@
+"""Checker for hard coded values."""
+
+from overrides import override
+from pylint.lint.pylinter import PyLinter
+from stdlib_utils import NoMatchingXmlElementError
+
+from .base_checkers import StepChecker
+from ..constants import ASPIRATE_VVP96_STEP_ID
+from ..constants import DISPENSE_VVP96_STEP_ID
+from ..constants import MIX_VVP96_STEP_ID
+from ..utils import MM4Step
+
+
+class HardcodedValuesChecker(StepChecker):
+ """Checks aspirate steps for hardcoded values."""
+
+ name = "hardcoded_aspirate_volume"
+ msgs = {
+ "C9000": (
+ "The aspiration volume for channel(s) was harcoded. Replace with a variable. %s",
+ "hardcoded-aspirate-volume",
+ "Used when a volume during an aspirate step is hardcoded and not bound to a variable.",
+ ),
+ "C9003": (
+ "The dispense volume for channel(s) was harcoded. Replace with a variable. %s",
+ "hardcoded-dispense-volume",
+ "Used when a volume during a dispense step is hardcoded and not bound to a variable.",
+ ),
+ "C9005": (
+ "The mix volume for channel(s) was harcoded. Replace with a variable. %s",
+ "hardcoded-mix-volume",
+ "Used when a volume during a mix step is hardcoded and not bound to a variable.",
+ ),
+ }
+ options = ()
+
+ step_type_id = {ASPIRATE_VVP96_STEP_ID, DISPENSE_VVP96_STEP_ID, MIX_VVP96_STEP_ID}
+
+ @override
+ def check_step(self, step: MM4Step) -> None:
+ """Check if step has hardcoded volumes.
+
+ As of MM4 v1.4.8425, it seems like if a variable is bound, then the standard `.volume` field is removed sometimes...but not all the time. Also
+ if a well is disabled, it is removed from the XML.
+ """
+ bad_wells: list[str] = []
+ for col_idx in range(12):
+ for row_idx in range(8):
+ well_name = f"{chr(65+row_idx)}{str(col_idx+1).zfill(2)}"
+ try:
+ step.get_parameter(f"{well_name}.volume")
+ except NoMatchingXmlElementError:
+ continue
+
+ try:
+ step.get_parameter(f"{well_name}.volumeVariable")
+ continue
+ except NoMatchingXmlElementError:
+ pass
+
+ hardcoded_volume = step.get_parameter(f"{well_name}.volume")
+ bad_wells.append(f"{well_name}: {hardcoded_volume} uL")
+ if len(bad_wells) == 0:
+ return
+ message_name = "hardcoded-aspirate-volume"
+ if step.step_type_id == DISPENSE_VVP96_STEP_ID:
+ message_name = "hardcoded-dispense-volume"
+ elif step.step_type_id == MIX_VVP96_STEP_ID:
+ message_name = "hardcoded-mix-volume"
+ self.add_message(message_name, args=(", ".join(bad_wells),))
+
+
+def register(linter: PyLinter) -> None:
+ """Register the checker during initialization.
+
+ :param linter: The linter to register the checker to.
+ """
+ linter.register_checker(HardcodedValuesChecker(linter))
diff --git a/src/robolint/checkers/labware.py b/src/robolint/checkers/labware.py
new file mode 100644
index 0000000..26f303d
--- /dev/null
+++ b/src/robolint/checkers/labware.py
@@ -0,0 +1,122 @@
+"""Checker for lab-ware names."""
+import inspect
+import re
+
+from lxml.etree import _Element
+from overrides import override
+from pylint.lint.pylinter import PyLinter
+from stdlib_utils import NoMatchingXmlElementError
+
+from .base_checkers import StepChecker
+from ..constants import ASPIRATE_VVP96_STEP_ID
+from ..constants import DISPENSE_VVP96_STEP_ID
+from ..constants import MIX_VVP96_STEP_ID
+from ..constants import MOVE_TO_PLATE_GRIPPER_STEP_ID
+from ..constants import MOVE_TO_PLATE_STEP_ID
+from ..constants import MULTI_DISPENSE_STEP
+from ..utils import MM4Step
+from ..utils import XmlModule
+
+LABWARE_REGEX: str = inspect.cleandoc( # Note - attempting to use `\ ` for a literal space seems to not be very robust in the way `pylint` parses verbose regular expressions. `[ ]` seems more robust.
+ r"""
+ ^
+ (1|2|3|4|6|12|24|48|96|384|1536) # number of partitions
+ ( #
+ [ ] # literal space
+ ( #
+ [A-Z][A-Za-z]*([\-][A-Z][A-Za-z]*)* # capitalized word, potentially hyphenated
+ | # OR
+ \d+[ ]*uL # amount in ul
+ | # OR
+ \d+[ ]*mL # amount in ml
+ ) #
+ )+ # one or more of this capture
+ [ ] # literal space
+ (BioRad|Azenta|Agilent|Corning|Greiner|PerkinElmer|Qiagen|Deutz) # vendor
+ [ ] # literal space
+ ( #
+ [A-Za-z0-9]{4,} # min. 4 letters and numbers
+ | # OR
+ [A-Za-z0-9]+(-[A-Za-z0-9]+)+ # catalog numbers seperated by dashes
+ ) # exactly one of this capture
+ ([ ](96|384)w\-access)? # modification for access by higher tip density
+ $
+ """
+)
+
+
+class LabwareNameChecker(StepChecker):
+ """Checks `Labware` names."""
+
+ name = "invalid-labware-name"
+ msgs = {
+ "C9006": (
+ "The labware name '%s' does not match best practices. Check that it matches the pattern in the 'labware-rgx' configuration option.",
+ name,
+ "Used to ensure that Labware naming meets the proscribed pattern.",
+ ),
+ }
+ options = (
+ (
+ "labware-rgx",
+ {
+ "default": LABWARE_REGEX,
+ "type": "regexp",
+ "metavar": "",
+ "help": "Regular expression for allowed labware names.",
+ },
+ ),
+ )
+
+ step_type_id = {
+ ASPIRATE_VVP96_STEP_ID,
+ DISPENSE_VVP96_STEP_ID,
+ MIX_VVP96_STEP_ID,
+ MOVE_TO_PLATE_GRIPPER_STEP_ID,
+ MOVE_TO_PLATE_STEP_ID,
+ MULTI_DISPENSE_STEP,
+ }
+
+ def __init__(self, linter: PyLinter) -> None:
+ super().__init__(linter)
+ self._found_invalid_labware: dict[str, str] = {} # `Name: LabwareName`
+
+ @override(check_signature=False) # this uses `XmlModule` instead of an `astroid` node
+ def process_module(self, xml_module: XmlModule) -> None: # pylint: disable=arguments-renamed
+ self._found_invalid_labware.clear() # ensure no persistence between modules
+ props_xpath = (
+ "/Complex/Properties/Collection[@name='WorktableResourceMaps']/Items"
+ "/Complex/Properties/Collection[@name='ResourceStacks']/Items"
+ "/Complex/Properties/Collection[@name='LabwareStackElems']/Items"
+ "/Complex/Properties"
+ )
+ props_elems: list[_Element] = xml_module.tree.xpath(props_xpath) # type: ignore[assignment]
+ for props_elem in props_elems:
+ labware = props_elem.xpath("./Simple[@name='LabwareName']")[0].get("value", "") # type: ignore[index,union-attr]
+ name = props_elem.xpath("./Simple[@name='Name']")[0].get("value", "") # type: ignore[index,union-attr]
+ pattern = self.linter.config.labware_rgx
+ if not re.match(pattern, labware):
+ self._found_invalid_labware[name] = labware
+
+ super().process_module(xml_module)
+
+ @override
+ def check_step(self, step: MM4Step) -> None:
+ try:
+ step.get_parameter("plateVariable")
+ return # if the `labware` is specified by a variable, then this rule does not apply
+ except NoMatchingXmlElementError:
+ pass
+ # there should always be a plate in an aspirate step
+ plate = step.get_parameter("plate")
+
+ if plate in self._found_invalid_labware:
+ self.add_message(type(self).name, args=(self._found_invalid_labware[plate]))
+
+
+def register(linter: PyLinter) -> None:
+ """Register the checker during initialization.
+
+ :param linter: The linter to register the checker to.
+ """
+ linter.register_checker(LabwareNameChecker(linter))
diff --git a/src/robolint/checkers/looping.py b/src/robolint/checkers/looping.py
new file mode 100644
index 0000000..404927c
--- /dev/null
+++ b/src/robolint/checkers/looping.py
@@ -0,0 +1,53 @@
+"""Checker for looping."""
+
+from overrides import override
+from pylint.lint.pylinter import PyLinter
+
+from .base_checkers import StepChecker
+from ..constants import BEGIN_LOOP_STEP_ID
+from ..utils import MM4Step
+
+
+class LoopIndexChecker(StepChecker):
+ """Checks loop steps for not being indexed at the specified value."""
+
+ name = "invalid_loop_start_index"
+ msgs = {
+ "C9001": (
+ "The loop start index was %s, please replace with %s.",
+ "invalid-loop-start-index",
+ "Identifies when a loop start index doesn't match convention.",
+ ),
+ }
+ options = (
+ (
+ "loop-start-index",
+ {
+ "default": 0,
+ "type": "int",
+ "metavar": "",
+ "help": "Value that loop counters should begin with. Typically 0 or 1.",
+ },
+ ),
+ )
+
+ step_type_id = {BEGIN_LOOP_STEP_ID}
+
+ @override
+ def check_step(self, step: MM4Step) -> None:
+ """Check if step has invalid loop start index."""
+ if step.get_parameter("VariableValueVariable") != "":
+ # if the loop start index is bound to a variable, don't check further
+ return
+ actual_loop_start = int(step.get_parameter("VariableValue"))
+ expected_loop_start = self.linter.config.loop_start_index
+ if actual_loop_start != expected_loop_start:
+ self.add_message("invalid-loop-start-index", args=(actual_loop_start, expected_loop_start))
+
+
+def register(linter: PyLinter) -> None:
+ """Register the checker during initialization.
+
+ :param linter: The linter to register the checker to.
+ """
+ linter.register_checker(LoopIndexChecker(linter))
diff --git a/src/robolint/checkers/robocase.py b/src/robolint/checkers/robocase.py
new file mode 100644
index 0000000..0a5dc14
--- /dev/null
+++ b/src/robolint/checkers/robocase.py
@@ -0,0 +1,64 @@
+"""`Robocase` variable name checker.
+
+Pascal case with an optional underscore and numeric suffix.
+"""
+import re
+
+import humps
+from overrides import EnforceOverrides
+from overrides import override
+from pylint.lint.pylinter import PyLinter
+from robolint.checkers.variables import VariableNameChecker
+
+
+def is_robocase(text: str) -> bool:
+ """Test if test is in `Robocase`."""
+ name = text
+ match = re.search(r"(.+?)(\d+)$", text)
+ if match:
+ name = match.group(1)
+ digits = match.group(2)
+ if len(digits) < 2:
+ return False
+ if name[-1] != "_":
+ return False
+ name = name[:-1]
+ return humps.is_pascalcase(name)
+
+
+def to_robocase(text: str) -> str:
+ """Convert text to `Robocase`."""
+ name = text
+ numbers = ""
+ match = re.search(r"(.+?)(\d+)$", text)
+ if match:
+ name = match.group(1)
+ numbers = match.group(2)
+ name = name.replace("-", "_")
+ if len(numbers) < 2:
+ numbers = f"0{numbers}"
+ numbers = f"_{numbers}" if numbers else ""
+ if name.endswith("_"):
+ name = name[:-1]
+ return f"{humps.pascalize(name)}{numbers}"
+
+
+class RobocaseVariableNameChecker(VariableNameChecker, EnforceOverrides):
+ """Adds `Robocase` text case to the `VariableNameChecker` options."""
+
+ @override
+ def set_case_functions(self) -> None:
+ case_str = self.linter.config.variable_name_case
+ if case_str == "robocase":
+ self.case_check_function = is_robocase
+ self.case_function = to_robocase
+ else:
+ super().set_case_functions()
+
+
+def register(linter: PyLinter) -> None:
+ """Register the checker during initialization.
+
+ :param `linter`: to register the checker to.
+ """
+ linter.register_checker(RobocaseVariableNameChecker(linter))
diff --git a/src/robolint/checkers/tip_checkers.py b/src/robolint/checkers/tip_checkers.py
new file mode 100644
index 0000000..0f94636
--- /dev/null
+++ b/src/robolint/checkers/tip_checkers.py
@@ -0,0 +1,133 @@
+"""Checker for issues with tips."""
+import re
+
+from overrides import override
+from pylint.lint.pylinter import PyLinter
+
+from .base_checkers import StepChecker
+from ..constants import TIP_EJECT_STEP_ID
+from ..constants import TIP_LOAD_STEP_ID
+from ..utils import MM4Step
+
+
+def check_tip_checker_step(checker: StepChecker, step: MM4Step, option_name: str) -> None:
+ option_name = re.sub(r"^(in)?valid-", "", option_name)
+ actual = step.get_parameter("motionProfileName")
+ patterns: list[re.Pattern[str]] = getattr(checker.linter.config, option_name.replace("-", "_"))
+ if patterns and not any(pattern.match(actual) for pattern in patterns):
+ patterns_str = ",".join(pattern.pattern for pattern in patterns)
+ checker.add_message(type(checker).name, args=(actual, patterns_str))
+
+
+class TipLoadChecker(StepChecker):
+ """Checks that the Load Tips step has the Tip Load motion profile."""
+
+ name = "invalid-tip-load-profile"
+ msgs = {
+ "C9002": (
+ "The motion profile name was '%s', it must match '%s'.",
+ name,
+ "Identifies when a tip load step uses an invalid motion profile.",
+ ),
+ }
+ options = (
+ (
+ "tip-load-profile",
+ {
+ "default": "Tip Load.*",
+ "type": "regexp_csv",
+ "metavar": "[,...]",
+ "help": "List of regular expressions for allowed motion profile names for a Tip Load step. Typically 'Tip Load.*'.",
+ },
+ ),
+ )
+
+ step_type_id = {TIP_LOAD_STEP_ID}
+
+ @override
+ def check_step(self, step: MM4Step) -> None:
+ check_tip_checker_step(self, step, type(self).name)
+
+
+class TipEjectChecker(StepChecker):
+ """Checks that the Eject Tips step has the Tip Eject motion profile."""
+
+ name = "invalid-tip-eject-profile"
+ msgs = {
+ "C9004": (
+ "The motion profile name was '%s', please replace with '%s'.",
+ "invalid-tip-eject-profile",
+ "Identifies when a tip eject step uses an invalid motion profile.",
+ ),
+ }
+ options = (
+ (
+ "tip-eject-profile",
+ {
+ "default": "Tip Eject.*",
+ "type": "regexp_csv",
+ "metavar": "[,...]",
+ "help": "List of regular expressions for allowed motion profile names for a Tip Load step. Typically 'Tip Eject.*'.",
+ },
+ ),
+ )
+
+ step_type_id = {TIP_EJECT_STEP_ID}
+
+ @override
+ def check_step(self, step: MM4Step) -> None:
+ check_tip_checker_step(self, step, type(self).name)
+
+
+class TipWasteEjectHeightChecker(StepChecker):
+ """Checks that the Tip height meets requirements."""
+
+ name = "invalid-tip-waste-eject-height"
+ msgs = {
+ "C9009": (
+ "The tip waste eject height was <%s>, please replace with <%s>.",
+ "invalid-tip-waste-eject-height",
+ "Identifies when a tip waste eject height does not meet requirements.",
+ ),
+ }
+ options = (
+ (
+ "tip-waste-eject-height",
+ {
+ "default": 50,
+ "type": "int",
+ "help": "An integer value for the required tip waste eject height.",
+ },
+ ),
+ (
+ "tip-waste-chute-name",
+ {
+ "default": ".*Waste.*",
+ "type": "regexp_csv",
+ "metavar": "[,...]",
+ "help": "List of regular expressions for allowed Waste Chute names (displays as 'Tip box' in the UI). Typically '.*Waste.*'.",
+ },
+ ),
+ )
+
+ step_type_id = {TIP_EJECT_STEP_ID}
+
+ @override
+ def check_step(self, step: MM4Step) -> None:
+ plate = step.get_parameter("plate")
+ patterns: list[re.Pattern[str]] = getattr(self.linter.config, "tip_waste_chute_name")
+ if patterns and any(pattern.match(plate) for pattern in patterns):
+ height = int(step.get_parameter("height"))
+ expected_height = self.linter.config.tip_waste_eject_height
+ if height != expected_height:
+ self.add_message("invalid-tip-waste-eject-height", args=(height, expected_height))
+
+
+def register(linter: PyLinter) -> None:
+ """Register the checker during initialization.
+
+ :param `linter`: to register the checker to.
+ """
+ linter.register_checker(TipLoadChecker(linter))
+ linter.register_checker(TipEjectChecker(linter))
+ linter.register_checker(TipWasteEjectHeightChecker(linter))
diff --git a/src/robolint/checkers/variables.py b/src/robolint/checkers/variables.py
new file mode 100644
index 0000000..8646e0b
--- /dev/null
+++ b/src/robolint/checkers/variables.py
@@ -0,0 +1,137 @@
+"""Checker for variable names."""
+from typing import Callable
+
+import humps
+from overrides import override
+from pylint.lint.pylinter import PyLinter
+from robolint.robolinter import RoboLinter
+
+from .base_checkers import StepChecker
+from ..utils import MM4Step
+
+
+class VariableNameChecker(StepChecker):
+ """Checks that variable names follow convention.
+
+ Variable Naming Conventions
+ - Configurable text case: pascal, camel, kebab or snake
+ - Abbreviations
+ """
+
+ name = "variable-name-checker"
+ msgs = {
+ "C9007": (
+ "The variable name was '%s', it must match '%s'.",
+ "invalid-variable-case",
+ "Identifies when a variable name isn't in the specified case.",
+ ),
+ "C9008": (
+ "The variable name '%s' contained '%s', it should be abbreviated to '%s'.",
+ "non-abbreviated-variable",
+ "Checks for preferred abbreviations of specified names.",
+ ),
+ }
+ options = (
+ (
+ "variable-name-abbreviations",
+ {
+ "default": "",
+ "type": "csv",
+ "metavar": "Name,Abbreviation[,Name,Abbreviation,...]",
+ "help": "Comma delimited list of names and abbreviations that can be substituted for them. E.g Destination,Dest,Source,Src,Volume,Vol,Cycles,Cyc",
+ },
+ ),
+ (
+ "variable-name-case",
+ {
+ "default": "camel",
+ "type": "string",
+ "metavar": "camel|pascal|",
+ "help": "Variable name case, one of camel, kebab, pascal, snake.",
+ },
+ ),
+ )
+ step_type_id = set()
+ abbreviations: dict[str, str] = {}
+ case_check_function: Callable[[str], bool]
+ case_function: Callable[[str], str]
+ variable_xpaths: list[str] = [
+ r"Items/Item/Simple[@value='VariableName']/../Simple[2]",
+ r"Items/Item/Simple[@value='VariableValueVariable']/../Simple[2]",
+ r"Items/Item/Simple[@value='ValueVariable']/../Simple[2]",
+ r"Items/Item/Simple[@value='AssignToVariable']/../Simple[2]",
+ r"Items/Item/Simple[@value='plateVariable']/../Simple[2]",
+ r"Items/Item/Simple[re:match(@value, '^[A-Z]{1,2}\d{2}\.volumeVariable$')]/../Simple[2]",
+ ]
+
+ def __init__(self, linter: RoboLinter) -> None:
+ super().__init__(linter)
+ self.variables_observed: set[str] = set()
+ self._variables_observed_in_step: set[str] = set()
+ self._initialized: bool = False
+
+ def set_abbreviations(self) -> None:
+ items = self.linter.config.variable_name_abbreviations
+ if len(items) % 2 != 0:
+ raise ValueError("Odd number of variable names / abbreviations provided: " + ",".join(items))
+ self.abbreviations: dict[str, str] = {items[counter]: items[counter + 1] for counter in range(0, len(items), 2)}
+
+ def set_case_functions(self) -> None:
+ """Set the functions that will test and transform the text case."""
+ case_str = self.linter.config.variable_name_case
+ if case_str == "pascal":
+ self.case_check_function = humps.is_pascalcase
+ self.case_function = humps.pascalize
+ elif case_str == "camel":
+ self.case_check_function = humps.is_camelcase
+ self.case_function = humps.camelize
+ elif case_str == "kebab":
+ self.case_check_function = humps.is_kebabcase
+ self.case_function = humps.kebabize
+ elif case_str == "snake":
+ self.case_check_function = humps.is_snakecase
+ self.case_function = humps.decamelize
+ else:
+ raise ValueError(f"Could not determine variable case. Input was '{case_str}'")
+
+ @override
+ def check_step(self, step: MM4Step) -> None:
+ if not self._initialized:
+ self.set_abbreviations()
+ self.set_case_functions()
+ self._initialized = True
+
+ self._variables_observed_in_step = set()
+ for xpath in type(self).variable_xpaths:
+ for node in step.parameters_node.xpath(xpath, namespaces={"re": "http://exslt.org/regular-expressions"}): # type: ignore[union-attr]
+ name = node.get("value", default="") # type: ignore[union-attr]
+ self.variables_observed.add(name)
+ self._run_checks(name)
+
+ def _run_checks(self, name: str) -> None:
+ if name not in self._variables_observed_in_step:
+ self._variables_observed_in_step.add(name)
+ if not self.case_check_function(name):
+ new_name = self.case_function(name)
+ self.add_message("invalid-variable-case", args=(name, new_name))
+
+ self._check_abbreviations(name)
+
+ def _check_abbreviations(self, name: str) -> None:
+ name_cmp = name.casefold()
+ for token in humps.main.SPLIT_RE.split(name): # type: ignore[attr-defined] # it's there but not in the `pyi` file.
+ if not token:
+ continue
+ token_cmp = token.casefold()
+ for key, value in self.abbreviations.items():
+ key_cmp = key.casefold()
+ if key_cmp != name_cmp and key_cmp == token_cmp:
+ self.add_message("non-abbreviated-variable", args=(name, key, value))
+
+
+def register(linter: PyLinter) -> None:
+ """Register the checker during initialization.
+
+ :param `linter`: to register the checker to.
+ """
+ linter.register_checker(VariableNameChecker(linter))
diff --git a/src/robolint/constants.py b/src/robolint/constants.py
new file mode 100644
index 0000000..105cbf0
--- /dev/null
+++ b/src/robolint/constants.py
@@ -0,0 +1,22 @@
+"""Constants."""
+
+import re
+
+ASPIRATE_VVP96_STEP_ID = "453a73f0-be3e-0038-26c9-a717cfa4bcfb"
+DISPENSE_VVP96_STEP_ID = "36d4aadc-134e-1706-4d47-6169e0b808cb"
+MIX_VVP96_STEP_ID = "f3b7522f-7975-ee0a-362e-3b28132bca34"
+MOVE_TO_PLATE_GRIPPER_STEP_ID = "b4456bd8-e178-8457-c202-c10ef4c671cb"
+MOVE_TO_PLATE_STEP_ID = "4e41a614-7111-d08c-2f7e-6ba429023c36"
+MULTI_DISPENSE_STEP = "979b25d3-8b26-8739-ce3a-f58aa12bb7ad"
+COMMENT_STEPS_ID = "b373a8f8-2f99-4c10-aece-1b9a9273f5f1"
+BEGIN_LOOP_STEP_ID = "565c02ef-0d8e-40bd-8906-4cc50e02fcb7"
+TIP_EJECT_STEP_ID = "6ba4bf72-4115-3e40-0495-ad84983cf2e8"
+TIP_LOAD_STEP_ID = "1d04aba5-dc42-e044-e57e-311ff530b30f"
+
+DIRECTIVE_REGEX = re.compile(
+ r""" \#[ ]*robolint[ ]*\: # initial pragma
+ [ ]*disable[ ]*=[ ]* # disable directive
+ ([a-z\-]+([ ]*\,[ ]*[a-z\-]+)*) # names of rules to disable
+ """,
+ re.VERBOSE,
+)
diff --git a/src/robolint/exceptions.py b/src/robolint/exceptions.py
new file mode 100644
index 0000000..50447ec
--- /dev/null
+++ b/src/robolint/exceptions.py
@@ -0,0 +1,6 @@
+"""Exceptions."""
+
+
+class NoStepTypeIdError(AttributeError):
+ def __init__(self, class_name: str) -> None:
+ super().__init__(f"The step type ID has not been defined as a class attribute for {class_name}")
diff --git a/src/robolint/hooks/__init__.py b/src/robolint/hooks/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/robolint/hooks/enforce_workspace_settings.py b/src/robolint/hooks/enforce_workspace_settings.py
new file mode 100644
index 0000000..a38bcd2
--- /dev/null
+++ b/src/robolint/hooks/enforce_workspace_settings.py
@@ -0,0 +1,92 @@
+"""Hook to enforce Workspace best practices.
+
+Using hook style from https://github.com/pre-commit/pre-commit-hooks/blob/main/pre_commit_hooks/check_json.py
+"""
+
+import logging
+from typing import Optional
+from typing import Sequence
+from xml.etree.ElementTree import Element
+from xml.etree.ElementTree import ElementTree
+
+from defusedxml.ElementTree import parse
+from stdlib_utils import configure_logging
+
+logger = logging.getLogger("enforce-workspace-settings-hook")
+configure_logging()
+
+
+def _find_existing_tag_indices(element_tree: ElementTree) -> dict[str, int]:
+ indices: dict[str, int] = {}
+ for idx, elem in enumerate(element_tree.iter()):
+ if elem.tag == "ShowSummaryDialogAtEndOfMethod":
+ indices[elem.tag] = idx - 1 # Eli (20221213): not sure why one needs to be subtracted, but it does
+ break
+
+ return indices
+
+
+def enforce_values(
+ filename: str,
+) -> list[str]:
+ """Enforce values match best practices.
+
+ Returns:
+ A list of any variable names that had their values altered.
+ """
+ changed_values: list[str] = []
+ element_tree = parse(filename)
+ root = element_tree.getroot()
+ for path, bad_value, good_value in (
+ ("./InitWorkspaceHardwareOptions/AlwaysReConnect", "false", "true"),
+ ("./InitWorkspaceHardwareOptions/CloseOnSuccess", "false", "true"),
+ ("./InitWorkspaceHardwareOptions/TryToConnectAll", "false", "true"),
+ ):
+ always_reconnect_node = root.find(path)
+ if always_reconnect_node.text == bad_value:
+ changed_values.append(f"{path[2:]} changed from '{bad_value}' to '{good_value}'")
+ always_reconnect_node.text = good_value
+
+ idx_of_show_summary_dialog = _find_existing_tag_indices(element_tree)["ShowSummaryDialogAtEndOfMethod"]
+
+ if root[idx_of_show_summary_dialog + 1].tag != "CreateNewLogWhenStartingMethod":
+ # `MM4` puts this right after `ShowSummaryDialogAtEndOfMethod`
+ existing_created_new_log_node = root.find("CreateNewLogWhenStartingMethod")
+ if existing_created_new_log_node is None:
+ changed_values.append("CreateNewLogWhenStartingMethod added and set to 'false'")
+ else:
+ root.remove(existing_created_new_log_node)
+ changed_values.append("CreateNewLogWhenStartingMethod was moved to correct position and set to 'false'")
+
+ log_node = Element("CreateNewLogWhenStartingMethod")
+ log_node.text = "false"
+ root.insert(idx_of_show_summary_dialog + 1, log_node)
+
+ if not changed_values:
+ return []
+ utils.write_xml_to_file(root=root, filename=filename)
+ return changed_values
+
+
+def main(argv: Optional[Sequence[str]] = None) -> int:
+ """Handle entry from `pre-commit`."""
+ return_code = 0
+ for filename in utils.get_files_to_process(argv):
+ changed_values = enforce_values(filename)
+ if changed_values:
+ logger.info(
+ f"{filename}: The following value(s) were changed (try your commit again and this check should pass): {changed_values}"
+ )
+ return_code = 1
+
+ return return_code
+
+
+if ( # pylint:disable=no-else-raise # Eli (20221028): this is needed to import it when run by pre-commit as a hook
+ __name__ == "__main__"
+): # pylint:disable=duplicate-code # Eli (20221028) can't figure out a way around this to make it compatible with pytest and pre-commit
+ import utils # pylint:disable=import-error # Eli (20221028): this is needed to import it when run by pre-commit as a hook
+
+ raise SystemExit(main())
+else:
+ from . import utils
diff --git a/src/robolint/hooks/strip_workspace_config_values.py b/src/robolint/hooks/strip_workspace_config_values.py
new file mode 100644
index 0000000..b8e4c1c
--- /dev/null
+++ b/src/robolint/hooks/strip_workspace_config_values.py
@@ -0,0 +1,67 @@
+"""Hook to strip/remove any values in the Workspace Config files.
+
+Using hook style from https://github.com/pre-commit/pre-commit-hooks/blob/main/pre_commit_hooks/check_json.py
+"""
+
+import argparse
+import logging
+from typing import Optional
+from typing import Sequence
+
+from defusedxml.ElementTree import parse
+from stdlib_utils import configure_logging
+
+logger = logging.getLogger("strip-workspace-variables-hook")
+configure_logging()
+
+
+def strip_values_from_file(filename: str, ignore_names: Optional[set[str]] = None) -> list[str]:
+ """Clear any values in Variables file.
+
+ Returns:
+ A list of any variable names that had their values cleared.
+ """
+ if ignore_names is None:
+ ignore_names = set()
+ cleared_values: list[str] = []
+ element_tree = parse(filename)
+ root = element_tree.getroot()
+ for variable in root.iter("Variable"):
+ variable_name = variable.find("Name").text
+ if variable_name in ignore_names:
+ continue
+ variable_value_node = variable.find("Value")
+ if variable_value_node.text:
+ cleared_values.append(variable_name)
+ variable_value_node.text = ""
+ if not cleared_values:
+ return []
+ utils.write_xml_to_file(root=root, filename=filename)
+ return cleared_values
+
+
+def main(argv: Optional[Sequence[str]] = None) -> int:
+ """Handle entry from `pre-commit`."""
+ parser = argparse.ArgumentParser()
+ parser.add_argument("filenames", nargs="*", help="Changed files.")
+ parser.add_argument("--ignore-variable", action="append")
+ args = parser.parse_args(argv)
+ return_code = 0
+ for filename in args.filenames:
+ cleared_values = strip_values_from_file(filename, args.ignore_variable)
+ if cleared_values:
+ logger.info(
+ f"{filename}: The following value(s) were cleared (try your commit again and this check should pass): {cleared_values}"
+ )
+ return_code = 1
+ return return_code
+
+
+if ( # pylint:disable=no-else-raise # Eli (20221028): this is needed to import it when run by pre-commit as a hook
+ __name__ == "__main__"
+): # pylint:disable=duplicate-code # Eli (20221028) can't figure out a way around this to make it compatible with pytest and pre-commit
+ import utils # pylint:disable=import-error # Eli (20221028): this is needed to import it when run by pre-commit as a hook
+
+ raise SystemExit(main())
+else:
+ from . import utils
diff --git a/src/robolint/hooks/utils.py b/src/robolint/hooks/utils.py
new file mode 100644
index 0000000..e2ec4cd
--- /dev/null
+++ b/src/robolint/hooks/utils.py
@@ -0,0 +1,48 @@
+"""Utility functions used for the hooks."""
+
+import argparse
+import logging
+import os
+import subprocess
+import sys
+from typing import Iterable
+from typing import Optional
+from typing import Sequence
+from xml.etree import ElementTree
+
+from stdlib_utils import configure_logging
+
+logger = logging.getLogger("hook-utils")
+configure_logging()
+
+
+def write_xml_to_file(*, root: ElementTree.Element, filename: str) -> None:
+ xml_str = ElementTree.tostring(root, encoding="UTF-8").decode("utf-8")
+ xml_str = f'\n{xml_str}'
+ with open(filename, "w", encoding="utf-8") as handle:
+ handle.write(xml_str)
+
+
+def get_files_to_process(argv: Optional[Sequence[str]] = None) -> Iterable[str]:
+ """Get the files that `pre-commit` passed in."""
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "filenames",
+ nargs="*",
+ help="Filenames pre-commit believes are changed.",
+ )
+ args = parser.parse_args(argv)
+ filenames = args.filenames
+ if not isinstance(filenames, Iterable):
+ raise NotImplementedError(f"filenames was type {type(filenames)}")
+ return filenames
+
+
+def run_mypy() -> None:
+ if "PYTHONPATH" not in os.environ:
+ print("PYTHONPATH must be set") # allow-print
+ sys.exit(-1)
+ parts = os.environ["PYTHONPATH"].split(os.pathsep)
+ parts = ["mypy"] + parts
+ response = subprocess.run(parts, check=False, capture_output=False)
+ sys.exit(response.returncode)
diff --git a/src/robolint/robolinter.py b/src/robolint/robolinter.py
new file mode 100644
index 0000000..9610604
--- /dev/null
+++ b/src/robolint/robolinter.py
@@ -0,0 +1,113 @@
+"""Overriding the builtin Pylint linter to be able to process XML files."""
+import re
+import traceback
+from typing import Optional
+from xml.etree.ElementTree import ParseError
+
+import astroid
+from pylint import checkers
+from pylint.exceptions import NoLineSuppliedError
+from pylint.exceptions import UnknownMessageError
+from pylint.interfaces import HIGH
+from pylint.lint.message_state_handler import _MessageStateHandler
+from pylint.lint.pylinter import PyLinter
+from pylint.utils import ASTWalker
+
+from .constants import COMMENT_STEPS_ID
+from .constants import DIRECTIVE_REGEX
+from .utils import parse_steps_from_etree
+from .utils import parse_xml_module_from_file
+from .utils import XmlModule
+
+
+class RoboLintMessageStateHandler(_MessageStateHandler):
+ """Overridden handler to allow parsing directives from MM4 Comment steps."""
+
+ def process_tokens(self, tokens: XmlModule) -> None:
+ """Process tokens from the current module to search for module/block level options.
+
+ See func_block_disable_msg.py test case for expected behavior.
+ """
+ steps = parse_steps_from_etree(tokens.tree.getroot())
+ for step_idx, step in enumerate(steps):
+ if step.step_type_id != COMMENT_STEPS_ID:
+ continue
+ comment = step.get_parameter("Comment")
+ match = re.search(DIRECTIVE_REGEX, comment)
+ if match is None:
+ continue
+ rule_names = match.group(1).split(",")
+ for rule_name in rule_names:
+ self.disable_next(rule_name.strip(), line=step_idx)
+
+ def disable_next(
+ self,
+ msgid: str,
+ _: str = "package",
+ line: Optional[int] = None,
+ ignore_unknown: bool = False,
+ ) -> None:
+ """Disable a message for the next line."""
+ if (
+ line is None
+ ): # In MM4, lines can be 0, the original `pylint` code was `if not line` which was throwing an error on 0
+ raise NoLineSuppliedError
+ try:
+ self._set_msg_status(
+ msgid,
+ enable=False,
+ scope="line",
+ line=line + 1,
+ ignore_unknown=ignore_unknown,
+ )
+ except UnknownMessageError:
+ self.linter.add_message(
+ "unknown-option-value",
+ args=("robolint: disable", msgid),
+ line=line + 1,
+ confidence=HIGH,
+ )
+ self._register_by_id_managed_msg(msgid, line + 1)
+
+
+class RoboLinter(RoboLintMessageStateHandler, PyLinter):
+ """Override `PyLinter` to be able to parse XML files."""
+
+ def get_ast(self, filepath: str, modname: str, data: Optional[str] = None) -> Optional[XmlModule]:
+ """Return an `ElementTree` representation of a module or a string."""
+ try:
+ return parse_xml_module_from_file(filepath)
+ except ParseError as e:
+ self.add_message(
+ "syntax-error",
+ line=0,
+ col_offset=None,
+ args=f"Parsing failed: '{e}'",
+ confidence=HIGH,
+ )
+ except Exception as e:
+ traceback.print_exc()
+ # We raise BuildingError here as this is essentially an `astroid` issue
+ # Creating an issue template and adding the `astroid-error` message is handled
+ # by caller: _check_files
+ raise astroid.AstroidBuildingError(
+ "Building error when trying to create element tree representation of module '{modname}'",
+ modname=modname,
+ ) from e
+ return None
+
+ def _check_astroid_module(
+ self,
+ node: XmlModule,
+ walker: ASTWalker,
+ rawcheckers: list[checkers.BaseRawFileChecker],
+ tokencheckers: list[checkers.BaseTokenChecker],
+ ) -> Optional[bool]:
+ """Check given element tree with given walker and checkers."""
+ self.process_tokens(node)
+ if self._ignore_file:
+ return False
+ # run raw and tokens checkers
+ for raw_checker in rawcheckers:
+ raw_checker.process_module(node)
+ return True
diff --git a/src/robolint/run.py b/src/robolint/run.py
new file mode 100644
index 0000000..1ed3397
--- /dev/null
+++ b/src/robolint/run.py
@@ -0,0 +1,53 @@
+"""Overriding `Pylint` Run objects to be able to run `Robolint`."""
+from pathlib import Path
+import re
+import sys
+import traceback
+from typing import Optional
+from typing import Sequence
+
+import pylint.lint.pylinter
+from pylint.lint.run import Run
+from robolint.robolinter import RoboLinter
+from robolint.utils import robolint_overrides
+
+
+pylint_prepare_crash_report = pylint.lint.pylinter.prepare_crash_report
+
+
+def robolint_prepare_crash_report(exception: Exception, filepath: str, crash_file_path: str) -> Path:
+ # Make errors more visible. Pylint silently crashes by default...not sure why.
+ print(f"There was an error handling '{filepath}'.") # allow-print
+ traceback.print_exc(1)
+ error_file_path: Path = pylint_prepare_crash_report(exception, filepath, crash_file_path)
+ print(f" A log file of the error is located at: '{error_file_path}'.") # allow-print
+ return error_file_path
+
+
+pylint.lint.pylinter.prepare_crash_report = robolint_prepare_crash_report
+
+robolint_overrides()
+
+
+class RobolintRun(Run):
+ # pylint:disable=too-few-public-methods # Eli (20230214): this is just overriding to set the linter class
+ LinterClass = RoboLinter
+
+
+# copied from `pylint's` `__init__.py`
+def run_pylint(argv: Optional[Sequence[str]] = None) -> int:
+ """Run pylint.
+
+ `argv` can be a sequence of strings normally supplied as arguments on the command line
+ """
+ try:
+ RobolintRun(argv or sys.argv[1:])
+ except KeyboardInterrupt:
+ sys.exit(2)
+ return 0
+
+
+# copied from pylint`s `venv/bin/pylint`
+if __name__ == "__main__":
+ sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
+ sys.exit(run_pylint())
diff --git a/src/robolint/test_utils.py b/src/robolint/test_utils.py
new file mode 100644
index 0000000..e249e7d
--- /dev/null
+++ b/src/robolint/test_utils.py
@@ -0,0 +1,23 @@
+"""Testing helpers."""
+
+import sys
+from typing import NoReturn
+
+import pylint
+from pylint.testutils import CheckerTestCase
+from robolint.run import robolint_prepare_crash_report
+
+
+# arbitrary exit code that's not 1 (that's reserved in `pre-commit` for modifying files)
+INTERNAL_ROBOLINT_ERROR_EXIT_CODE = 3
+
+
+def prepare_crash_report_and_exit(exception: Exception, filepath: str, crash_file_path: str) -> NoReturn:
+ """`Pylint` silences errors but we want test cases to fail."""
+ robolint_prepare_crash_report(exception, filepath, crash_file_path)
+ sys.exit(INTERNAL_ROBOLINT_ERROR_EXIT_CODE)
+
+
+class RobolintCheckerTestCase(CheckerTestCase):
+ def setup_class(self) -> None:
+ pylint.lint.pylinter.prepare_crash_report = prepare_crash_report_and_exit
diff --git a/src/robolint/utils.py b/src/robolint/utils.py
new file mode 100644
index 0000000..82f9669
--- /dev/null
+++ b/src/robolint/utils.py
@@ -0,0 +1,134 @@
+"""General utilities."""
+
+import argparse
+import re
+from typing import Any
+from typing import Union
+
+from lxml.etree import _Element
+from lxml.etree import _ElementTree
+from lxml.etree import parse
+import pylint
+from pylint.testutils import CheckerTestCase
+from stdlib_utils import find_exactly_one_xml_element
+
+
+class XmlModule: # pylint: disable=too-few-public-methods
+ """Mimic an `astroid` module but for XML."""
+
+ file: str # needed for `self.current_file`
+ tolineno: int # needed for `pylint.utils.file_state.FileState`
+ tree: _ElementTree
+
+
+class MM4Step:
+ """Base class for MM4 Step in a Method."""
+
+ _parameters_node: _Element
+
+ def __init__(self, *, xml_node: _ElementTree, step_index: int, step_type_id: str) -> None:
+ self.xml_node = xml_node
+ self.step_index = step_index
+ self.step_type_id = step_type_id
+ self._parameters: dict[str, str] = {}
+
+ def parse(self) -> None:
+ self._parameters_node = find_exactly_one_xml_element(self.xml_node, './/Dictionary[@name="Parameters"]')
+
+ @property
+ def parameters_node(self) -> _Element:
+ try:
+ return self._parameters_node
+ except AttributeError:
+ pass
+ self.parse()
+ return self._parameters_node
+
+ def get_parameter(self, parameter_name: str) -> str:
+ """Get a parameter value from Method Manager XML."""
+ try:
+ return self._parameters[parameter_name]
+ except KeyError:
+ pass
+ parent_node = find_exactly_one_xml_element(self.parameters_node, f'.//Simple[@value="{parameter_name}"]...')
+ value_node = find_exactly_one_xml_element(parent_node, ".//Simple[@type]")
+ value = value_node.attrib["value"]
+ if not isinstance(value, str):
+ raise NotImplementedError(f"The value should always be str, but {value} was type {type(value)}")
+ self._parameters[parameter_name] = value
+ return value
+
+
+def parse_steps_from_etree(root: _Element) -> list[MM4Step]:
+ """Parse the steps from an Element Tree."""
+ parsed_steps: list[MM4Step] = []
+ for collection in root.iter("Collection"):
+ attributes = collection.attrib
+ try:
+ collection_type = attributes["name"]
+ except KeyError as e:
+ raise NotImplementedError(
+ f"Collections are always supposed to have a name attribute, but this one only had attributes {attributes}."
+ ) from e
+ if collection_type != "Steps":
+ continue
+ steps_node = find_exactly_one_xml_element(collection, "Items")
+ for step_idx, step_node in enumerate(steps_node.iter("Complex")):
+ step_id_node = find_exactly_one_xml_element(step_node, './/Simple[@name="CommandId"]')
+ step_type_id = step_id_node.attrib["value"]
+ parsed_steps.append(MM4Step(xml_node=step_node, step_index=step_idx, step_type_id=step_type_id))
+ break
+
+ return parsed_steps
+
+
+def parse_steps(filename: str) -> list[MM4Step]:
+ """Parse the steps from an XML file."""
+ element_tree = parse(filename)
+ root = element_tree.getroot()
+ return parse_steps_from_etree(root)
+
+
+def parse_xml_module_from_file(filepath: str) -> XmlModule:
+ line_count = 0
+ with open(filepath, encoding="utf-8") as xml_file:
+ for _, _ in enumerate(xml_file):
+ line_count += 1
+ xml = XmlModule()
+ xml.tolineno = line_count + 1 # needed for `pylint.utils.file_state.FileState`
+ xml.file = filepath # needed for `self.current_file`
+ xml.tree = parse(filepath)
+ return xml
+
+
+def robolint_regex_transformer(value: str) -> re.Pattern[str]:
+ """Return `re.compile(value)`."""
+ try:
+ if "\n" in value:
+ return re.compile(value, re.VERBOSE)
+ return re.compile(value)
+ except re.error as err:
+ msg = f"Error in provided regular expression: {value} beginning at index {err.pos}: {err.msg}"
+ raise argparse.ArgumentTypeError(msg) from err
+
+
+def robolint_regexp_validator(
+ _: Any, name: str, value: Union[str, re.Pattern[str]] # pylint: disable=unused-argument
+) -> re.Pattern[str]:
+ if hasattr(value, "pattern"):
+ return value # type: ignore[return-value]
+ if "\n" in value:
+ return re.compile(value, re.VERBOSE)
+ return re.compile(value)
+
+
+def robolint_overrides() -> None:
+ pylint.config.argument._regex_transformer = robolint_regex_transformer # pylint: disable=protected-access
+ pylint.config.argument._TYPE_TRANSFORMERS["regexp"] = robolint_regex_transformer # pylint: disable=protected-access
+ pylint.config.option._regexp_validator = robolint_regexp_validator # pylint: disable=protected-access
+ pylint.config.option.VALIDATORS["regexp"] = robolint_regex_transformer
+
+
+class RobolintCheckerTestCase(CheckerTestCase):
+ def setup_class(self) -> None:
+ robolint_overrides()
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..2d73769
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+"""`Pytest` configuration."""
+import os
+import sys
+from unittest.mock import MagicMock
+
+import pytest
+from pytest_mock import MockerFixture
+
+sys.dont_write_bytecode = True
+sys.stdout = (
+ sys.stderr
+) # allow printing of `pytest` output when running `pytest-xdist` https://stackoverflow.com/questions/27006884/pytest-xdist-without-capturing-output
+
+
+@pytest.fixture(scope="function", name="mock_print")
+def fixture_mock_print(mocker: MockerFixture) -> MagicMock:
+ return mocker.patch("builtins.print", autospec=True) # don't print all the error messages to console
+
+
+@pytest.fixture(scope="function", name="aws_credentials", autouse=True)
+def fixture_aws_credentials() -> None:
+ """Mock AWS Credentials for moto."""
+ os.environ["AWS_ACCESS_KEY_ID"] = "testing"
+ os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
+ os.environ["AWS_SECURITY_TOKEN"] = "testing"
+ os.environ["AWS_SESSION_TOKEN"] = "testing"
+ os.environ["AWS_DEFAULT_REGION"] = "us-east-1"
diff --git a/tests/pylintrc b/tests/pylintrc
new file mode 100644
index 0000000..64c9d3c
--- /dev/null
+++ b/tests/pylintrc
@@ -0,0 +1,369 @@
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Pickle collected data fWor later comparisons.
+persistent=yes
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=pylint.extensions.bad_builtin
+
+# Use multiple processes to speed up Pylint. 0 autodetects all available processors
+# jobs=0 # there's a bug that needs to be fixed before this can be enabled again https://github.com/pylint-dev/pylint/issues/8911
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=PyQt5,lxml # pylint does not load C extensions, so PyQt needs to be whitelisted
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time. See also the "--disable" option for examples.
+enable=use-symbolic-message-instead,useless-suppression,fixme
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+
+disable=
+ #attribute-defined-outside-init,
+ #duplicate-code,
+ # tests can be named longer and more descriptively than normal functions
+ invalid-name,
+ # pylint struggles to understand fixtures in test function arguments
+ unused-argument,
+ # test functions can have tons of arguments (typically parametrized functions)
+ too-many-arguments,
+ # test functions don't need docstrings
+ missing-function-docstring,
+ # test classes don't need docstrings
+ missing-class-docstring,
+ # test modules don't need docstrings
+ missing-module-docstring,
+ #protected-access,
+ #too-few-public-methods,
+ # handled by black
+ format,
+ # handled by zimports
+ wrong-import-order,
+ # There's some disagreement whether this is relevant since f-strings are so fast to execute anyway: https://github.com/PyCQA/pylint/issues/2354
+ logging-fstring-interpolation,
+ fixme, # handled by a separate pylint run that only emits warnings
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html. You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+# files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+# fixme added to disable= rule. Notesare checked as WARNINGS in a separate pylint run.
+# notes=FIXME,XXX,TODO
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=yes
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_$|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+# List of optional constructs for which whitespace checking is disabled
+# no-space-check=trailing-comma,dict-separator
+
+# Maximum number of lines in a module
+max-module-lines=2000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+
+[DEPRECATED_BUILTINS]
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,input,eval
+
+
+[BASIC]
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=e,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=yes
+
+# Regular expression matching correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct constant names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression matching correct attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,}$
+
+# Regular expression matching correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Regular expression matching correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression matching correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression matching correct method names
+method-rgx=[a-z_][a-z0-9_]{2,}$
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=__.*__
+#no-docstring-rgx=(__.*__)|(test*.py)|(fixtures.py)
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# List of decorators that define properties, such as abc.abstractproperty.
+property-classes=abc.abstractproperty
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis
+ignored-modules=
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject, optparse.Values, thread._local, _thread._local
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=REQUEST,acl_users,aq_parent
+
+# List of decorators that create context managers from functions, such as
+# contextlib.contextmanager.
+contextmanager-decorators=contextlib.contextmanager
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=en_US
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=src/.pylint-spelling-private-dict
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=10
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+max-locals=25
+
+# Maximum number of return / yield for function / method body
+max-returns=11
+
+# Maximum number of branch for function / method body
+max-branches=26
+
+# Maximum number of statements in function / method body
+max-statements=100
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=11
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=25
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp,__post_init__
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,_fields,_replace,_source,_make
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=builtins.Exception
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/enforce_workspace_settings/__init__.py b/tests/unit/enforce_workspace_settings/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/enforce_workspace_settings/test_enforce_values.py b/tests/unit/enforce_workspace_settings/test_enforce_values.py
new file mode 100644
index 0000000..58d8173
--- /dev/null
+++ b/tests/unit/enforce_workspace_settings/test_enforce_values.py
@@ -0,0 +1,83 @@
+import os
+import shutil
+from tempfile import TemporaryDirectory
+
+import pytest
+from robolint.hooks.enforce_workspace_settings import enforce_values
+from stdlib_utils import get_current_file_abs_directory
+
+PATH_TO_CURRENT_FILE = get_current_file_abs_directory()
+PATH_TO_CONFIGS = os.path.join(PATH_TO_CURRENT_FILE, "workspace_settings_configs")
+
+
+def test_When_file_is_clean__Then_returns_falsey() -> None:
+ actual = enforce_values(os.path.join(PATH_TO_CONFIGS, "clean.xml"))
+ assert not actual
+
+
+def test_When_one_value_is_wrong__Then_problem_identified_and_new_file_is_clean() -> None:
+ original_file_path = os.path.join(PATH_TO_CONFIGS, "always_reconnect_false.xml")
+ with TemporaryDirectory() as temp_dir:
+ new_file_path = os.path.join(temp_dir, "blah.xml")
+ shutil.copy(original_file_path, new_file_path)
+ initial_result = enforce_values(new_file_path)
+ assert len(initial_result) == 1
+
+ actual = enforce_values(new_file_path)
+ assert not actual
+
+
+@pytest.mark.parametrize(
+ ",".join(("test_filename", "expected_message", "test_description")),
+ [
+ (
+ "always_reconnect_false.xml",
+ "InitWorkspaceHardwareOptions/AlwaysReConnect changed from 'false' to 'true'",
+ "Always Reconnect value should be true",
+ ),
+ (
+ "close_on_success_false.xml",
+ "InitWorkspaceHardwareOptions/CloseOnSuccess changed from 'false' to 'true'",
+ "Close on Success value should be true",
+ ),
+ (
+ "try_connect_all_false.xml",
+ "InitWorkspaceHardwareOptions/TryToConnectAll changed from 'false' to 'true'",
+ "Try to Connect All value should be true",
+ ),
+ (
+ "create_new_log_missing.xml",
+ "CreateNewLogWhenStartingMethod added and set to 'false'",
+ "Create new Log should be present",
+ ),
+ (
+ "create_new_log_in_wrong_position.xml",
+ "CreateNewLogWhenStartingMethod was moved to correct position and set to 'false'",
+ "Create new Log should be in the position that MM4 automatically puts it, to avoid git conflicts",
+ ),
+ ],
+)
+def test_When_single_error_is_present__Then_problem_identified_and_new_file_is_clean(
+ test_filename: str, expected_message: str, test_description: str
+) -> None:
+ original_file_path = os.path.join(PATH_TO_CONFIGS, test_filename)
+ with TemporaryDirectory() as temp_dir:
+ new_file_path = os.path.join(temp_dir, "blah.xml")
+ shutil.copy(original_file_path, new_file_path)
+ initial_result = enforce_values(new_file_path)
+ assert initial_result == [expected_message]
+
+ actual = enforce_values(new_file_path)
+ assert not actual
+
+
+def test_When_multiple_errors_are_present__Then_problems_identified_and_new_file_is_clean() -> None:
+ original_file_path = os.path.join(PATH_TO_CONFIGS, "multiple_errors.xml")
+ with TemporaryDirectory() as temp_dir:
+ new_file_path = os.path.join(temp_dir, "blah.xml")
+ shutil.copy(original_file_path, new_file_path)
+ initial_result = enforce_values(new_file_path)
+ assert len(initial_result) > 1
+
+ actual = enforce_values(new_file_path)
+ assert not actual
diff --git a/tests/unit/enforce_workspace_settings/test_main.py b/tests/unit/enforce_workspace_settings/test_main.py
new file mode 100644
index 0000000..cb3359c
--- /dev/null
+++ b/tests/unit/enforce_workspace_settings/test_main.py
@@ -0,0 +1,47 @@
+from unittest.mock import call
+
+from pytest_mock import MockerFixture
+from robolint.hooks import enforce_workspace_settings
+from robolint.hooks.enforce_workspace_settings import main
+
+from ..utils import mock_argv_filelist
+
+
+def test_main_calls_enforce_values_with_passed_filenames(mocker: MockerFixture) -> None:
+ mocked_function = mocker.patch.object(enforce_workspace_settings, "enforce_values", return_value=[])
+ expected_file_list = ["dummy4.config", "dummy2.config"]
+ mock_argv_filelist(mocker, expected_file_list)
+ main()
+ assert mocked_function.call_count == 2
+ mocked_function.assert_has_calls([call(expected_file_list[0]), call(expected_file_list[1])], any_order=True)
+
+
+def test_When_enforce_values_identifies_problem__Then_main_returns_1(mocker: MockerFixture) -> None:
+ mocker.patch.object(enforce_workspace_settings, "enforce_values", return_value=["var1"])
+ expected_file_list = ["dummy3.config"]
+ mock_argv_filelist(mocker, expected_file_list)
+ actual = main()
+ assert actual == 1
+
+
+def test_When_enforce_values_identifies_problem__Then_main_logs_the_variable_and_file_name(
+ mocker: MockerFixture,
+) -> None:
+ expected_variable_name = "var2"
+ mocker.patch.object(enforce_workspace_settings, "enforce_values", return_value=[expected_variable_name])
+ mocked_logger = mocker.patch.object(enforce_workspace_settings.logger, "info")
+ expected_file_list = ["dummy2.config"]
+ mock_argv_filelist(mocker, expected_file_list)
+ main()
+ assert mocked_logger.call_count == 1
+ actual_call_str = str(mocked_logger.call_args_list[0])
+ assert expected_variable_name in actual_call_str
+ assert expected_file_list[0] in actual_call_str
+
+
+def test_When_enforce_values_identifies_no_problems__Then_main_returns_0(mocker: MockerFixture) -> None:
+ mocker.patch.object(enforce_workspace_settings, "enforce_values", return_value=[])
+ expected_file_list = ["dummy1.config"]
+ mock_argv_filelist(mocker, expected_file_list)
+ actual = main()
+ assert actual == 0
diff --git a/tests/unit/enforce_workspace_settings/workspace_settings_configs/always_reconnect_false.xml b/tests/unit/enforce_workspace_settings/workspace_settings_configs/always_reconnect_false.xml
new file mode 100644
index 0000000..ae6266b
--- /dev/null
+++ b/tests/unit/enforce_workspace_settings/workspace_settings_configs/always_reconnect_false.xml
@@ -0,0 +1,36 @@
+
+
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+
+
+
+
+
+ 330
+
+
+ true
+
+
+ false
+ true
+ true
+ true
+
+
+ true
+ true
+ true
+ true
+
+
+ Verbose
+ false
+
\ No newline at end of file
diff --git a/tests/unit/enforce_workspace_settings/workspace_settings_configs/clean.xml b/tests/unit/enforce_workspace_settings/workspace_settings_configs/clean.xml
new file mode 100644
index 0000000..37e70bf
--- /dev/null
+++ b/tests/unit/enforce_workspace_settings/workspace_settings_configs/clean.xml
@@ -0,0 +1,36 @@
+
+
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+
+
+
+
+
+ 330
+
+
+ true
+
+
+ true
+ true
+ true
+ true
+
+
+ true
+ true
+ true
+ true
+
+
+ Verbose
+ false
+
\ No newline at end of file
diff --git a/tests/unit/enforce_workspace_settings/workspace_settings_configs/close_on_success_false.xml b/tests/unit/enforce_workspace_settings/workspace_settings_configs/close_on_success_false.xml
new file mode 100644
index 0000000..c4233ec
--- /dev/null
+++ b/tests/unit/enforce_workspace_settings/workspace_settings_configs/close_on_success_false.xml
@@ -0,0 +1,36 @@
+
+
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+
+
+
+
+
+ 330
+
+
+ true
+
+
+ true
+ false
+ true
+ true
+
+
+ true
+ true
+ true
+ true
+
+
+ Verbose
+ false
+
\ No newline at end of file
diff --git a/tests/unit/enforce_workspace_settings/workspace_settings_configs/create_new_log_in_wrong_position.xml b/tests/unit/enforce_workspace_settings/workspace_settings_configs/create_new_log_in_wrong_position.xml
new file mode 100644
index 0000000..ddf5b8c
--- /dev/null
+++ b/tests/unit/enforce_workspace_settings/workspace_settings_configs/create_new_log_in_wrong_position.xml
@@ -0,0 +1,36 @@
+
+
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+
+
+
+
+
+ 330
+
+
+ true
+
+
+ true
+ true
+ true
+ true
+
+
+ true
+ true
+ true
+ true
+
+
+ Verbose
+ false
+ false
+
\ No newline at end of file
diff --git a/tests/unit/enforce_workspace_settings/workspace_settings_configs/create_new_log_missing.xml b/tests/unit/enforce_workspace_settings/workspace_settings_configs/create_new_log_missing.xml
new file mode 100644
index 0000000..2ed90ed
--- /dev/null
+++ b/tests/unit/enforce_workspace_settings/workspace_settings_configs/create_new_log_missing.xml
@@ -0,0 +1,35 @@
+
+
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+
+
+
+
+
+ 330
+
+
+ true
+
+
+ true
+ true
+ true
+ true
+
+
+ true
+ true
+ true
+ true
+
+
+ Verbose
+ false
+
\ No newline at end of file
diff --git a/tests/unit/enforce_workspace_settings/workspace_settings_configs/multiple_errors.xml b/tests/unit/enforce_workspace_settings/workspace_settings_configs/multiple_errors.xml
new file mode 100644
index 0000000..433be45
--- /dev/null
+++ b/tests/unit/enforce_workspace_settings/workspace_settings_configs/multiple_errors.xml
@@ -0,0 +1,35 @@
+
+
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+
+
+
+
+
+ 330
+
+
+ true
+
+
+ true
+ false
+ true
+ true
+
+
+ true
+ true
+ true
+ true
+
+
+ Verbose
+ false
+
\ No newline at end of file
diff --git a/tests/unit/enforce_workspace_settings/workspace_settings_configs/try_connect_all_false.xml b/tests/unit/enforce_workspace_settings/workspace_settings_configs/try_connect_all_false.xml
new file mode 100644
index 0000000..0fcbe18
--- /dev/null
+++ b/tests/unit/enforce_workspace_settings/workspace_settings_configs/try_connect_all_false.xml
@@ -0,0 +1,36 @@
+
+
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+
+
+
+
+
+ 330
+
+
+ true
+
+
+ true
+ true
+ true
+ false
+
+
+ true
+ true
+ true
+ true
+
+
+ Verbose
+ false
+
\ No newline at end of file
diff --git a/tests/unit/pip_tools_hook/__init__.py b/tests/unit/pip_tools_hook/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/pip_tools_hook/requirements_files/requirements-clean-linux.txt b/tests/unit/pip_tools_hook/requirements_files/requirements-clean-linux.txt
new file mode 100644
index 0000000..fa244d4
--- /dev/null
+++ b/tests/unit/pip_tools_hook/requirements_files/requirements-clean-linux.txt
@@ -0,0 +1,38 @@
+#
+# This file is autogenerated by pip-compile with python 3.9
+# To update, run:
+#
+# pip-compile --generate-hashes --output-file=/home/tests/unit/pip_tools_hook/requirements_files/requirements-clean.txt /home/tests/unit/pip_tools_hook/requirements_files/requirements.in
+#
+attrs==22.1.0 \
+ --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \
+ --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c
+ # via pytest
+iniconfig==1.1.1 \
+ --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
+ --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32
+ # via pytest
+packaging==21.3 \
+ --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
+ --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
+ # via pytest
+pluggy==1.0.0 \
+ --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
+ --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
+ # via pytest
+py==1.11.0 \
+ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \
+ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378
+ # via pytest
+pyparsing==3.0.9 \
+ --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
+ --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
+ # via packaging
+pytest==7.1.2 \
+ --hash=sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c \
+ --hash=sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45
+ # via -r /home/tests/unit/pip_tools_hook/requirements_files/requirements.in
+tomli==2.0.1 \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via pytest
diff --git a/tests/unit/pip_tools_hook/requirements_files/requirements-clean-win32.txt b/tests/unit/pip_tools_hook/requirements_files/requirements-clean-win32.txt
new file mode 100644
index 0000000..a37c5c7
--- /dev/null
+++ b/tests/unit/pip_tools_hook/requirements_files/requirements-clean-win32.txt
@@ -0,0 +1,45 @@
+#
+# This file is autogenerated by pip-compile with python 3.9
+# To update, run:
+#
+# pip-compile --generate-hashes --output-file=requirements.txt requirements.in
+#
+atomicwrites==1.4.1 \
+ --hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11
+ # via pytest
+attrs==22.1.0 \
+ --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \
+ --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c
+ # via pytest
+colorama==0.4.6 \
+ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
+ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
+ # via pytest
+iniconfig==1.1.1 \
+ --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
+ --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32
+ # via pytest
+packaging==21.3 \
+ --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
+ --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
+ # via pytest
+pluggy==1.0.0 \
+ --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
+ --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
+ # via pytest
+py==1.11.0 \
+ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \
+ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378
+ # via pytest
+pyparsing==3.0.9 \
+ --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
+ --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
+ # via packaging
+pytest==7.1.2 \
+ --hash=sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c \
+ --hash=sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45
+ # via -r requirements.in
+tomli==2.0.1 \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via pytest
diff --git a/tests/unit/pip_tools_hook/requirements_files/requirements-dirty.txt b/tests/unit/pip_tools_hook/requirements_files/requirements-dirty.txt
new file mode 100644
index 0000000..5b82a8b
--- /dev/null
+++ b/tests/unit/pip_tools_hook/requirements_files/requirements-dirty.txt
@@ -0,0 +1,34 @@
+#
+# This file is autogenerated by pip-compile with python 3.9
+# To update, run:
+#
+# pip-compile --generate-hashes --output-file=/home/tests/unit/pip_tools_hook/requirements_files/requirements-dirty.txt /home/tests/unit/pip_tools_hook/requirements_files/requirements.in
+#
+iniconfig==1.1.1 \
+ --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
+ --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32
+ # via pytest
+packaging==21.3 \
+ --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
+ --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
+ # via pytest
+pluggy==1.0.0 \
+ --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \
+ --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3
+ # via pytest
+py==1.11.0 \
+ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \
+ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378
+ # via pytest
+pyparsing==3.0.9 \
+ --hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
+ --hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
+ # via packaging
+pytest==7.1.2 \
+ --hash=sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c \
+ --hash=sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45
+ # via -r /home/tests/unit/pip_tools_hook/requirements_files/requirements.in
+tomli==2.0.1 \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via pytest
diff --git a/tests/unit/pip_tools_hook/requirements_files/requirements.in b/tests/unit/pip_tools_hook/requirements_files/requirements.in
new file mode 100644
index 0000000..93578f0
--- /dev/null
+++ b/tests/unit/pip_tools_hook/requirements_files/requirements.in
@@ -0,0 +1,2 @@
+# Simple requirements file to be quick for pip-compile
+pytest==7.1.2
\ No newline at end of file
diff --git a/tests/unit/pip_tools_hook/test_compile.py b/tests/unit/pip_tools_hook/test_compile.py
new file mode 100644
index 0000000..b8e71e3
--- /dev/null
+++ b/tests/unit/pip_tools_hook/test_compile.py
@@ -0,0 +1,71 @@
+import os
+from pathlib import Path
+import shutil
+import sys
+import tempfile
+from unittest.mock import MagicMock
+
+from hooks.pip_tools_hook import main
+from pytest_mock import MockerFixture
+from stdlib_utils import get_current_file_abs_directory
+
+
+PATH_TO_CURRENT_FILE = get_current_file_abs_directory()
+PATH_TO_REQUIREMENTS = os.path.join(PATH_TO_CURRENT_FILE, "requirements_files")
+
+
+def test_When_requirements_txt_up_to_date__Then_no_printed_message(
+ mocker: MockerFixture, mock_print: MagicMock
+) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ origin = Path().absolute()
+ try:
+ os.chdir(temp_dir)
+ shutil.copy(
+ os.path.join(PATH_TO_REQUIREMENTS, f"requirements-clean-{sys.platform}.txt"), "requirements.txt"
+ )
+ shutil.copy(os.path.join(PATH_TO_REQUIREMENTS, "requirements.in"), "requirements.in")
+ argv = ["dummy.py", "compile", "requirements.txt", "requirements.in"]
+ mocker.patch.object(sys, "argv", argv)
+
+ actual_exit_code = main()
+
+ mock_print.assert_not_called()
+ assert actual_exit_code == 0
+
+ finally:
+ os.chdir(origin)
+
+
+def test_When_requirements_txt_not_up_to_date__Then_message_is_printed(
+ mocker: MockerFixture, mock_print: MagicMock
+) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ temp_file_path = os.path.join(temp_dir, "requirements.txt")
+ shutil.copy(os.path.join(PATH_TO_REQUIREMENTS, "requirements-dirty.txt"), temp_file_path)
+ argv = ["dummy.py", "compile", temp_file_path, os.path.join(PATH_TO_REQUIREMENTS, "requirements.in")]
+ mocker.patch.object(sys, "argv", argv)
+ actual = main()
+ assert actual == 1
+ assert mock_print.call_count > 2
+
+
+def test_Given_arg_to_always_exit_zero__When_requirements_txt_not_up_to_date__Then_exit_code_is_zero_after_compile_run(
+ mocker: MockerFixture, mock_print: MagicMock
+) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ temp_file_path = os.path.join(temp_dir, "requirements.txt")
+ shutil.copy(os.path.join(PATH_TO_REQUIREMENTS, "requirements-dirty.txt"), temp_file_path)
+ argv = [
+ "dummy.py",
+ "exit-zero",
+ "compile",
+ temp_file_path,
+ os.path.join(PATH_TO_REQUIREMENTS, "requirements.in"),
+ ]
+ mocker.patch.object(sys, "argv", argv)
+ actual = main()
+ assert actual == 0
+ compile_notification_print_args = mock_print.call_args_list[2]
+ actual_args, _ = compile_notification_print_args
+ assert "compiled" in actual_args[0]
diff --git a/tests/unit/pip_tools_hook/test_sync.py b/tests/unit/pip_tools_hook/test_sync.py
new file mode 100644
index 0000000..21ab354
--- /dev/null
+++ b/tests/unit/pip_tools_hook/test_sync.py
@@ -0,0 +1,62 @@
+import sys
+from typing import Optional
+from unittest.mock import MagicMock
+
+import pytest
+from pytest_mock import MockerFixture
+
+from .utils import mock_run_and_call_main
+
+
+@pytest.mark.parametrize(
+ ",".join(
+ ("test_description", "test_sync_type", "test_stdout", "expected_return_code", "expected_minimum_print_count")
+ ),
+ [
+ (
+ "Given pip-sync call is mocked to return no changes, When pip-sync on checkout is called, Then no messages are printed",
+ "sync-on-checkout",
+ "Everything fine",
+ 0,
+ None,
+ ),
+ (
+ "Given pip-sync call is mocked to return changes, When pip-sync on checkout is called, Then no messages are printed",
+ "sync-on-checkout",
+ "lots of stuff",
+ 0,
+ None,
+ ),
+ (
+ "Given pip-sync call is mocked to return no changes, When pip-sync on commit is called, Then no messages are printed",
+ "sync-on-commit",
+ "Everything fine",
+ 0,
+ None,
+ ),
+ (
+ "Given pip-sync call is mocked to return changes, When pip-sync on commit is called, Then messages are printed",
+ "sync-on-commit",
+ "lots of stuff",
+ 1,
+ 2,
+ ),
+ ],
+)
+def test_sync(
+ test_description: str,
+ test_sync_type: str,
+ test_stdout: bytes,
+ expected_return_code: int,
+ expected_minimum_print_count: Optional[int],
+ mocker: MockerFixture,
+ mock_print: MagicMock,
+) -> None:
+ argv = ["dummy.py", test_sync_type]
+ mocker.patch.object(sys, "argv", argv)
+ actual = mock_run_and_call_main(mocker, test_stdout)
+ assert actual == expected_return_code
+ if expected_minimum_print_count is None:
+ mock_print.assert_not_called()
+ else:
+ assert mock_print.call_count >= expected_minimum_print_count
diff --git a/tests/unit/pip_tools_hook/utils.py b/tests/unit/pip_tools_hook/utils.py
new file mode 100644
index 0000000..4c7acc1
--- /dev/null
+++ b/tests/unit/pip_tools_hook/utils.py
@@ -0,0 +1,8 @@
+from hooks import pip_tools_hook
+from hooks.pip_tools_hook import main
+from pytest_mock import MockerFixture
+
+
+def mock_run_and_call_main(mocker: MockerFixture, stdout: bytes) -> int:
+ mocker.patch.object(pip_tools_hook, "execute_command", autospec=True, return_value=(stdout, "", 0))
+ return main()
diff --git a/tests/unit/robolint/__init__.py b/tests/unit/robolint/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/robolint/checkers/__init__.py b/tests/unit/robolint/checkers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/robolint/checkers/test_hardcoded_aspirate_values.py b/tests/unit/robolint/checkers/test_hardcoded_aspirate_values.py
new file mode 100644
index 0000000..20906f6
--- /dev/null
+++ b/tests/unit/robolint/checkers/test_hardcoded_aspirate_values.py
@@ -0,0 +1,89 @@
+import os
+from unittest.mock import ANY
+
+from pylint.testutils import MessageTest
+from robolint import HardcodedValuesChecker
+from robolint.test_utils import RobolintCheckerTestCase
+
+from ..fixtures import get_parsed_steps
+from ..fixtures import get_parsed_xml_module
+
+
+class TestHardcodedValuesCheckerSingleSteps(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = HardcodedValuesChecker
+
+ def test_When_Single_Channel_Using_variable__Then_no_error(self) -> None:
+ steps = get_parsed_steps(os.path.join("hardcoded-aspiration-volume", "single-active-channel-with-variable"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[0])
+
+ def test_When_Single_Channel_hardcoded__Then_error(self) -> None:
+ steps = get_parsed_steps(os.path.join("hardcoded-aspiration-volume", "single-active-channel-hardcoded"))
+ step_index_to_test = 1
+ with self.assertAddsMessages(
+ MessageTest(
+ "hardcoded-aspirate-volume",
+ args=("A01: 5 uL",),
+ line=step_index_to_test,
+ )
+ ):
+ self.checker.visit_step(steps[step_index_to_test])
+
+ def test_When_Single_Channel_hardcoded_not_A01__Then_error(self) -> None:
+ steps = get_parsed_steps(os.path.join("hardcoded-aspiration-volume", "single-active-channel-not-a1-hardcoded"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "hardcoded-aspirate-volume",
+ args=("C05: 8.3 uL",),
+ line=ANY,
+ )
+ ):
+ self.checker.visit_step(steps[0])
+
+ def test_When_one_variable_and_one_hardcoded__Then_error(self) -> None:
+ steps = get_parsed_steps(os.path.join("hardcoded-aspiration-volume", "one-variable-one-hardcoded"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "hardcoded-aspirate-volume",
+ args=("D08: 4.3 uL",),
+ line=ANY,
+ )
+ ):
+ self.checker.visit_step(steps[0])
+
+ def test_When_two_hardcoded__Then_error_shows_both_wells(self) -> None:
+ steps = get_parsed_steps(os.path.join("hardcoded-aspiration-volume", "two-active-hardcoded-channels"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "hardcoded-aspirate-volume",
+ args=("C05: 8.3 uL, D08: 4.3 uL",),
+ line=ANY,
+ )
+ ):
+ self.checker.visit_step(steps[0])
+
+ def test_When_Volume_Property_Set_And_Variable_Also_Set__Then_no_error(self) -> None:
+ steps = get_parsed_steps(
+ os.path.join("hardcoded-aspiration-volume", "volume-property-set-even-though-variable-also-set")
+ )
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[5])
+
+
+class TestHardcodedValuesCheckerWholeModule(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = HardcodedValuesChecker
+
+ def test_When_Single_Channel_hardcoded__Then_error(self) -> None:
+ xml_module = get_parsed_xml_module(
+ os.path.join("hardcoded-aspiration-volume", "single-active-channel-hardcoded.xml")
+ )
+ with self.assertAddsMessages(
+ MessageTest(
+ "hardcoded-aspirate-volume",
+ args=("A01: 5 uL",),
+ line=1,
+ )
+ ):
+ self.checker.process_module(xml_module)
diff --git a/tests/unit/robolint/checkers/test_hardcoded_dispense_values.py b/tests/unit/robolint/checkers/test_hardcoded_dispense_values.py
new file mode 100644
index 0000000..1b53b19
--- /dev/null
+++ b/tests/unit/robolint/checkers/test_hardcoded_dispense_values.py
@@ -0,0 +1,64 @@
+import os
+from unittest.mock import ANY
+
+from pylint.testutils import MessageTest
+from robolint import HardcodedValuesChecker
+from robolint.test_utils import RobolintCheckerTestCase
+
+from ..fixtures import get_parsed_steps
+from ..fixtures import get_parsed_xml_module
+
+
+class TestHardcodedValuesCheckerSingleSteps(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = HardcodedValuesChecker
+ XML_SUBPATH = "hardcoded_dispense_volume"
+
+ def test_When_Single_Channel_Using_variable__Then_no_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "one-channel-variable"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[0])
+
+ def test_When_Single_Channel_DispenseAll_and_all_others_Variable__Then_no_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "one-channel-dispense-all-other-channel-variable"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[0])
+
+ def test_When_one_variable_and_one_hardcoded__Then_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "one-channel-variable-other-hardcoded"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "hardcoded-dispense-volume",
+ args=("F09: 2.9 uL",),
+ line=ANY,
+ )
+ ):
+ self.checker.visit_step(steps[0])
+
+ def test_When_two_hardcoded__Then_error_shows_both_wells(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "two-channels-hardcoded"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "hardcoded-dispense-volume",
+ args=("C07: 22.1 uL, F09: 5.6 uL",),
+ line=ANY,
+ )
+ ):
+ self.checker.visit_step(steps[0])
+
+
+class TestHardcodedValuesCheckerWholeModule(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = HardcodedValuesChecker
+ XML_SUBPATH = "hardcoded_dispense_volume"
+
+ def test_When_one_variable_and_one_hardcoded__Then_error(self) -> None:
+ xml_module = get_parsed_xml_module(os.path.join(self.XML_SUBPATH, "one-channel-variable-other-hardcoded.xml"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "hardcoded-dispense-volume",
+ args=("F09: 2.9 uL",),
+ line=0,
+ )
+ ):
+ self.checker.process_module(xml_module)
diff --git a/tests/unit/robolint/checkers/test_hardcoded_mix_values.py b/tests/unit/robolint/checkers/test_hardcoded_mix_values.py
new file mode 100644
index 0000000..75074d3
--- /dev/null
+++ b/tests/unit/robolint/checkers/test_hardcoded_mix_values.py
@@ -0,0 +1,59 @@
+import os
+from unittest.mock import ANY
+
+from pylint.testutils import MessageTest
+from robolint import HardcodedValuesChecker
+from robolint.test_utils import RobolintCheckerTestCase
+
+from ..fixtures import get_parsed_steps
+from ..fixtures import get_parsed_xml_module
+
+
+class TestHardcodedValuesCheckerSingleSteps(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = HardcodedValuesChecker
+ XML_SUBPATH = "hardcoded_mix_volume"
+
+ def test_When_one_variable_and_one_hardcoded__Then_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "one-channel-variable-other-hardcoded"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "hardcoded-mix-volume",
+ args=("D05: 2.3 uL",),
+ line=ANY,
+ )
+ ):
+ self.checker.visit_step(steps[0])
+
+ def test_When_one_variable__Then_no_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "one-channel-variable"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[0])
+
+ def test_When_two_hardcoded__Then_error_shows_both_wells(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "two-channels-hardcoded"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "hardcoded-mix-volume",
+ args=("G04: 12.8 uL, B10: 9.3 uL",),
+ line=ANY,
+ )
+ ):
+ self.checker.visit_step(steps[0])
+
+
+class TestHardcodedValuesCheckerWholeModule(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = HardcodedValuesChecker
+ XML_SUBPATH = "hardcoded_mix_volume"
+
+ def test_When_one_variable_and_one_hardcoded__Then_error(self) -> None:
+ xml_module = get_parsed_xml_module(os.path.join(self.XML_SUBPATH, "one-channel-variable-other-hardcoded.xml"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "hardcoded-mix-volume",
+ args=("D05: 2.3 uL",),
+ line=ANY,
+ )
+ ):
+ self.checker.process_module(xml_module)
diff --git a/tests/unit/robolint/checkers/test_labware_names.py b/tests/unit/robolint/checkers/test_labware_names.py
new file mode 100644
index 0000000..bc52e1e
--- /dev/null
+++ b/tests/unit/robolint/checkers/test_labware_names.py
@@ -0,0 +1,126 @@
+import os
+from unittest.mock import ANY
+
+from pylint.testutils import MessageTest
+from pylint.testutils import set_config
+from robolint.checkers.labware import LabwareNameChecker
+from robolint.utils import parse_xml_module_from_file
+from robolint.utils import RobolintCheckerTestCase
+from stdlib_utils import get_current_file_abs_directory
+
+
+PATH_TO_CURRENT_FILE = get_current_file_abs_directory()
+PATH_TO_XMLS = os.path.join(PATH_TO_CURRENT_FILE, "..", "xml")
+
+
+class TestLabwareNameCheckerCheckerSingleSteps(RobolintCheckerTestCase):
+ CHECKER_CLASS = LabwareNameChecker
+
+ def test_when_no_labware_definitions_then_no_errors(self) -> None:
+ filepath = os.path.join(PATH_TO_XMLS, "labware-names", "no-labware-definitions.xml")
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertNoMessages():
+ self.checker.process_module(tree)
+
+ @set_config(labware_rgx="non-matching-pattern") # type:ignore[misc] # `untyped decorator`
+ def test_When_names_does_not_match_pattern__Then_error(self) -> None:
+ filepath = os.path.join(PATH_TO_XMLS, "labware-names", "single-aspirate-step-with-invalid-labware-name.xml")
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertAddsMessages(
+ MessageTest(
+ LabwareNameChecker.name,
+ args=("384 PCR BioRad"),
+ line=0,
+ )
+ ):
+ self.checker.process_module(tree)
+
+ def test_When_names_pattern__Then_no_error(self) -> None:
+ filepath = os.path.join(PATH_TO_XMLS, "labware-names", "single-aspirate-step-with-valid-labware-name.xml")
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertNoMessages():
+ self.checker.process_module(tree)
+
+ def test_Given_module_processed_with_bad_labware__When_module_processed_with_good_labware_with_same_plate_name__Then_no_error(
+ self,
+ ) -> None:
+ filepath = os.path.join(PATH_TO_XMLS, "labware-names", "single-aspirate-step-with-invalid-labware-name.xml")
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertAddsMessages(
+ MessageTest(
+ LabwareNameChecker.name,
+ args=("384 PCR BioRad"),
+ line=0,
+ )
+ ):
+ self.checker.process_module(tree)
+
+ filepath = os.path.join(
+ PATH_TO_XMLS,
+ "labware-names",
+ "single-aspirate-step-with-valid-labware-but-same-platename-as-invalid-file.xml",
+ )
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertNoMessages():
+ self.checker.process_module(tree)
+
+ def test_With_two_aspirate_steps_and_invalid_labware__Then_two_messages(self) -> None:
+ filepath = os.path.join(PATH_TO_XMLS, "labware-names", "two-aspirate-steps-with-labware-names.xml")
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertAddsMessages(
+ MessageTest(
+ LabwareNameChecker.name,
+ args=("96 Alpaqua Magnet"),
+ line=ANY,
+ ),
+ MessageTest(
+ LabwareNameChecker.name,
+ args=("96 Alpaqua Magnet"),
+ line=ANY,
+ ),
+ ):
+ self.checker.process_module(tree)
+
+ def test_With_no_plate_specified_it_works(self) -> None:
+ filepath = os.path.join(PATH_TO_XMLS, "labware-names", "single-aspirate-step-no-plate-specified.xml")
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertNoMessages():
+ self.checker.process_module(tree)
+
+ def test_with_dispense_step_it_works(self) -> None:
+ filepath = os.path.join(PATH_TO_XMLS, "labware-names", "dispense-step-with-labware-name.xml")
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertNoMessages():
+ self.checker.process_module(tree)
+
+ def test_with_mix_step_it_works(self) -> None:
+ filepath = os.path.join(PATH_TO_XMLS, "labware-names", "mix-step-with-labware-name.xml")
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertNoMessages():
+ self.checker.process_module(tree)
+
+ def test_with_move_to_plate_gripper_step_it_works(self) -> None:
+ filepath = os.path.join(PATH_TO_XMLS, "labware-names", "move-to-plate-gripper-step-with-labware-name.xml")
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertNoMessages():
+ self.checker.process_module(tree)
+
+ def test_with_move_to_plate_step_it_works(self) -> None:
+ filepath = os.path.join(PATH_TO_XMLS, "labware-names", "move-to-plate-step-with-labware-name.xml")
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertNoMessages():
+ self.checker.process_module(tree)
+
+ def test_with_multi_dispense_step_it_works(self) -> None:
+ filepath = os.path.join(PATH_TO_XMLS, "labware-names", "multi-dispense-step-with-labware-name.xml")
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertNoMessages():
+ self.checker.process_module(tree)
+
+ def test_When_move_to_plate_step_uses_variable_for_plate__Then_no_error(self) -> None:
+ filepath = os.path.join(
+ PATH_TO_XMLS, "labware-names", "submethod-inheriting-worktable-with-move-to-plate-and-load-tips.xml"
+ )
+ tree = parse_xml_module_from_file(filepath)
+ with self.assertNoMessages():
+ self.checker.process_module(tree)
diff --git a/tests/unit/robolint/checkers/test_looping.py b/tests/unit/robolint/checkers/test_looping.py
new file mode 100644
index 0000000..421c7bb
--- /dev/null
+++ b/tests/unit/robolint/checkers/test_looping.py
@@ -0,0 +1,91 @@
+import os
+
+from pylint.testutils import MessageTest
+from pylint.testutils import set_config
+from robolint import LoopIndexChecker
+from robolint.test_utils import RobolintCheckerTestCase
+
+from ..fixtures import get_parsed_steps
+from ..fixtures import get_parsed_xml_module
+
+
+class TestLoopIndexCheckerSingleSteps(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = LoopIndexChecker
+ XML_SUBPATH = "invalid_loop_start_index"
+
+ @set_config(loop_start_index=0) # type:ignore[misc] # mypy is throwing an error saying this decorator is untyped
+ def test_Given_checker_set_for_0__When_Loop_starts_at_0__Then_no_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "loop-starting-at-zero"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[0])
+
+ @set_config(loop_start_index=0) # type:ignore[misc] # mypy is throwing an error saying this decorator is untyped
+ def test_When_Loop_bound_to_variable_and_hardcoded_to_3__Then_no_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "loop-bound-to-variable-but-hardcoded-to-3"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[0])
+
+ @set_config(loop_start_index=0) # type:ignore[misc] # mypy is throwing an error saying this decorator is untyped
+ def test_Given_checker_set_for_0__When_Loop_starts_at_1__Then_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "loop-starting-at-one"))
+ step_index_to_test = 0
+ with self.assertAddsMessages(
+ MessageTest(
+ "invalid-loop-start-index",
+ args=(1, 0),
+ line=step_index_to_test,
+ )
+ ):
+ self.checker.visit_step(steps[step_index_to_test])
+
+ @set_config(loop_start_index=0) # type:ignore[misc] # mypy is throwing an error saying this decorator is untyped
+ def test_Given_checker_set_for_0__When_Loop_starts_at_1_at_2nd_step__Then_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "loop-starting-at-one-at-second-step"))
+ step_index_to_test = 1
+ with self.assertAddsMessages(
+ MessageTest(
+ "invalid-loop-start-index",
+ args=(1, 0),
+ line=step_index_to_test,
+ )
+ ):
+ self.checker.visit_step(steps[step_index_to_test])
+
+ @set_config(loop_start_index=1) # type:ignore[misc] # mypy is throwing an error saying this decorator is untyped
+ def test_Given_checker_set_for_1__When_Loop_starts_at_1__Then_no_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "loop-starting-at-one"))
+ step_index_to_test = 0
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[step_index_to_test])
+
+ @set_config(loop_start_index=1) # type:ignore[misc] # mypy is throwing an error saying this decorator is untyped
+ def test_Given_checker_set_for_1__When_Loop_starts_at_0__Then_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "loop-starting-at-zero"))
+ step_index_to_test = 0
+ with self.assertAddsMessages(
+ MessageTest(
+ "invalid-loop-start-index",
+ args=(0, 1),
+ line=step_index_to_test,
+ )
+ ):
+ self.checker.visit_step(steps[step_index_to_test])
+
+
+class TestLoopIndexCheckerWholeModule(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = LoopIndexChecker
+ XML_SUBPATH = "invalid_loop_start_index"
+
+ @set_config(loop_start_index=0) # type:ignore[misc] # mypy is throwing an error saying this decorator is untyped
+ def test_Given_checker_set_for_0__When_Loop_starts_at_1__Then_error(self) -> None:
+ xml_module = get_parsed_xml_module(os.path.join(self.XML_SUBPATH, "loop-starting-at-one.xml"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "invalid-loop-start-index",
+ args=(1, 0),
+ line=0,
+ )
+ ):
+ self.checker.process_module(xml_module)
diff --git a/tests/unit/robolint/checkers/test_robocase.py b/tests/unit/robolint/checkers/test_robocase.py
new file mode 100644
index 0000000..d8c3105
--- /dev/null
+++ b/tests/unit/robolint/checkers/test_robocase.py
@@ -0,0 +1,25 @@
+import pytest
+from robolint.checkers.robocase import is_robocase
+from robolint.checkers.robocase import to_robocase
+
+
+TEST_ITEMS = [
+ ("PascalCaseUnderscoreNumberSuffix_01", True, "PascalCaseUnderscoreNumberSuffix_01"),
+ ("PascalCaseUnderscoreNumberSuffixSingleDigit_1", False, "PascalCaseUnderscoreNumberSuffixSingleDigit_01"),
+ ("PascalCaseNoSuffix", True, "PascalCaseNoSuffix"),
+ ("PascalCaseNoUnderscoreSuffix01", False, "PascalCaseNoUnderscoreSuffix_01"),
+ ("camelCaseUnderscoreNumberSuffix_01", False, "CamelCaseUnderscoreNumberSuffix_01"),
+ ("camelCaseDashSuffix-01", False, "CamelCaseDashSuffix_01"),
+ ("snake_case_no_underscore_suffix01", False, "SnakeCaseNoUnderscoreSuffix_01"),
+ ("kebab-case-no-suffix", False, "KebabCaseNoSuffix"),
+]
+
+
+@pytest.mark.parametrize("text,result,_", TEST_ITEMS)
+def test_is_robocase(text: str, result: bool, _: str) -> None:
+ assert is_robocase(text) is result
+
+
+@pytest.mark.parametrize("text,_,result", TEST_ITEMS)
+def test_to_robocase(text: str, _: bool, result: str) -> None:
+ assert to_robocase(text) == result
diff --git a/tests/unit/robolint/checkers/test_step_checker.py b/tests/unit/robolint/checkers/test_step_checker.py
new file mode 100644
index 0000000..1ac5434
--- /dev/null
+++ b/tests/unit/robolint/checkers/test_step_checker.py
@@ -0,0 +1,48 @@
+import os
+
+from defusedxml.ElementTree import parse
+from overrides import override
+import pytest
+from pytest_mock import MockerFixture
+from robolint import HardcodedValuesChecker
+from robolint import MM4Step
+from robolint import NoStepTypeIdError
+from robolint import RoboLinter
+from robolint import StepChecker
+from robolint.test_utils import RobolintCheckerTestCase
+from robolint.utils import XmlModule
+
+from ..fixtures import get_parsed_steps
+from ..fixtures import PATH_TO_XMLS
+
+
+def test_Given_no_command_type_provided__When_init__Then_error() -> None:
+ class DummyStepChecker(StepChecker):
+ @override
+ def check_step(self, step: MM4Step) -> None:
+ pass
+
+ with pytest.raises(NoStepTypeIdError, match="DummyStepChecker"):
+ DummyStepChecker(RoboLinter())
+
+
+class TestCheckerWholeModule(RobolintCheckerTestCase):
+ # this is just using the aspirate values checker as a representative implemented class to check the functions in the base `StepChecker` class
+ CHECKER_CLASS = HardcodedValuesChecker
+
+ def test_When_Method_contains_multiple_steps__Then_only_aspirate_steps_are_visited(
+ self, mocker: MockerFixture
+ ) -> None:
+ subpath = os.path.join("hardcoded-aspiration-volume", "rule-disabled-single-active-channel-hardcoded.xml")
+ file = os.path.join(PATH_TO_XMLS, subpath)
+ steps = get_parsed_steps(subpath)
+ xml = XmlModule()
+ xml.tree = parse(file)
+ xml.file = file
+ expected_aspirate_steps_count = 1
+
+ assert len(steps) > expected_aspirate_steps_count
+ spied_visit_step = mocker.spy(HardcodedValuesChecker, "visit_step")
+ self.checker.process_module(xml)
+
+ assert spied_visit_step.call_count == expected_aspirate_steps_count
diff --git a/tests/unit/robolint/checkers/test_tip_eject_height.py b/tests/unit/robolint/checkers/test_tip_eject_height.py
new file mode 100644
index 0000000..eeadbf6
--- /dev/null
+++ b/tests/unit/robolint/checkers/test_tip_eject_height.py
@@ -0,0 +1,51 @@
+import os
+from unittest.mock import ANY
+
+from pylint.testutils import MessageTest
+from pylint.testutils import set_config
+from robolint import TipWasteEjectHeightChecker
+from robolint.test_utils import RobolintCheckerTestCase
+
+from ..fixtures import get_parsed_steps
+from ..fixtures import get_parsed_xml_module
+
+
+class TestTipHeightSingleSteps(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = TipWasteEjectHeightChecker
+ XML_SUBPATH = "tip_height"
+
+ def test_Given_tip_height_is_15_and_default_is_50__Then_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "tip-height-of-15.xml"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "invalid-tip-waste-eject-height",
+ args=(15, 50),
+ line=ANY,
+ )
+ ):
+ self.checker.visit_step(steps[0])
+
+
+class TestTipHeightWholeModule(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = TipWasteEjectHeightChecker
+ XML_SUBPATH = "tip_height"
+
+ @set_config(tip_waste_eject_height=15) # type:ignore[misc] # `untyped decorator`
+ def test_Given_tip_height_is_15_and_locally_set_config_value_is_15__Then_pass(self) -> None:
+ xml_module = get_parsed_xml_module(os.path.join(self.XML_SUBPATH, "tip-height-of-15.xml"))
+ with self.assertNoMessages():
+ self.checker.process_module(xml_module)
+
+ @set_config(tip_waste_chute_name="") # type:ignore[misc] # `untyped decorator`
+ def test_Given_no_tip_waste_chute_name_specified_then_check_is_not_performed(self) -> None:
+ xml_module = get_parsed_xml_module(os.path.join(self.XML_SUBPATH, "tip-height-of-15.xml"))
+ with self.assertNoMessages():
+ self.checker.process_module(xml_module)
+
+ @set_config(tip_waste_chute_name="dummy.*") # type:ignore[misc] # `untyped decorator`
+ def test_Given_tip_waste_chute_name_will_not_match_then_check_is_not_performed(self) -> None:
+ xml_module = get_parsed_xml_module(os.path.join(self.XML_SUBPATH, "tip-height-of-15.xml"))
+ with self.assertNoMessages():
+ self.checker.process_module(xml_module)
diff --git a/tests/unit/robolint/checkers/test_tip_motion_profile.py b/tests/unit/robolint/checkers/test_tip_motion_profile.py
new file mode 100644
index 0000000..e3c4e05
--- /dev/null
+++ b/tests/unit/robolint/checkers/test_tip_motion_profile.py
@@ -0,0 +1,134 @@
+import os
+import re
+from unittest.mock import ANY
+
+from pylint.testutils import MessageTest
+from pylint.testutils import set_config
+import pytest
+from robolint import TipEjectChecker
+from robolint import TipLoadChecker
+from robolint.test_utils import RobolintCheckerTestCase
+
+from ..fixtures import get_parsed_steps
+from ..fixtures import get_parsed_xml_module
+
+
+class TestPylintRegexpCsvExpectations(RobolintCheckerTestCase):
+ """Testing of `Pylint` expectations.
+
+ `"type": "regexp_csv"` splits on commas but does not respect `CSV` parsing rules.
+ As such, it doesn't appear to support commas within expressions.
+ In code though, it supports `str | list[Pattern[str]]`.
+ """
+
+ CHECKER_CLASS = TipLoadChecker
+
+ def test_When_tip_load_motion_profile_config_is_invalid_regex__Then_raises_system_exit(self) -> None:
+ with pytest.raises(SystemExit):
+ self.linter.set_option("tip-load-profile", "\\")
+
+ @pytest.mark.xfail(raises=SystemExit, reason="Pylint regexp_csv option is not CSV compliant.")
+ def test_When_config_option_regexp_csv_has_escaped_comma__Then_pylint_can_process_it(self) -> None:
+ self.linter.set_option("tip-load-profile", "a pattern containing \\,comma")
+
+ def test_regexp_csv_config_option_supports_list_of_pattern_objects(self) -> None:
+ self.linter.set_option("tip-load-profile", [re.compile(r"apattern")])
+ option = self.linter.config.tip_load_profile
+ assert isinstance(option, list)
+ assert len(option) == 1
+ assert isinstance(option[0], re.Pattern)
+
+ def test_When_tip_load_motion_profile_config_has_multiple_patterns__Then_we_get_a_list_of_patterns(self) -> None:
+ self.linter.set_option("tip-load-profile", r"first,second")
+ option = self.linter.config.tip_load_profile
+ assert isinstance(option, list)
+ assert len(option) == 2
+ assert isinstance(option[0], re.Pattern)
+
+
+class TestTipLoadMotionProfileSingleSteps(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = TipLoadChecker
+ XML_SUBPATH = "tip_motion_profile"
+
+ @set_config(tip_load_profile="Tip Load.*") # type:ignore[misc] # `untyped decorator`
+ def test_Given_tip_load_motion_profile_is_mid_speed__Then_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "load-tips-with-invalid-mid-speed-profile.xml"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "invalid-tip-load-profile",
+ args=("Mid Speed", "Tip Load.*"),
+ line=ANY,
+ )
+ ):
+ self.checker.visit_step(steps[0])
+
+ @set_config(tip_load_profile="Tip Load.*") # type:ignore[misc] # `untyped decorator`
+ def test_When_tip_load_motion_profile_is_already_correct__Then_no_message(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "load-tips-with-valid-tip-load-profile.xml"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[0])
+
+ @set_config(tip_load_profile="") # type:ignore[misc] # `untyped decorator`
+ def test_When_tip_load_motion_profile_config_is_empty__Then_no_message(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "load-tips-with-invalid-mid-speed-profile.xml"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[0])
+
+
+class TestTipEjectMotionProfileSingleSteps(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = TipEjectChecker
+ XML_SUBPATH = "tip_motion_profile"
+
+ @set_config(tip_eject_profile="Tip Eject.*") # type:ignore[misc] # `untyped decorator`
+ def test_Given_tip_eject_motion_profile_is_fast_speed__Then_error(self) -> None:
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "eject-tips-with-invalid-fast-speed-profile.xml"))
+ with self.assertAddsMessages(
+ MessageTest(
+ "invalid-tip-eject-profile",
+ args=("Fast Speed", "Tip Eject.*"),
+ line=ANY,
+ )
+ ):
+ self.checker.visit_step(steps[0])
+
+
+class TestTipEjectMotionProfileCheckerWholeModule(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = TipEjectChecker
+ XML_SUBPATH = "tip_motion_profile"
+
+ @set_config(tip_eject_profile="Tip Eject.*") # type:ignore[misc] # `untyped decorator`
+ def test_Given_tip_eject_motion_profile_must_match_tip_eject__When_profile_is_fast_speed__Then_error(self) -> None:
+ xml_module = get_parsed_xml_module(
+ os.path.join(self.XML_SUBPATH, "eject-tips-with-invalid-fast-speed-profile.xml")
+ )
+ with self.assertAddsMessages(
+ MessageTest(
+ "invalid-tip-eject-profile",
+ args=("Fast Speed", "Tip Eject.*"),
+ line=ANY,
+ )
+ ):
+ self.checker.process_module(xml_module)
+
+
+class TestTipLoadMotionProfileCheckerWholeModule(RobolintCheckerTestCase):
+
+ CHECKER_CLASS = TipLoadChecker
+ XML_SUBPATH = "tip_motion_profile"
+
+ @set_config(tip_load_profile="Tip Load.*") # type:ignore[misc] # `untyped decorator`
+ def test_Given_tip_load_motion_profile_must_match_tip_load__When_profile_is_fast_speed__Then_error(self) -> None:
+ xml_module = get_parsed_xml_module(
+ os.path.join(self.XML_SUBPATH, "load-tips-with-invalid-mid-speed-profile.xml")
+ )
+ with self.assertAddsMessages(
+ MessageTest(
+ "invalid-tip-load-profile",
+ args=("Mid Speed", "Tip Load.*"),
+ line=ANY,
+ )
+ ):
+ self.checker.process_module(xml_module)
diff --git a/tests/unit/robolint/checkers/test_variable_names.py b/tests/unit/robolint/checkers/test_variable_names.py
new file mode 100644
index 0000000..d42decc
--- /dev/null
+++ b/tests/unit/robolint/checkers/test_variable_names.py
@@ -0,0 +1,145 @@
+import os
+from typing import Callable
+from unittest.mock import patch
+
+import humps
+from mock import MagicMock
+from pylint.testutils import MessageTest
+import pytest
+from robolint.checkers.robocase import RobocaseVariableNameChecker
+from robolint.checkers.variables import VariableNameChecker
+from robolint.test_utils import RobolintCheckerTestCase
+
+from ..fixtures import get_parsed_steps
+from ..fixtures import get_parsed_xml_module
+
+
+class TestVariableCaseInitialized(RobolintCheckerTestCase):
+ CHECKER_CLASS = VariableNameChecker
+ XML_SUBPATH = "variable-names"
+
+ @pytest.mark.parametrize(
+ "variable_name_case, example_filename, check_function, case_function",
+ [
+ ("pascal", "variable-name-in-pascal-case", humps.is_pascalcase, humps.pascalize),
+ ("camel", "variable-name-in-camel-case", humps.is_camelcase, humps.camelize),
+ ("kebab", "variable-name-in-kebab-case", humps.is_kebabcase, humps.kebabize),
+ ("snake", "variable-name-in-snake-case", humps.is_snakecase, humps.decamelize),
+ ],
+ )
+ def test_When_variable_name_case_config_option_set__Then_case_functions_correct(
+ self,
+ variable_name_case: str,
+ example_filename: str,
+ check_function: Callable[[str], bool],
+ case_function: Callable[[str], str],
+ ) -> None:
+ self.checker.linter.set_option("variable-name-case", variable_name_case)
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, example_filename))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[1])
+ assert self.checker.case_check_function is check_function
+ assert self.checker.case_function is case_function
+
+ def test_When_invalid_variable_name_case_config_option_set__Then_ValueError(self) -> None:
+ self.checker.linter.set_option("variable-name-case", "dummy")
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "variable-name-in-pascal-case"))
+ with pytest.raises(ValueError) as err:
+ self.checker.visit_step(steps[1])
+ assert "Invalid variable name case" in str(err.value)
+
+
+class TestVariableNamingInSteps(RobolintCheckerTestCase):
+ CHECKER_CLASS = RobocaseVariableNameChecker
+ XML_SUBPATH = "variable-names"
+
+ def test_When_variable_in_pascal_case_and_no_suffix__Then_no_messages(self) -> None:
+ self.checker.linter.set_option("variable-name-case", "robocase")
+ xml_module = get_parsed_xml_module(os.path.join(self.XML_SUBPATH, "variable-name-in-pascal-case"))
+ with self.assertNoMessages():
+ self.checker.process_module(xml_module)
+
+ def test_When_variable_in_kebab_case_Then_message(self) -> None:
+ self.checker.linter.set_option("variable-name-case", "robocase")
+ xml_module = get_parsed_xml_module(os.path.join(self.XML_SUBPATH, "variable-name-in-kebab-case"))
+ with self.assertAddsMessages(
+ MessageTest("invalid-variable-case", args=("loop-counter", "LoopCounter"), line=1),
+ MessageTest("invalid-variable-case", args=("loop-counter", "LoopCounter"), line=2),
+ ):
+ self.checker.process_module(xml_module)
+
+ def test_When_variable_contains_abbreviation_then_message(self) -> None:
+ self.checker.linter.set_option("variable-name-case", "robocase")
+ self.checker.linter.set_option("variable-name-abbreviations", "Cycles,Cyc")
+ xml_module = get_parsed_xml_module(os.path.join(self.XML_SUBPATH, "variable-name-contains-abbreviation"))
+ with self.assertAddsMessages(
+ MessageTest("non-abbreviated-variable", args=("NumCycles", "Cycles", "Cyc"), line=1),
+ MessageTest("non-abbreviated-variable", args=("NumCycles", "Cycles", "Cyc"), line=2),
+ ):
+ self.checker.process_module(xml_module)
+
+ def test_When_abbreviation_csv_is_missing_an_item__Then_value_error(self) -> None:
+ self.checker.linter.set_option("variable-name-case", "robocase")
+ self.checker.linter.set_option("variable-name-abbreviations", "Cycles,")
+ xml_module = get_parsed_xml_module(os.path.join(self.XML_SUBPATH, "variable-name-contains-abbreviation"))
+ with pytest.raises(ValueError):
+ self.checker.process_module(xml_module)
+
+ def test_When_variable_contains_single_digit_suffix_then_message(self) -> None:
+ self.checker.linter.set_option("variable-name-case", "robocase")
+ xml_module = get_parsed_xml_module(os.path.join(self.XML_SUBPATH, "variable-name-contains-single-digit-suffix"))
+ with self.assertAddsMessages(
+ MessageTest("invalid-variable-case", args=("LoopCounter1", "LoopCounter_01"), line=1),
+ MessageTest("invalid-variable-case", args=("LoopCounter1", "LoopCounter_01"), line=2),
+ ):
+ self.checker.process_module(xml_module)
+
+ @patch("robolint.RobocaseVariableNameChecker._run_checks")
+ def test_When_VariableValueVariable_and_ValueVariable_present_in_begin_loop_step_Then_checker_called_twice(
+ self, mocked_func: MagicMock
+ ) -> None:
+ self.checker.linter.set_option("variable-name-case", "robocase")
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "loop-with-variable-name-variable-and-value-variable"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[0])
+ assert set(["LoopCounter"]) == self.checker.variables_observed
+ assert mocked_func.call_count == 2
+
+ @patch("robolint.RobocaseVariableNameChecker._run_checks")
+ def test_When_AssignToVariable_present_in_expression_step_Then_checker_called_once(
+ self, mocked_func: MagicMock
+ ) -> None:
+ self.checker.linter.set_option("variable-name-case", "robocase")
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "loop-with-variable-name-variable-and-value-variable"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[1])
+ assert set(["LoopCounter"]) == self.checker.variables_observed
+ assert mocked_func.call_count == 1
+
+ @patch("robolint.RobocaseVariableNameChecker._run_checks")
+ def test_When_VariableName_and_ValueVariable_present_in_end_loop_step_Then_checker_called_twice(
+ self, mocked_func: MagicMock
+ ) -> None:
+ self.checker.linter.set_option("variable-name-case", "robocase")
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "loop-with-variable-name-variable-and-value-variable"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[2])
+ assert set(["LoopCounter"]) == self.checker.variables_observed
+ assert mocked_func.call_count == 2
+
+
+class TestAspirateStepVariableForPlateAndVolume(RobolintCheckerTestCase):
+ CHECKER_CLASS = RobocaseVariableNameChecker
+ XML_SUBPATH = "variable-names"
+
+ @patch("robolint.RobocaseVariableNameChecker._run_checks")
+ def test_When_aspirate_step_contains_plateVariable_and_volumeVariable_Then_nodes_are_visited_And_expected_names_found(
+ self, mocked_func: MagicMock
+ ) -> None:
+ self.checker.linter.set_option("variable-name-case", "robocase")
+ steps = get_parsed_steps(os.path.join(self.XML_SUBPATH, "aspirate-step-variable-for-plate-and-volume"))
+ with self.assertNoMessages():
+ self.checker.visit_step(steps[-1])
+ # 1 use of plateVariable containing 'SrcPlate', 96 uses of volumeVariable containing 'Volume'
+ assert mocked_func.call_count == 97
+ assert set(["SrcPlate", "Volume"]) == self.checker.variables_observed
diff --git a/tests/unit/robolint/fixtures.py b/tests/unit/robolint/fixtures.py
new file mode 100644
index 0000000..a1a8e05
--- /dev/null
+++ b/tests/unit/robolint/fixtures.py
@@ -0,0 +1,40 @@
+import os
+
+from robolint import MM4Step
+from robolint import parse_steps_from_etree
+from robolint import parse_xml_module_from_file
+from robolint import XmlModule
+from stdlib_utils import get_current_file_abs_directory
+
+PATH_TO_CURRENT_FILE = get_current_file_abs_directory()
+PATH_TO_XMLS = os.path.join(PATH_TO_CURRENT_FILE, "xml")
+
+PARSED_STEPS: dict[str, list[MM4Step]] = {}
+
+PARSED_XML_MODULES: dict[str, XmlModule] = {}
+
+
+def get_parsed_xml_module(filepath_within_xml_dir: str) -> XmlModule:
+ if not filepath_within_xml_dir.endswith(".xml"):
+ filepath_within_xml_dir += ".xml"
+ try:
+ return PARSED_XML_MODULES[filepath_within_xml_dir]
+ except KeyError:
+ pass
+ parsed_xml_module = parse_xml_module_from_file(os.path.join(PATH_TO_XMLS, filepath_within_xml_dir))
+ PARSED_XML_MODULES[filepath_within_xml_dir] = parsed_xml_module
+ return parsed_xml_module
+
+
+def get_parsed_steps(filepath_within_xml_dir: str) -> list[MM4Step]:
+ if not filepath_within_xml_dir.endswith(".xml"):
+ filepath_within_xml_dir += ".xml"
+ try:
+ return PARSED_STEPS[filepath_within_xml_dir]
+ except KeyError:
+ pass
+ xml_module = get_parsed_xml_module(filepath_within_xml_dir)
+
+ parsed_steps = parse_steps_from_etree(xml_module.tree.getroot())
+ PARSED_STEPS[filepath_within_xml_dir] = parsed_steps
+ return parsed_steps
diff --git a/tests/unit/robolint/regex/__init__.py b/tests/unit/robolint/regex/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/robolint/regex/test_labware_name_regex.py b/tests/unit/robolint/regex/test_labware_name_regex.py
new file mode 100644
index 0000000..fb26322
--- /dev/null
+++ b/tests/unit/robolint/regex/test_labware_name_regex.py
@@ -0,0 +1,43 @@
+import re
+
+import pytest
+from pytest import param
+from robolint.checkers.labware import LABWARE_REGEX
+
+LABWARE_PATTERN = re.compile(LABWARE_REGEX, re.VERBOSE)
+
+
+@pytest.mark.parametrize(
+ "labware_name",
+ [
+ param("384 PCR BioRad HSP38xx", id="catalog number with uppercase, numeric, and lowercase"),
+ param("1 Well Reservoir Agilent 204017-100"),
+ param("12 Col Reservoir Agilent 204095-100"),
+ param("384 F-Bottom ProxiPlate PerkinElmer 6008280", id="all numeric catalog number"),
+ param("384 F-Bottom MTP Corning 3985", id="hyphenated description"),
+ param("24 F-Bottom MTP Deutz CR1424dg", id="catalog number ending in lowercase"),
+ param("96 500 uL Tubes Azenta 68-0703-11", id="uL volume in description"),
+ param("96 500uL Tubes Azenta 68-0703-11", id="uL volume adjacent to number without space"),
+ param("96 1 mL Tubes Azenta 68-0703-11", id="mL volume in description"),
+ param("96 Nanoplate QIAcuity Qiagen 250021", id="multiple capital letters in plate name"),
+ param("24 F-Bottom MTP Deutz CR1424dg 96w-access", id="96-well access modified labware definition"),
+ param("24 F-Bottom MTP Deutz CR1424dg 384w-access", id="384-well access modified labware definition"),
+ ],
+)
+def test_When_labware_is_valid__Then_regex_match(labware_name: str) -> None:
+ assert re.fullmatch(LABWARE_PATTERN, labware_name) is not None
+
+
+@pytest.mark.parametrize(
+ "labware_name",
+ [
+ param("384 F-bottom MTP Corning 3985", id="non-capitalized bottom for F-bottom"),
+ param("96 500 ul Tubes Azenta 68-0703-11", id="lowercase microliters"),
+ param("96 1 ml Tubes Azenta 68-0703-11", id="lowercase milliliters"),
+ param("24 F-Bottom MTP Deutz CR1424dg 96w access", id="96-well access without required hyphen"),
+ param("24 F-Bottom MTP Deutz CR1424dg 96w-Access", id="96-well access capitalized A"),
+ param("24 F-Bottom MTP Deutz CR1424dg 96W-access", id="96-well access capitalized W"),
+ ],
+)
+def test_When_labware_invalid__Then_no_regex_match(labware_name: str) -> None:
+ assert re.fullmatch(LABWARE_PATTERN, labware_name) is None
diff --git a/tests/unit/robolint/test_checker_error_is_handled.py b/tests/unit/robolint/test_checker_error_is_handled.py
new file mode 100644
index 0000000..1f45f75
--- /dev/null
+++ b/tests/unit/robolint/test_checker_error_is_handled.py
@@ -0,0 +1,67 @@
+import os
+
+from overrides import override
+from pylint.testutils import CheckerTestCase
+import pytest
+from pytest_mock import MockerFixture
+from robolint.checkers.base_checkers import StepChecker
+from robolint.run import RobolintRun
+from robolint.test_utils import INTERNAL_ROBOLINT_ERROR_EXIT_CODE
+from robolint.test_utils import RobolintCheckerTestCase
+from robolint.utils import MM4Step
+from stdlib_utils import get_current_file_abs_directory
+
+PATH_TO_CURRENT_FILE = get_current_file_abs_directory()
+PATH_TO_XMLS = os.path.join(PATH_TO_CURRENT_FILE, "xml")
+
+
+class DummyChecker(StepChecker):
+ step_type_id: set[str] = set()
+
+ @override
+ def check_step(self, step: MM4Step) -> None:
+ raise Exception( # pylint:disable=broad-exception-raised # Eli (20230714) deliberately raising a general exception
+ "Dummy Checker threw an Exception"
+ )
+
+
+class TestHandledByRobolintCheckerTestCase(RobolintCheckerTestCase):
+ CHECKER_CLASS = DummyChecker
+
+ def test_When_using_RobolintCheckerTestCase_and_StepChecker_raises_Exception_Then_exit_3(
+ self, mocker: MockerFixture
+ ) -> None:
+ spied_checker = mocker.spy(DummyChecker, "check_step")
+ with pytest.raises(SystemExit) as err:
+ RobolintRun(
+ args=[
+ "--disable=all",
+ os.path.join(
+ PATH_TO_XMLS, "hardcoded-aspiration-volume", "single-active-channel-with-variable.xml"
+ ),
+ ]
+ )
+ assert err.exception_code == INTERNAL_ROBOLINT_ERROR_EXIT_CODE, f"System exited with code {err.exception_code}" # type: ignore[attr-defined]
+ assert spied_checker.call_count == 1, f"DummyChecker.check_step was called {spied_checker.call_count} times"
+
+
+class TestNotHandledByCheckerTestCase(CheckerTestCase):
+ CHECKER_CLASS = DummyChecker
+
+ def test_When_using_CheckerTestCase_and_StepChecker_raises_Exception_Then_exit_0(
+ self, mocker: MockerFixture
+ ) -> None:
+ spied_checker = mocker.spy(DummyChecker, "check_step")
+ with pytest.raises(SystemExit) as err:
+ RobolintRun(
+ args=[
+ "--disable=all",
+ os.path.join(
+ PATH_TO_XMLS, "hardcoded-aspiration-volume", "single-active-channel-with-variable.xml"
+ ),
+ ]
+ )
+ assert (
+ err.exception_code == 0 # type: ignore[attr-defined]
+ ), "When utilizing robolint_prepare_crash_report, we should exit(0) with no SystemExit exception"
+ assert spied_checker.call_count == 1, f"DummyChecker.check_step was called {spied_checker.call_count} times"
diff --git a/tests/unit/robolint/test_robolinter.py b/tests/unit/robolint/test_robolinter.py
new file mode 100644
index 0000000..86cd1e8
--- /dev/null
+++ b/tests/unit/robolint/test_robolinter.py
@@ -0,0 +1,118 @@
+import os
+from unittest.mock import MagicMock
+
+from defusedxml.ElementTree import parse
+import pytest
+from pytest import CaptureFixture
+from pytest_mock import MockerFixture
+from robolint import HardcodedValuesChecker
+from robolint import RoboLinter
+from robolint.utils import XmlModule
+
+from .fixtures import PATH_TO_XMLS
+
+
+def test_When_run_on_file_with_error__Then_message_shows_up_in_stdout(capsys: CaptureFixture[str]) -> None:
+ # adapted from https://github.com/PyCQA/pylint/blob/main/tests/conftest.py#L31
+
+ linter = RoboLinter()
+ linter.register_checker(HardcodedValuesChecker(linter))
+
+ linter.check([os.path.join(PATH_TO_XMLS, "hardcoded-aspiration-volume", "one-variable-one-hardcoded.xml")])
+
+ out, err = capsys.readouterr()
+ assert err == ""
+ assert "hardcoded-aspirate-volume" in out
+
+
+def test_When_comment_disables_rule__Then_message_is_locally_disabled_and_suppressed(mock_print: MagicMock) -> None:
+ linter = RoboLinter()
+ linter.register_checker(HardcodedValuesChecker(linter))
+
+ linter.check(
+ [os.path.join(PATH_TO_XMLS, "hardcoded-aspiration-volume", "rule-disabled-single-active-channel-hardcoded.xml")]
+ )
+
+ assert linter.stats.by_msg == {"suppressed-message": 1, "locally-disabled": 1}
+
+
+def test_When_comment_does_not_disable_rule__Then_message_not_supressed(mock_print: MagicMock) -> None:
+ linter = RoboLinter()
+ linter.register_checker(HardcodedValuesChecker(linter))
+
+ linter.check(
+ [
+ os.path.join(
+ PATH_TO_XMLS,
+ "hardcoded-aspiration-volume",
+ "comment-not-disabling-rule-and-single-active-channel-hardcoded.xml",
+ )
+ ]
+ )
+
+ assert linter.stats.by_msg == {"hardcoded-aspirate-volume": 1}
+
+
+def test_When_comment_disabling_rule_is_not_a_valid_rule__Then_unknown_option_value_emitted_with_correct_line(
+ capsys: CaptureFixture[str],
+) -> None:
+ expected_line_number = 1
+
+ linter = RoboLinter()
+ linter.register_checker(HardcodedValuesChecker(linter))
+
+ linter.check(
+ [
+ os.path.join(
+ PATH_TO_XMLS,
+ "robolinter",
+ "invalid-rule-disabled-in-comment.xml",
+ )
+ ]
+ )
+
+ assert linter.stats.by_msg == {"unknown-option-value": 1}
+ out, err = capsys.readouterr()
+ assert err == ""
+ assert "robolint: disable" in out
+ assert f"xml:{expected_line_number}:0:" in out
+
+
+@pytest.mark.parametrize(
+ ",".join(("test_file_subpath", "expected_call_args", "test_description")),
+ [
+ (
+ os.path.join("hardcoded-aspiration-volume", "rule-disabled-single-active-channel-hardcoded.xml"),
+ (("hardcoded-aspirate-volume", 0),),
+ "file with clean comment for single rule",
+ ),
+ (
+ os.path.join(
+ "hardcoded-aspiration-volume", "messy-comment-rule-disabled-single-active-channel-hardcoded.xml"
+ ),
+ (("hardcoded-aspirate-volume", 0),),
+ "file with messy comment for single rule",
+ ),
+ (
+ os.path.join("robolinter", "two-rules-disabled-in-one-comment.xml"),
+ (("hardcoded-aspirate-volume", 0), ("invalid-variable-name", 0)),
+ "file with comment for two rules",
+ ),
+ ],
+)
+def test_When_process_tokens_run__Then_rule_disabled(
+ test_file_subpath: str,
+ expected_call_args: tuple[tuple[str, int], ...],
+ test_description: str,
+ mocker: MockerFixture,
+) -> None:
+ linter = RoboLinter()
+ mocked_disable_next = mocker.patch.object(linter, "disable_next", autospec=True)
+ file = os.path.join(PATH_TO_XMLS, test_file_subpath)
+ xml = XmlModule()
+ xml.file = file
+ xml.tree = parse(file)
+ linter.process_tokens(xml)
+ assert mocked_disable_next.call_count == len(expected_call_args)
+ for rule_name, line_number in expected_call_args:
+ mocked_disable_next.assert_any_call(rule_name, line=line_number)
diff --git a/tests/unit/robolint/utils/__init__.py b/tests/unit/robolint/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/robolint/utils/test_mm4_step.py b/tests/unit/robolint/utils/test_mm4_step.py
new file mode 100644
index 0000000..a4080bf
--- /dev/null
+++ b/tests/unit/robolint/utils/test_mm4_step.py
@@ -0,0 +1,55 @@
+import os
+
+from lxml.etree import _Element
+import pytest
+from pytest_mock import MockerFixture
+from robolint import MM4Step
+from robolint import parse_steps
+from robolint import utils
+
+from ..fixtures import PATH_TO_XMLS
+
+
+@pytest.fixture(scope="function", name="basic_step")
+def fixture_basic_step() -> MM4Step:
+ file = os.path.join(PATH_TO_XMLS, "hardcoded-aspiration-volume", "single-active-channel-with-variable.xml")
+ steps = parse_steps(file)
+ return steps[0]
+
+
+def test__When_parameters_node_accessed__Then_parameters_node_available(basic_step: MM4Step) -> None:
+
+ parameters_node = basic_step.parameters_node
+
+ assert isinstance(parameters_node, _Element)
+ assert parameters_node.attrib["name"] == "Parameters"
+
+
+def test__Given_parameters_node_already_accessed__When_parameters_node_accessed_again__Then_cached_value_used(
+ mocker: MockerFixture, basic_step: MM4Step
+) -> None:
+ spied_parse = mocker.spy(basic_step, "parse")
+ assert isinstance(basic_step.parameters_node, _Element)
+ original_call_count = spied_parse.call_count
+ assert original_call_count > 0
+
+ basic_step.parameters_node # pylint:disable=pointless-statement # this is the actual action of the test
+
+ assert spied_parse.call_count == original_call_count
+
+
+def test_When_get_parameter_called__Then_value_returned(basic_step: MM4Step) -> None:
+ assert basic_step.get_parameter("A01.enable") == "True"
+
+
+def test_Given_parameter_already_obtained__When_get_parameter_called__Then_cached_value_used(
+ basic_step: MM4Step, mocker: MockerFixture
+) -> None:
+ spied_find_element = mocker.spy(utils, "find_exactly_one_xml_element")
+ basic_step.get_parameter("A01.enable")
+ original_call_count = spied_find_element.call_count
+ assert original_call_count > 0
+
+ basic_step.get_parameter("A01.enable")
+
+ assert spied_find_element.call_count == original_call_count
diff --git a/tests/unit/robolint/utils/test_parse_steps.py b/tests/unit/robolint/utils/test_parse_steps.py
new file mode 100644
index 0000000..fa6f5b5
--- /dev/null
+++ b/tests/unit/robolint/utils/test_parse_steps.py
@@ -0,0 +1,17 @@
+import os
+
+from robolint import parse_steps
+
+from ..fixtures import PATH_TO_XMLS
+
+
+def test__When_single_step__Then_returns_one_step() -> None:
+ file = os.path.join(PATH_TO_XMLS, "hardcoded-aspiration-volume", "single-active-channel-with-variable.xml")
+ steps = parse_steps(file)
+ assert len(steps) == 1
+
+
+def test__When_multiple_step__Then_returns_multiple_steps() -> None:
+ file = os.path.join(PATH_TO_XMLS, "hardcoded-aspiration-volume", "single-active-channel-hardcoded.xml")
+ steps = parse_steps(file)
+ assert len(steps) == 2
diff --git a/tests/unit/robolint/xml/hardcoded-aspiration-volume/comment-not-disabling-rule-and-single-active-channel-hardcoded.xml b/tests/unit/robolint/xml/hardcoded-aspiration-volume/comment-not-disabling-rule-and-single-active-channel-hardcoded.xml
new file mode 100644
index 0000000..76a8993
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded-aspiration-volume/comment-not-disabling-rule-and-single-active-channel-hardcoded.xml
@@ -0,0 +1,485 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded-aspiration-volume/messy-comment-rule-disabled-single-active-channel-hardcoded.xml b/tests/unit/robolint/xml/hardcoded-aspiration-volume/messy-comment-rule-disabled-single-active-channel-hardcoded.xml
new file mode 100644
index 0000000..921a329
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded-aspiration-volume/messy-comment-rule-disabled-single-active-channel-hardcoded.xml
@@ -0,0 +1,481 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded-aspiration-volume/one-variable-one-hardcoded.xml b/tests/unit/robolint/xml/hardcoded-aspiration-volume/one-variable-one-hardcoded.xml
new file mode 100644
index 0000000..28917a0
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded-aspiration-volume/one-variable-one-hardcoded.xml
@@ -0,0 +1,477 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded-aspiration-volume/rule-disabled-single-active-channel-hardcoded.xml b/tests/unit/robolint/xml/hardcoded-aspiration-volume/rule-disabled-single-active-channel-hardcoded.xml
new file mode 100644
index 0000000..4890d40
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded-aspiration-volume/rule-disabled-single-active-channel-hardcoded.xml
@@ -0,0 +1,485 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded-aspiration-volume/single-active-channel-hardcoded.xml b/tests/unit/robolint/xml/hardcoded-aspiration-volume/single-active-channel-hardcoded.xml
new file mode 100644
index 0000000..f343579
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded-aspiration-volume/single-active-channel-hardcoded.xml
@@ -0,0 +1,605 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded-aspiration-volume/single-active-channel-not-a1-hardcoded.xml b/tests/unit/robolint/xml/hardcoded-aspiration-volume/single-active-channel-not-a1-hardcoded.xml
new file mode 100644
index 0000000..063c78b
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded-aspiration-volume/single-active-channel-not-a1-hardcoded.xml
@@ -0,0 +1,457 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded-aspiration-volume/single-active-channel-with-variable.xml b/tests/unit/robolint/xml/hardcoded-aspiration-volume/single-active-channel-with-variable.xml
new file mode 100644
index 0000000..eb87932
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded-aspiration-volume/single-active-channel-with-variable.xml
@@ -0,0 +1,461 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded-aspiration-volume/two-active-hardcoded-channels.xml b/tests/unit/robolint/xml/hardcoded-aspiration-volume/two-active-hardcoded-channels.xml
new file mode 100644
index 0000000..9fac437
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded-aspiration-volume/two-active-hardcoded-channels.xml
@@ -0,0 +1,477 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded-aspiration-volume/volume-property-set-even-though-variable-also-set.xml b/tests/unit/robolint/xml/hardcoded-aspiration-volume/volume-property-set-even-though-variable-also-set.xml
new file mode 100644
index 0000000..e6b9d3e
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded-aspiration-volume/volume-property-set-even-though-variable-also-set.xml
@@ -0,0 +1,823 @@
+ο»Ώ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded_dispense_volume/one-channel-dispense-all-other-channel-variable.xml b/tests/unit/robolint/xml/hardcoded_dispense_volume/one-channel-dispense-all-other-channel-variable.xml
new file mode 100644
index 0000000..94abde0
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded_dispense_volume/one-channel-dispense-all-other-channel-variable.xml
@@ -0,0 +1,477 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded_dispense_volume/one-channel-variable-other-hardcoded.xml b/tests/unit/robolint/xml/hardcoded_dispense_volume/one-channel-variable-other-hardcoded.xml
new file mode 100644
index 0000000..be18f86
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded_dispense_volume/one-channel-variable-other-hardcoded.xml
@@ -0,0 +1,481 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded_dispense_volume/one-channel-variable.xml b/tests/unit/robolint/xml/hardcoded_dispense_volume/one-channel-variable.xml
new file mode 100644
index 0000000..c67ffda
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded_dispense_volume/one-channel-variable.xml
@@ -0,0 +1,457 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded_dispense_volume/two-channels-hardcoded.xml b/tests/unit/robolint/xml/hardcoded_dispense_volume/two-channels-hardcoded.xml
new file mode 100644
index 0000000..5d07e35
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded_dispense_volume/two-channels-hardcoded.xml
@@ -0,0 +1,477 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded_mix_volume/one-channel-variable-other-hardcoded.xml b/tests/unit/robolint/xml/hardcoded_mix_volume/one-channel-variable-other-hardcoded.xml
new file mode 100644
index 0000000..da01f46
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded_mix_volume/one-channel-variable-other-hardcoded.xml
@@ -0,0 +1,485 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded_mix_volume/one-channel-variable.xml b/tests/unit/robolint/xml/hardcoded_mix_volume/one-channel-variable.xml
new file mode 100644
index 0000000..ca22066
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded_mix_volume/one-channel-variable.xml
@@ -0,0 +1,465 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/hardcoded_mix_volume/two-channels-hardcoded.xml b/tests/unit/robolint/xml/hardcoded_mix_volume/two-channels-hardcoded.xml
new file mode 100644
index 0000000..61d4087
--- /dev/null
+++ b/tests/unit/robolint/xml/hardcoded_mix_volume/two-channels-hardcoded.xml
@@ -0,0 +1,481 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/invalid_loop_start_index/loop-bound-to-variable-but-hardcoded-to-3.xml b/tests/unit/robolint/xml/invalid_loop_start_index/loop-bound-to-variable-but-hardcoded-to-3.xml
new file mode 100644
index 0000000..d6da221
--- /dev/null
+++ b/tests/unit/robolint/xml/invalid_loop_start_index/loop-bound-to-variable-but-hardcoded-to-3.xml
@@ -0,0 +1,367 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/invalid_loop_start_index/loop-starting-at-one-at-second-step.xml b/tests/unit/robolint/xml/invalid_loop_start_index/loop-starting-at-one-at-second-step.xml
new file mode 100644
index 0000000..76795dd
--- /dev/null
+++ b/tests/unit/robolint/xml/invalid_loop_start_index/loop-starting-at-one-at-second-step.xml
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/invalid_loop_start_index/loop-starting-at-one.xml b/tests/unit/robolint/xml/invalid_loop_start_index/loop-starting-at-one.xml
new file mode 100644
index 0000000..2bdc696
--- /dev/null
+++ b/tests/unit/robolint/xml/invalid_loop_start_index/loop-starting-at-one.xml
@@ -0,0 +1,371 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/invalid_loop_start_index/loop-starting-at-zero.xml b/tests/unit/robolint/xml/invalid_loop_start_index/loop-starting-at-zero.xml
new file mode 100644
index 0000000..db46217
--- /dev/null
+++ b/tests/unit/robolint/xml/invalid_loop_start_index/loop-starting-at-zero.xml
@@ -0,0 +1,371 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/dispense-step-with-labware-name.xml b/tests/unit/robolint/xml/labware-names/dispense-step-with-labware-name.xml
new file mode 100644
index 0000000..c97d45a
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/dispense-step-with-labware-name.xml
@@ -0,0 +1,461 @@
+ο»Ώ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/mix-step-with-labware-name.xml b/tests/unit/robolint/xml/labware-names/mix-step-with-labware-name.xml
new file mode 100644
index 0000000..469d88a
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/mix-step-with-labware-name.xml
@@ -0,0 +1,465 @@
+ο»Ώ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/move-to-plate-gripper-step-with-labware-name.xml b/tests/unit/robolint/xml/labware-names/move-to-plate-gripper-step-with-labware-name.xml
new file mode 100644
index 0000000..685b5f8
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/move-to-plate-gripper-step-with-labware-name.xml
@@ -0,0 +1,357 @@
+ο»Ώ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/move-to-plate-step-with-labware-name.xml b/tests/unit/robolint/xml/labware-names/move-to-plate-step-with-labware-name.xml
new file mode 100644
index 0000000..2319453
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/move-to-plate-step-with-labware-name.xml
@@ -0,0 +1,357 @@
+ο»Ώ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/multi-dispense-step-with-labware-name.xml b/tests/unit/robolint/xml/labware-names/multi-dispense-step-with-labware-name.xml
new file mode 100644
index 0000000..bdcefdc
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/multi-dispense-step-with-labware-name.xml
@@ -0,0 +1,449 @@
+ο»Ώ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/no-labware-definitions.xml b/tests/unit/robolint/xml/labware-names/no-labware-definitions.xml
new file mode 100644
index 0000000..ba82531
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/no-labware-definitions.xml
@@ -0,0 +1,399 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/single-aspirate-step-no-plate-specified.xml b/tests/unit/robolint/xml/labware-names/single-aspirate-step-no-plate-specified.xml
new file mode 100644
index 0000000..31d932d
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/single-aspirate-step-no-plate-specified.xml
@@ -0,0 +1,447 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/single-aspirate-step-with-invalid-labware-name.xml b/tests/unit/robolint/xml/labware-names/single-aspirate-step-with-invalid-labware-name.xml
new file mode 100644
index 0000000..fc0145f
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/single-aspirate-step-with-invalid-labware-name.xml
@@ -0,0 +1,457 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/single-aspirate-step-with-valid-labware-but-same-platename-as-invalid-file.xml b/tests/unit/robolint/xml/labware-names/single-aspirate-step-with-valid-labware-but-same-platename-as-invalid-file.xml
new file mode 100644
index 0000000..d40f08b
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/single-aspirate-step-with-valid-labware-but-same-platename-as-invalid-file.xml
@@ -0,0 +1,457 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/single-aspirate-step-with-valid-labware-name.xml b/tests/unit/robolint/xml/labware-names/single-aspirate-step-with-valid-labware-name.xml
new file mode 100644
index 0000000..c9e76ad
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/single-aspirate-step-with-valid-labware-name.xml
@@ -0,0 +1,457 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/submethod-inheriting-worktable-with-move-to-plate-and-load-tips.xml b/tests/unit/robolint/xml/labware-names/submethod-inheriting-worktable-with-move-to-plate-and-load-tips.xml
new file mode 100644
index 0000000..5d65c43
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/submethod-inheriting-worktable-with-move-to-plate-and-load-tips.xml
@@ -0,0 +1,399 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/labware-names/two-aspirate-steps-with-labware-names.xml b/tests/unit/robolint/xml/labware-names/two-aspirate-steps-with-labware-names.xml
new file mode 100644
index 0000000..ce0aa2f
--- /dev/null
+++ b/tests/unit/robolint/xml/labware-names/two-aspirate-steps-with-labware-names.xml
@@ -0,0 +1,601 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/robolinter/invalid-rule-disabled-in-comment.xml b/tests/unit/robolint/xml/robolinter/invalid-rule-disabled-in-comment.xml
new file mode 100644
index 0000000..c7b9b45
--- /dev/null
+++ b/tests/unit/robolint/xml/robolinter/invalid-rule-disabled-in-comment.xml
@@ -0,0 +1,485 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/robolinter/two-rules-disabled-in-one-comment.xml b/tests/unit/robolint/xml/robolinter/two-rules-disabled-in-one-comment.xml
new file mode 100644
index 0000000..f7bd9fd
--- /dev/null
+++ b/tests/unit/robolint/xml/robolinter/two-rules-disabled-in-one-comment.xml
@@ -0,0 +1,505 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/tip_height/tip-height-of-15.xml b/tests/unit/robolint/xml/tip_height/tip-height-of-15.xml
new file mode 100644
index 0000000..7000ec7
--- /dev/null
+++ b/tests/unit/robolint/xml/tip_height/tip-height-of-15.xml
@@ -0,0 +1,357 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/tip_motion_profile/eject-tips-with-invalid-fast-speed-profile.xml b/tests/unit/robolint/xml/tip_motion_profile/eject-tips-with-invalid-fast-speed-profile.xml
new file mode 100644
index 0000000..7000ec7
--- /dev/null
+++ b/tests/unit/robolint/xml/tip_motion_profile/eject-tips-with-invalid-fast-speed-profile.xml
@@ -0,0 +1,357 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/tip_motion_profile/load-tips-with-invalid-mid-speed-profile.xml b/tests/unit/robolint/xml/tip_motion_profile/load-tips-with-invalid-mid-speed-profile.xml
new file mode 100644
index 0000000..341b47a
--- /dev/null
+++ b/tests/unit/robolint/xml/tip_motion_profile/load-tips-with-invalid-mid-speed-profile.xml
@@ -0,0 +1,367 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/tip_motion_profile/load-tips-with-valid-tip-load-profile.xml b/tests/unit/robolint/xml/tip_motion_profile/load-tips-with-valid-tip-load-profile.xml
new file mode 100644
index 0000000..481e07f
--- /dev/null
+++ b/tests/unit/robolint/xml/tip_motion_profile/load-tips-with-valid-tip-load-profile.xml
@@ -0,0 +1,367 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/variable-names/aspirate-step-variable-for-plate-and-volume.xml b/tests/unit/robolint/xml/variable-names/aspirate-step-variable-for-plate-and-volume.xml
new file mode 100644
index 0000000..242a5b9
--- /dev/null
+++ b/tests/unit/robolint/xml/variable-names/aspirate-step-variable-for-plate-and-volume.xml
@@ -0,0 +1,2539 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/variable-names/loop-with-variable-name-variable-and-value-variable.xml b/tests/unit/robolint/xml/variable-names/loop-with-variable-name-variable-and-value-variable.xml
new file mode 100644
index 0000000..ba82531
--- /dev/null
+++ b/tests/unit/robolint/xml/variable-names/loop-with-variable-name-variable-and-value-variable.xml
@@ -0,0 +1,399 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/variable-names/variable-name-contains-abbreviation.xml b/tests/unit/robolint/xml/variable-names/variable-name-contains-abbreviation.xml
new file mode 100644
index 0000000..cf2c803
--- /dev/null
+++ b/tests/unit/robolint/xml/variable-names/variable-name-contains-abbreviation.xml
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/variable-names/variable-name-contains-single-digit-suffix.xml b/tests/unit/robolint/xml/variable-names/variable-name-contains-single-digit-suffix.xml
new file mode 100644
index 0000000..d1c797d
--- /dev/null
+++ b/tests/unit/robolint/xml/variable-names/variable-name-contains-single-digit-suffix.xml
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/variable-names/variable-name-in-camel-case.xml b/tests/unit/robolint/xml/variable-names/variable-name-in-camel-case.xml
new file mode 100644
index 0000000..afd3010
--- /dev/null
+++ b/tests/unit/robolint/xml/variable-names/variable-name-in-camel-case.xml
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/variable-names/variable-name-in-kebab-case.xml b/tests/unit/robolint/xml/variable-names/variable-name-in-kebab-case.xml
new file mode 100644
index 0000000..ef44253
--- /dev/null
+++ b/tests/unit/robolint/xml/variable-names/variable-name-in-kebab-case.xml
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/variable-names/variable-name-in-pascal-case.xml b/tests/unit/robolint/xml/variable-names/variable-name-in-pascal-case.xml
new file mode 100644
index 0000000..76795dd
--- /dev/null
+++ b/tests/unit/robolint/xml/variable-names/variable-name-in-pascal-case.xml
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/robolint/xml/variable-names/variable-name-in-snake-case.xml b/tests/unit/robolint/xml/variable-names/variable-name-in-snake-case.xml
new file mode 100644
index 0000000..5aa0244
--- /dev/null
+++ b/tests/unit/robolint/xml/variable-names/variable-name-in-snake-case.xml
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/strip_workspace_variables/__init__.py b/tests/unit/strip_workspace_variables/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/strip_workspace_variables/test_main.py b/tests/unit/strip_workspace_variables/test_main.py
new file mode 100644
index 0000000..ebd2c73
--- /dev/null
+++ b/tests/unit/strip_workspace_variables/test_main.py
@@ -0,0 +1,49 @@
+from unittest.mock import call
+
+from pytest_mock import MockerFixture
+from robolint.hooks import strip_workspace_config_values
+from robolint.hooks.strip_workspace_config_values import main
+
+from ..utils import mock_argv_filelist
+
+
+def test_main_calls_strip_values_from_file_with_passed_filenames(mocker: MockerFixture) -> None:
+ mocked_strip_values_from_file = mocker.patch.object(
+ strip_workspace_config_values, "strip_values_from_file", return_value=[]
+ )
+ expected_file_list = ["dummy1.config", "dummy2.config"]
+ mock_argv_filelist(mocker, expected_file_list)
+ main()
+ assert mocked_strip_values_from_file.call_count == 2
+ mocked_strip_values_from_file.assert_has_calls(
+ [call(expected_file_list[0], None), call(expected_file_list[1], None)], any_order=True
+ )
+
+
+def test_When_strip_values_identifies_problem__Then_main_returns_1(mocker: MockerFixture) -> None:
+ mocker.patch.object(strip_workspace_config_values, "strip_values_from_file", return_value=["var1"])
+ expected_file_list = ["dummy1.config"]
+ mock_argv_filelist(mocker, expected_file_list)
+ actual = main()
+ assert actual == 1
+
+
+def test_When_strip_values_identifies_problem__Then_main_logs_the_variable_and_file_name(mocker: MockerFixture) -> None:
+ expected_variable_name = "var2"
+ mocker.patch.object(strip_workspace_config_values, "strip_values_from_file", return_value=[expected_variable_name])
+ mocked_logger = mocker.patch.object(strip_workspace_config_values.logger, "info")
+ expected_file_list = ["dummy1.config"]
+ mock_argv_filelist(mocker, expected_file_list)
+ main()
+ assert mocked_logger.call_count == 1
+ actual_call_str = str(mocked_logger.call_args_list[0])
+ assert expected_file_list[0] in actual_call_str
+ assert expected_variable_name in actual_call_str
+
+
+def test_When_strip_values_identifies_no_problems__Then_main_returns_0(mocker: MockerFixture) -> None:
+ mocker.patch.object(strip_workspace_config_values, "strip_values_from_file", return_value=[])
+ expected_file_list = ["dummy1.config"]
+ mock_argv_filelist(mocker, expected_file_list)
+ actual = main()
+ assert actual == 0
diff --git a/tests/unit/strip_workspace_variables/test_strip_values_from_file.py b/tests/unit/strip_workspace_variables/test_strip_values_from_file.py
new file mode 100644
index 0000000..70e3d0b
--- /dev/null
+++ b/tests/unit/strip_workspace_variables/test_strip_values_from_file.py
@@ -0,0 +1,53 @@
+import hashlib
+import os
+from pathlib import Path
+import shutil
+from tempfile import TemporaryDirectory
+
+from robolint.hooks.strip_workspace_config_values import strip_values_from_file
+from stdlib_utils import get_current_file_abs_directory
+
+PATH_TO_CURRENT_FILE = get_current_file_abs_directory()
+PATH_TO_CONFIGS = os.path.join(PATH_TO_CURRENT_FILE, "workspace_variables_configs")
+
+
+def test_When_file_is_clean__Then_returns_falsey() -> None:
+ actual = strip_values_from_file(os.path.join(PATH_TO_CONFIGS, "clean.xml"))
+ assert not actual
+
+
+def test_When_first_variable_has_value__Then_new_file_is_clean() -> None:
+ original_file_path = os.path.join(PATH_TO_CONFIGS, "first_value_present.xml")
+ with TemporaryDirectory() as temp_dir:
+ new_file_path = os.path.join(temp_dir, "blah.xml")
+ shutil.copy(original_file_path, new_file_path)
+ initial_result = strip_values_from_file(new_file_path)
+ assert len(initial_result) == 1
+
+ actual = strip_values_from_file(new_file_path)
+ assert not actual
+
+
+def test_When_first_variable_has_value_spanning_lines__Then_new_file_is_clean() -> None:
+ original_file_path = os.path.join(PATH_TO_CONFIGS, "first_value_spans_lines.xml")
+ with TemporaryDirectory() as temp_dir:
+ new_file_path = os.path.join(temp_dir, "blah.xml")
+ shutil.copy(original_file_path, new_file_path)
+ initial_result = strip_values_from_file(new_file_path)
+ assert len(initial_result) == 1
+
+ actual = strip_values_from_file(new_file_path)
+ assert not actual
+
+
+def test_When_strip_values_encounters_a_variable_to_ignore__Then_the_file_stays_the_same() -> None:
+ original_file_path = os.path.join(PATH_TO_CONFIGS, "preserve.xml")
+ with TemporaryDirectory() as temp_dir:
+ new_file_path = os.path.join(temp_dir, "blah.xml")
+ shutil.copy(original_file_path, new_file_path)
+ result = strip_values_from_file(new_file_path, {"MethodManagerVersion"})
+ assert len(result) == 0
+ assert (
+ hashlib.md5(Path(original_file_path).read_text(encoding="UTF-8").encode()).hexdigest()
+ == hashlib.md5(Path(new_file_path).read_text(encoding="UTF-8").encode()).hexdigest()
+ )
diff --git a/tests/unit/strip_workspace_variables/workspace_variables_configs/clean.xml b/tests/unit/strip_workspace_variables/workspace_variables_configs/clean.xml
new file mode 100644
index 0000000..fa8e619
--- /dev/null
+++ b/tests/unit/strip_workspace_variables/workspace_variables_configs/clean.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ LoopCounter
+
+
+
+ ReadLoc
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/strip_workspace_variables/workspace_variables_configs/first_value_present.xml b/tests/unit/strip_workspace_variables/workspace_variables_configs/first_value_present.xml
new file mode 100644
index 0000000..5b0ee64
--- /dev/null
+++ b/tests/unit/strip_workspace_variables/workspace_variables_configs/first_value_present.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ LoopCounter
+ 5
+
+
+ ReadLoc
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/strip_workspace_variables/workspace_variables_configs/first_value_spans_lines.xml b/tests/unit/strip_workspace_variables/workspace_variables_configs/first_value_spans_lines.xml
new file mode 100644
index 0000000..5cc8a32
--- /dev/null
+++ b/tests/unit/strip_workspace_variables/workspace_variables_configs/first_value_spans_lines.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ LoopCounter
+
+Method pairSeqPooling run at 16-52 10Dec2015 by Peter Nelson on system PNELSON-LAP
+Workspace = Lynx730i_Prod
+Workspace Commit ID = 9bdd5dcf04bee46a7888cecbc1c6fc0f268e902a
+Config Commit ID = 994a9ce3a978c975f2f13436d861a009f4fdc310
+Common Method Commit ID = 1cd7c09a3e8608138264f726f54d50fc92002736
+Common Script Commit ID = 574b947cc78fd54b947e2ff352db4a5de1f3f8b6
+Input Plate Barcode = Plate1
+Pool Barcode = Pool 1
+Tip Box 1 = TipBox1
+
+
+
+ ReadLoc
+
+
+
+
\ No newline at end of file
diff --git a/tests/unit/strip_workspace_variables/workspace_variables_configs/preserve.xml b/tests/unit/strip_workspace_variables/workspace_variables_configs/preserve.xml
new file mode 100644
index 0000000..fb8f360
--- /dev/null
+++ b/tests/unit/strip_workspace_variables/workspace_variables_configs/preserve.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ LoopCounter
+
+
+
+ MethodManagerVersion
+ 1234
+
+
+
\ No newline at end of file
diff --git a/tests/unit/utils.py b/tests/unit/utils.py
new file mode 100644
index 0000000..fc45f12
--- /dev/null
+++ b/tests/unit/utils.py
@@ -0,0 +1,11 @@
+import sys
+from typing import Iterable
+
+from pytest_mock import MockerFixture
+
+
+def mock_argv_filelist(mocker: MockerFixture, file_list: Iterable[str]) -> None:
+
+ argv = ["dummy.py"]
+ argv.extend(file_list)
+ mocker.patch.object(sys, "argv", argv)