@owenlamont/ryl 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CODEOWNERS +1 -0
- package/.github/dependabot.yml +13 -0
- package/.github/workflows/ci.yml +107 -0
- package/.github/workflows/release.yml +613 -0
- package/.github/workflows/update_dependencies.yml +61 -0
- package/.github/workflows/update_linters.yml +56 -0
- package/.pre-commit-config.yaml +87 -0
- package/.yamllint +4 -0
- package/AGENTS.md +200 -0
- package/Cargo.lock +908 -0
- package/Cargo.toml +32 -0
- package/LICENSE +21 -0
- package/README.md +230 -0
- package/bin/ryl.js +1 -0
- package/clippy.toml +1 -0
- package/docs/config-presets.md +100 -0
- package/img/benchmark-5x5-5runs.svg +2176 -0
- package/package.json +28 -0
- package/pyproject.toml +42 -0
- package/ruff.toml +107 -0
- package/rumdl.toml +20 -0
- package/rust-toolchain.toml +3 -0
- package/rustfmt.toml +3 -0
- package/scripts/benchmark_perf_vs_yamllint.py +400 -0
- package/scripts/coverage-missing.ps1 +80 -0
- package/scripts/coverage-missing.sh +60 -0
- package/src/bin/discover_config_bin.rs +24 -0
- package/src/cli_support.rs +33 -0
- package/src/conf/mod.rs +85 -0
- package/src/config.rs +2099 -0
- package/src/decoder.rs +326 -0
- package/src/discover.rs +31 -0
- package/src/lib.rs +19 -0
- package/src/lint.rs +558 -0
- package/src/main.rs +535 -0
- package/src/migrate.rs +233 -0
- package/src/rules/anchors.rs +517 -0
- package/src/rules/braces.rs +77 -0
- package/src/rules/brackets.rs +77 -0
- package/src/rules/colons.rs +475 -0
- package/src/rules/commas.rs +372 -0
- package/src/rules/comments.rs +299 -0
- package/src/rules/comments_indentation.rs +243 -0
- package/src/rules/document_end.rs +175 -0
- package/src/rules/document_start.rs +84 -0
- package/src/rules/empty_lines.rs +152 -0
- package/src/rules/empty_values.rs +255 -0
- package/src/rules/float_values.rs +259 -0
- package/src/rules/flow_collection.rs +562 -0
- package/src/rules/hyphens.rs +104 -0
- package/src/rules/indentation.rs +803 -0
- package/src/rules/key_duplicates.rs +218 -0
- package/src/rules/key_ordering.rs +303 -0
- package/src/rules/line_length.rs +326 -0
- package/src/rules/mod.rs +25 -0
- package/src/rules/new_line_at_end_of_file.rs +23 -0
- package/src/rules/new_lines.rs +95 -0
- package/src/rules/octal_values.rs +121 -0
- package/src/rules/quoted_strings.rs +577 -0
- package/src/rules/span_utils.rs +37 -0
- package/src/rules/trailing_spaces.rs +65 -0
- package/src/rules/truthy.rs +420 -0
- package/tests/brackets_carriage_return.rs +114 -0
- package/tests/build_global_cfg_error.rs +23 -0
- package/tests/cli_anchors_rule.rs +143 -0
- package/tests/cli_braces_rule.rs +104 -0
- package/tests/cli_brackets_rule.rs +104 -0
- package/tests/cli_colons_rule.rs +65 -0
- package/tests/cli_commas_rule.rs +104 -0
- package/tests/cli_comments_indentation_rule.rs +61 -0
- package/tests/cli_comments_rule.rs +67 -0
- package/tests/cli_config_data_error.rs +30 -0
- package/tests/cli_config_flags.rs +66 -0
- package/tests/cli_config_migrate.rs +229 -0
- package/tests/cli_document_end_rule.rs +92 -0
- package/tests/cli_document_start_rule.rs +92 -0
- package/tests/cli_empty_lines_rule.rs +87 -0
- package/tests/cli_empty_values_rule.rs +68 -0
- package/tests/cli_env_config.rs +34 -0
- package/tests/cli_exit_and_errors.rs +41 -0
- package/tests/cli_file_encoding.rs +203 -0
- package/tests/cli_float_values_rule.rs +64 -0
- package/tests/cli_format_options.rs +316 -0
- package/tests/cli_global_cfg_relaxed.rs +20 -0
- package/tests/cli_hyphens_rule.rs +104 -0
- package/tests/cli_indentation_rule.rs +65 -0
- package/tests/cli_invalid_project_config.rs +39 -0
- package/tests/cli_key_duplicates_rule.rs +104 -0
- package/tests/cli_key_ordering_rule.rs +59 -0
- package/tests/cli_line_length_rule.rs +85 -0
- package/tests/cli_list_files.rs +29 -0
- package/tests/cli_new_line_rule.rs +141 -0
- package/tests/cli_new_lines_rule.rs +119 -0
- package/tests/cli_octal_values_rule.rs +60 -0
- package/tests/cli_quoted_strings_rule.rs +47 -0
- package/tests/cli_toml_config.rs +119 -0
- package/tests/cli_trailing_spaces_rule.rs +77 -0
- package/tests/cli_truthy_rule.rs +83 -0
- package/tests/cli_yaml_files_negation.rs +45 -0
- package/tests/colons_rule.rs +303 -0
- package/tests/common/compat.rs +114 -0
- package/tests/common/fake_env.rs +93 -0
- package/tests/common/mod.rs +1 -0
- package/tests/conf_builtin.rs +9 -0
- package/tests/config_anchors.rs +84 -0
- package/tests/config_braces.rs +121 -0
- package/tests/config_brackets.rs +127 -0
- package/tests/config_commas.rs +79 -0
- package/tests/config_comments.rs +65 -0
- package/tests/config_comments_indentation.rs +20 -0
- package/tests/config_deep_merge_nonstring_key.rs +24 -0
- package/tests/config_document_end.rs +54 -0
- package/tests/config_document_start.rs +55 -0
- package/tests/config_empty_lines.rs +48 -0
- package/tests/config_empty_values.rs +35 -0
- package/tests/config_env_errors.rs +23 -0
- package/tests/config_env_invalid_inline.rs +15 -0
- package/tests/config_env_missing.rs +63 -0
- package/tests/config_env_shim.rs +301 -0
- package/tests/config_explicit_file_parse_error.rs +55 -0
- package/tests/config_extended_features.rs +225 -0
- package/tests/config_extends_inline.rs +185 -0
- package/tests/config_extends_sequence.rs +18 -0
- package/tests/config_find_project_home_boundary.rs +54 -0
- package/tests/config_find_project_two_files_in_cwd.rs +47 -0
- package/tests/config_float_values.rs +34 -0
- package/tests/config_from_yaml_paths.rs +32 -0
- package/tests/config_hyphens.rs +51 -0
- package/tests/config_ignore_errors.rs +243 -0
- package/tests/config_ignore_overrides.rs +83 -0
- package/tests/config_indentation.rs +65 -0
- package/tests/config_invalid_globs.rs +16 -0
- package/tests/config_invalid_types.rs +19 -0
- package/tests/config_key_duplicates.rs +34 -0
- package/tests/config_key_ordering.rs +70 -0
- package/tests/config_line_length.rs +65 -0
- package/tests/config_locale.rs +111 -0
- package/tests/config_merge.rs +26 -0
- package/tests/config_new_lines.rs +89 -0
- package/tests/config_octal_values.rs +33 -0
- package/tests/config_quoted_strings.rs +195 -0
- package/tests/config_rule_level.rs +147 -0
- package/tests/config_rules_non_string_keys.rs +23 -0
- package/tests/config_scalar_overrides.rs +27 -0
- package/tests/config_to_toml.rs +110 -0
- package/tests/config_toml_coverage.rs +80 -0
- package/tests/config_toml_discovery.rs +304 -0
- package/tests/config_trailing_spaces.rs +152 -0
- package/tests/config_truthy.rs +77 -0
- package/tests/config_yaml_files.rs +62 -0
- package/tests/config_yaml_files_all_non_string.rs +15 -0
- package/tests/config_yaml_files_empty.rs +30 -0
- package/tests/coverage_commas.rs +46 -0
- package/tests/decoder_decode.rs +338 -0
- package/tests/discover_config_bin_all.rs +66 -0
- package/tests/discover_config_bin_env_invalid_yaml.rs +26 -0
- package/tests/discover_config_bin_project_config_parse_error.rs +24 -0
- package/tests/discover_config_bin_user_global_error.rs +26 -0
- package/tests/discover_module.rs +30 -0
- package/tests/discover_per_file_dir.rs +10 -0
- package/tests/discover_per_file_project_config_error.rs +21 -0
- package/tests/float_values.rs +43 -0
- package/tests/lint_multi_errors.rs +32 -0
- package/tests/main_yaml_ok_filtering.rs +30 -0
- package/tests/migrate_module.rs +259 -0
- package/tests/resolve_ctx_empty_parent.rs +16 -0
- package/tests/rule_anchors.rs +442 -0
- package/tests/rule_braces.rs +258 -0
- package/tests/rule_brackets.rs +217 -0
- package/tests/rule_commas.rs +205 -0
- package/tests/rule_comments.rs +197 -0
- package/tests/rule_comments_indentation.rs +127 -0
- package/tests/rule_document_end.rs +118 -0
- package/tests/rule_document_start.rs +60 -0
- package/tests/rule_empty_lines.rs +96 -0
- package/tests/rule_empty_values.rs +102 -0
- package/tests/rule_float_values.rs +109 -0
- package/tests/rule_hyphens.rs +65 -0
- package/tests/rule_indentation.rs +455 -0
- package/tests/rule_key_duplicates.rs +76 -0
- package/tests/rule_key_ordering.rs +207 -0
- package/tests/rule_line_length.rs +200 -0
- package/tests/rule_new_lines.rs +51 -0
- package/tests/rule_octal_values.rs +53 -0
- package/tests/rule_quoted_strings.rs +290 -0
- package/tests/rule_trailing_spaces.rs +41 -0
- package/tests/rule_truthy.rs +236 -0
- package/tests/user_global_invalid_yaml.rs +32 -0
- package/tests/yamllint_compat_anchors.rs +280 -0
- package/tests/yamllint_compat_braces.rs +411 -0
- package/tests/yamllint_compat_brackets.rs +364 -0
- package/tests/yamllint_compat_colons.rs +298 -0
- package/tests/yamllint_compat_colors.rs +80 -0
- package/tests/yamllint_compat_commas.rs +375 -0
- package/tests/yamllint_compat_comments.rs +167 -0
- package/tests/yamllint_compat_comments_indentation.rs +281 -0
- package/tests/yamllint_compat_config.rs +170 -0
- package/tests/yamllint_compat_document_end.rs +243 -0
- package/tests/yamllint_compat_document_start.rs +136 -0
- package/tests/yamllint_compat_empty_lines.rs +117 -0
- package/tests/yamllint_compat_empty_values.rs +179 -0
- package/tests/yamllint_compat_float_values.rs +216 -0
- package/tests/yamllint_compat_hyphens.rs +223 -0
- package/tests/yamllint_compat_indentation.rs +398 -0
- package/tests/yamllint_compat_key_duplicates.rs +139 -0
- package/tests/yamllint_compat_key_ordering.rs +170 -0
- package/tests/yamllint_compat_line_length.rs +375 -0
- package/tests/yamllint_compat_list.rs +127 -0
- package/tests/yamllint_compat_new_line.rs +133 -0
- package/tests/yamllint_compat_newline_types.rs +185 -0
- package/tests/yamllint_compat_octal_values.rs +172 -0
- package/tests/yamllint_compat_quoted_strings.rs +154 -0
- package/tests/yamllint_compat_syntax.rs +200 -0
- package/tests/yamllint_compat_trailing_spaces.rs +162 -0
- package/tests/yamllint_compat_truthy.rs +130 -0
- package/tests/yamllint_compat_yaml_files.rs +81 -0
- package/typos.toml +2 -0
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owenlamont/ryl",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Fast YAML linter inspired by yamllint",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ryl": "bin/ryl.js"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/owenlamont/ryl.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"yaml",
|
|
15
|
+
"lint",
|
|
16
|
+
"cli",
|
|
17
|
+
"rust"
|
|
18
|
+
],
|
|
19
|
+
"author": "Owen Lamont",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/owenlamont/ryl/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/owenlamont/ryl#readme",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["maturin>=1.4,<2.0"]
|
|
3
|
+
build-backend = "maturin"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ryl"
|
|
7
|
+
version = "0.4.1"
|
|
8
|
+
description = "Fast YAML linter inspired by yamllint"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: MacOS :: MacOS X",
|
|
17
|
+
"Operating System :: Microsoft :: Windows",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Programming Language :: Python",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Programming Language :: Rust",
|
|
28
|
+
"Topic :: File Formats",
|
|
29
|
+
"Topic :: Text Processing :: Markup",
|
|
30
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
31
|
+
"Topic :: Utilities",
|
|
32
|
+
]
|
|
33
|
+
keywords = ["yaml", "linter", "lint", "cli", "rust"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/owenlamont/ryl"
|
|
37
|
+
Repository = "https://github.com/owenlamont/ryl"
|
|
38
|
+
Issues = "https://github.com/owenlamont/ryl/issues"
|
|
39
|
+
|
|
40
|
+
[tool.maturin]
|
|
41
|
+
bindings = "bin"
|
|
42
|
+
sdist = true
|
package/ruff.toml
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
line-length = 88
|
|
2
|
+
indent-width = 4
|
|
3
|
+
|
|
4
|
+
[format]
|
|
5
|
+
quote-style = "double"
|
|
6
|
+
indent-style = "space"
|
|
7
|
+
skip-magic-trailing-comma = true
|
|
8
|
+
line-ending = "lf"
|
|
9
|
+
|
|
10
|
+
[lint]
|
|
11
|
+
# See https://docs.astral.sh/ruff/rules/
|
|
12
|
+
select = [
|
|
13
|
+
"A",
|
|
14
|
+
"ASYNC",
|
|
15
|
+
"B",
|
|
16
|
+
"C4",
|
|
17
|
+
"D",
|
|
18
|
+
"DOC",
|
|
19
|
+
"E",
|
|
20
|
+
"ERA",
|
|
21
|
+
"F",
|
|
22
|
+
"FURB",
|
|
23
|
+
"I",
|
|
24
|
+
"ISC",
|
|
25
|
+
"N",
|
|
26
|
+
"NPY",
|
|
27
|
+
"PD",
|
|
28
|
+
"PERF",
|
|
29
|
+
"PT",
|
|
30
|
+
"PTH",
|
|
31
|
+
"Q",
|
|
32
|
+
"S",
|
|
33
|
+
"RET",
|
|
34
|
+
"RUF",
|
|
35
|
+
"SIM",
|
|
36
|
+
"TID",
|
|
37
|
+
"UP",
|
|
38
|
+
]
|
|
39
|
+
ignore = [
|
|
40
|
+
"D100", # Missing docstring in public module
|
|
41
|
+
"D101", # Missing docstring in public class
|
|
42
|
+
"D104", # Missing docstring in public package
|
|
43
|
+
"D105", # Missing docstring in magic method
|
|
44
|
+
"D106", # Missing docstring in public nested class
|
|
45
|
+
"D107", # Missing docstring in __init__
|
|
46
|
+
"D202", # No blank lines allowed after function docstring
|
|
47
|
+
"D213", # Multi-line docstring summary should start at the second line
|
|
48
|
+
"D214", # Section is over-indented
|
|
49
|
+
"D215", # Section underline is over-indented
|
|
50
|
+
"D400", # First line should end with a period
|
|
51
|
+
"D401", # First line of docstring should be in imperative mood
|
|
52
|
+
"D415", # First line should end with a period, question mark, or exclamation
|
|
53
|
+
"D416", # Section name should end with a colon
|
|
54
|
+
"D417", # Missing argument descriptions in the docstring
|
|
55
|
+
"D418", # Function/ Method decorated with @overload shouldn't contain a docstring
|
|
56
|
+
"E203", # Whitespace before ':' (fights ruff format)
|
|
57
|
+
"ISC001", # Implicitly concatenated string literals on one line
|
|
58
|
+
]
|
|
59
|
+
extend-select = ["RUF027"]
|
|
60
|
+
preview = true
|
|
61
|
+
|
|
62
|
+
[lint.per-file-ignores]
|
|
63
|
+
"tests/**" = ["D102", "D103", "S101"]
|
|
64
|
+
"tests/uv_secure/package_info/test_dependency_file_parser.py" = ["E501"]
|
|
65
|
+
"scripts/benchmark_perf_vs_yamllint.py" = [
|
|
66
|
+
"B008",
|
|
67
|
+
"B904",
|
|
68
|
+
"B905",
|
|
69
|
+
"D103",
|
|
70
|
+
"E501",
|
|
71
|
+
"S404",
|
|
72
|
+
"S603",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# Allow fix for all enabled rules (when `--fix`) is provided.
|
|
76
|
+
fixable = ["ALL"]
|
|
77
|
+
unfixable = [
|
|
78
|
+
"B905", # Enforce strict argument on zip - but don't autofix as strict=False
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[lint.flake8-pytest-style]
|
|
82
|
+
mark-parentheses = false
|
|
83
|
+
|
|
84
|
+
[lint.flake8-tidy-imports]
|
|
85
|
+
# Disallow all relative imports.
|
|
86
|
+
ban-relative-imports = "all"
|
|
87
|
+
|
|
88
|
+
[lint.flake8-tidy-imports.banned-api]
|
|
89
|
+
"typing.cast".msg = "Type casts are not allowed"
|
|
90
|
+
|
|
91
|
+
[lint.isort]
|
|
92
|
+
case-sensitive = false
|
|
93
|
+
combine-as-imports = true
|
|
94
|
+
force-sort-within-sections = true
|
|
95
|
+
lines-after-imports = 2
|
|
96
|
+
order-by-type = false
|
|
97
|
+
section-order = [
|
|
98
|
+
"future",
|
|
99
|
+
"standard-library",
|
|
100
|
+
"third-party",
|
|
101
|
+
"first-party",
|
|
102
|
+
"local-folder",
|
|
103
|
+
]
|
|
104
|
+
split-on-trailing-comma = false
|
|
105
|
+
|
|
106
|
+
[lint.pydocstyle]
|
|
107
|
+
convention = "google"
|
package/rumdl.toml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
line-length = 88
|
|
2
|
+
disable = []
|
|
3
|
+
exclude = []
|
|
4
|
+
respect-gitignore = true
|
|
5
|
+
|
|
6
|
+
[MD004]
|
|
7
|
+
style = "dash"
|
|
8
|
+
|
|
9
|
+
[MD013]
|
|
10
|
+
line_length = 88
|
|
11
|
+
code_blocks = false
|
|
12
|
+
tables = true
|
|
13
|
+
headings = true
|
|
14
|
+
strict = false
|
|
15
|
+
|
|
16
|
+
[MD029]
|
|
17
|
+
style = "ordered"
|
|
18
|
+
|
|
19
|
+
[MD035]
|
|
20
|
+
style = "---"
|
package/rustfmt.toml
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.14"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "matplotlib>=3.9,<4",
|
|
6
|
+
# "orjson>=3.11,<4",
|
|
7
|
+
# "polars>=1.30,<2",
|
|
8
|
+
# "ryl",
|
|
9
|
+
# "tqdm>=4.67,<5",
|
|
10
|
+
# "typer>=0.16,<1",
|
|
11
|
+
# "yamllint",
|
|
12
|
+
# ]
|
|
13
|
+
# ///
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Iterable
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
import os
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
import shutil
|
|
23
|
+
import string
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
import matplotlib.pyplot as plt
|
|
28
|
+
import orjson
|
|
29
|
+
import polars as pl
|
|
30
|
+
from tqdm import tqdm
|
|
31
|
+
import typer
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Case:
|
|
36
|
+
file_count: int
|
|
37
|
+
file_size_kib: int
|
|
38
|
+
dataset_dir: Path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
app = typer.Typer(add_completion=False)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_int_list(raw: str) -> list[int]:
|
|
45
|
+
values = [int(part.strip()) for part in raw.split(",") if part.strip()]
|
|
46
|
+
if not values:
|
|
47
|
+
raise ValueError("expected at least one integer")
|
|
48
|
+
if any(value <= 0 for value in values):
|
|
49
|
+
raise ValueError("all values must be > 0")
|
|
50
|
+
return values
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def quote_shell(text: str) -> str:
|
|
54
|
+
if os.name == "nt":
|
|
55
|
+
return subprocess.list2cmdline([text])
|
|
56
|
+
import shlex
|
|
57
|
+
|
|
58
|
+
return shlex.quote(text)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def run_checked(
|
|
62
|
+
args: list[str], *, cwd: Path | None = None
|
|
63
|
+
) -> subprocess.CompletedProcess[str]:
|
|
64
|
+
return subprocess.run(args, cwd=cwd, check=True, text=True, capture_output=True)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def require_command(name: str) -> None:
|
|
68
|
+
if shutil.which(name) is None:
|
|
69
|
+
raise RuntimeError(f"required command is not installed or not on PATH: {name}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve_tool_path(name: str) -> Path:
|
|
73
|
+
path = shutil.which(name)
|
|
74
|
+
if path is None:
|
|
75
|
+
raise RuntimeError(
|
|
76
|
+
f"{name} executable not found on PATH in this uv environment"
|
|
77
|
+
)
|
|
78
|
+
return Path(path)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_yaml_blob(target_bytes: int, seed: int) -> str:
|
|
82
|
+
lines = ["root:"]
|
|
83
|
+
alphabet = string.ascii_lowercase + string.digits
|
|
84
|
+
index = 0
|
|
85
|
+
while len(("\n".join(lines) + "\n").encode("utf-8")) < target_bytes:
|
|
86
|
+
offset = (seed + index) % len(alphabet)
|
|
87
|
+
token = "".join(alphabet[(offset + i) % len(alphabet)] for i in range(28))
|
|
88
|
+
lines.append(f' key_{index:06d}: "{token}"')
|
|
89
|
+
index += 1
|
|
90
|
+
return "\n".join(lines) + "\n"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def materialize_case(case: Case, seed: int) -> None:
|
|
94
|
+
case.dataset_dir.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
target_bytes = case.file_size_kib * 1024
|
|
96
|
+
for file_index in range(case.file_count):
|
|
97
|
+
payload = build_yaml_blob(target_bytes=target_bytes, seed=seed + file_index)
|
|
98
|
+
file_path = case.dataset_dir / f"file_{file_index:05d}.yaml"
|
|
99
|
+
file_path.write_text(payload, encoding="utf-8")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def iter_cases(
|
|
103
|
+
file_counts: Iterable[int], file_sizes_kib: Iterable[int], base_dir: Path
|
|
104
|
+
) -> list[Case]:
|
|
105
|
+
cases: list[Case] = []
|
|
106
|
+
for size_kib in file_sizes_kib:
|
|
107
|
+
for count in file_counts:
|
|
108
|
+
case_dir = base_dir / f"files_{count:05d}__size_{size_kib:04d}kib"
|
|
109
|
+
cases.append(
|
|
110
|
+
Case(file_count=count, file_size_kib=size_kib, dataset_dir=case_dir)
|
|
111
|
+
)
|
|
112
|
+
return cases
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def expand_range(
|
|
116
|
+
*, start: int | None, end: int | None, step: int | None, value_name: str
|
|
117
|
+
) -> list[int] | None:
|
|
118
|
+
values = (start, end, step)
|
|
119
|
+
if all(value is None for value in values):
|
|
120
|
+
return None
|
|
121
|
+
if any(value is None for value in values):
|
|
122
|
+
raise typer.BadParameter(
|
|
123
|
+
f"{value_name} range requires start, end, and step together."
|
|
124
|
+
)
|
|
125
|
+
if start <= 0 or end <= 0 or step <= 0:
|
|
126
|
+
raise typer.BadParameter(f"{value_name} range values must be > 0.")
|
|
127
|
+
if end < start:
|
|
128
|
+
raise typer.BadParameter(f"{value_name} range end must be >= start.")
|
|
129
|
+
return list(range(start, end + 1, step))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def benchmark_case(
|
|
133
|
+
case: Case,
|
|
134
|
+
*,
|
|
135
|
+
ryl_bin: Path,
|
|
136
|
+
yamllint_bin: Path,
|
|
137
|
+
runs: int,
|
|
138
|
+
warmup: int,
|
|
139
|
+
output_json_path: Path,
|
|
140
|
+
) -> dict[str, dict[str, float | list[float] | str]]:
|
|
141
|
+
cfg = "extends: relaxed"
|
|
142
|
+
ryl_cmd = f"{quote_shell(str(ryl_bin))} -d {quote_shell(cfg)} {quote_shell(str(case.dataset_dir))}"
|
|
143
|
+
yamllint_cmd = f"{quote_shell(str(yamllint_bin))} -d {quote_shell(cfg)} {quote_shell(str(case.dataset_dir))}"
|
|
144
|
+
run_checked(
|
|
145
|
+
[
|
|
146
|
+
"hyperfine",
|
|
147
|
+
"--runs",
|
|
148
|
+
str(runs),
|
|
149
|
+
"--warmup",
|
|
150
|
+
str(warmup),
|
|
151
|
+
"--export-json",
|
|
152
|
+
str(output_json_path),
|
|
153
|
+
"-n",
|
|
154
|
+
"ryl",
|
|
155
|
+
ryl_cmd,
|
|
156
|
+
"-n",
|
|
157
|
+
"yamllint",
|
|
158
|
+
yamllint_cmd,
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
raw = orjson.loads(output_json_path.read_bytes())
|
|
162
|
+
parsed: dict[str, dict[str, float | list[float] | str]] = {}
|
|
163
|
+
for result in raw["results"]:
|
|
164
|
+
parsed[str(result["command"])] = {
|
|
165
|
+
"mean": float(result["mean"]),
|
|
166
|
+
"stddev": float(result["stddev"] or 0.0),
|
|
167
|
+
"min": float(result["min"]),
|
|
168
|
+
"max": float(result["max"]),
|
|
169
|
+
"times": [float(value) for value in result["times"]],
|
|
170
|
+
}
|
|
171
|
+
return parsed
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def plot_results(
|
|
175
|
+
df: pl.DataFrame,
|
|
176
|
+
out_png: Path,
|
|
177
|
+
out_svg: Path,
|
|
178
|
+
*,
|
|
179
|
+
runs: int,
|
|
180
|
+
ryl_version: str,
|
|
181
|
+
yamllint_version: str,
|
|
182
|
+
) -> None:
|
|
183
|
+
plt.style.use("seaborn-v0_8-whitegrid")
|
|
184
|
+
tools = ["ryl", "yamllint"]
|
|
185
|
+
sizes = sorted(int(value) for value in df["file_size_kib"].unique().to_list())
|
|
186
|
+
cmap = plt.cm.Blues
|
|
187
|
+
min_tone = 0.35
|
|
188
|
+
max_tone = 0.95
|
|
189
|
+
tones = [
|
|
190
|
+
min_tone + (max_tone - min_tone) * idx / max(len(sizes) - 1, 1)
|
|
191
|
+
for idx in range(len(sizes))
|
|
192
|
+
]
|
|
193
|
+
fig, axes = plt.subplots(1, len(tools), figsize=(14, 5.5), sharey=True)
|
|
194
|
+
if len(tools) == 1:
|
|
195
|
+
axes = [axes]
|
|
196
|
+
version_map = {"ryl": ryl_version, "yamllint": yamllint_version}
|
|
197
|
+
for axis, tool in zip(axes, tools):
|
|
198
|
+
tool_df = df.filter(pl.col("tool") == tool)
|
|
199
|
+
for idx, size in enumerate(sizes):
|
|
200
|
+
size_df = tool_df.filter(pl.col("file_size_kib") == size).sort("file_count")
|
|
201
|
+
x_values = [int(value) for value in size_df["file_count"].to_list()]
|
|
202
|
+
y_values = [float(value) for value in size_df["mean_seconds"].to_list()]
|
|
203
|
+
y_stddev = [float(value) for value in size_df["stddev_seconds"].to_list()]
|
|
204
|
+
color = cmap(tones[idx])
|
|
205
|
+
axis.plot(
|
|
206
|
+
x_values,
|
|
207
|
+
y_values,
|
|
208
|
+
color=color,
|
|
209
|
+
linewidth=2,
|
|
210
|
+
marker="o",
|
|
211
|
+
label=f"{size} KiB",
|
|
212
|
+
)
|
|
213
|
+
axis.fill_between(
|
|
214
|
+
x_values,
|
|
215
|
+
[value - std for value, std in zip(y_values, y_stddev)],
|
|
216
|
+
[value + std for value, std in zip(y_values, y_stddev)],
|
|
217
|
+
color=color,
|
|
218
|
+
alpha=0.16,
|
|
219
|
+
)
|
|
220
|
+
axis.set_title(version_map.get(tool, tool))
|
|
221
|
+
axis.set_xlabel("Number of YAML files")
|
|
222
|
+
axes[0].set_ylabel("Mean runtime (seconds)")
|
|
223
|
+
axes[-1].legend(title="File size", loc="upper left")
|
|
224
|
+
fig.suptitle(f"ryl vs yamllint (hyperfine, {runs} runs per point)", fontsize=13)
|
|
225
|
+
fig.tight_layout()
|
|
226
|
+
out_png.parent.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
fig.savefig(out_png, dpi=170)
|
|
228
|
+
fig.savefig(out_svg)
|
|
229
|
+
plt.close(fig)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@app.command()
|
|
233
|
+
def main(
|
|
234
|
+
file_counts: str = typer.Option(
|
|
235
|
+
"25,100,400,1000",
|
|
236
|
+
help="Comma-separated file counts. Ignored when --file-count-start/end/step are set.",
|
|
237
|
+
),
|
|
238
|
+
file_count_start: int | None = typer.Option(
|
|
239
|
+
None, help="Start of file-count range (inclusive)."
|
|
240
|
+
),
|
|
241
|
+
file_count_end: int | None = typer.Option(
|
|
242
|
+
None, help="End of file-count range (inclusive)."
|
|
243
|
+
),
|
|
244
|
+
file_count_step: int | None = typer.Option(
|
|
245
|
+
None, help="Increment for file-count range."
|
|
246
|
+
),
|
|
247
|
+
file_sizes_kib: str = typer.Option(
|
|
248
|
+
"1,8,32,128",
|
|
249
|
+
help="Comma-separated file sizes in KiB. Ignored when --file-size-start-kib/end/step are set.",
|
|
250
|
+
),
|
|
251
|
+
file_size_start_kib: int | None = typer.Option(
|
|
252
|
+
None, help="Start of file-size range in KiB (inclusive)."
|
|
253
|
+
),
|
|
254
|
+
file_size_end_kib: int | None = typer.Option(
|
|
255
|
+
None, help="End of file-size range in KiB (inclusive)."
|
|
256
|
+
),
|
|
257
|
+
file_size_step_kib: int | None = typer.Option(
|
|
258
|
+
None, help="Increment for file-size range in KiB."
|
|
259
|
+
),
|
|
260
|
+
runs: int = typer.Option(10, help="Number of hyperfine runs per point."),
|
|
261
|
+
warmup: int = typer.Option(2, help="Number of warmup runs per point."),
|
|
262
|
+
seed: int = typer.Option(7331, help="Base RNG seed for synthetic YAML generation."),
|
|
263
|
+
output_dir: Path = typer.Option(
|
|
264
|
+
Path("manual_outputs") / "benchmarks",
|
|
265
|
+
help="Directory where all artifacts are written.",
|
|
266
|
+
),
|
|
267
|
+
keep_datasets: bool = typer.Option(
|
|
268
|
+
False, help="Keep generated YAML datasets on disk instead of deleting them."
|
|
269
|
+
),
|
|
270
|
+
) -> None:
|
|
271
|
+
if runs <= 0:
|
|
272
|
+
raise typer.BadParameter("--runs must be > 0")
|
|
273
|
+
if warmup < 0:
|
|
274
|
+
raise typer.BadParameter("--warmup must be >= 0")
|
|
275
|
+
|
|
276
|
+
file_counts_range = expand_range(
|
|
277
|
+
start=file_count_start,
|
|
278
|
+
end=file_count_end,
|
|
279
|
+
step=file_count_step,
|
|
280
|
+
value_name="file count",
|
|
281
|
+
)
|
|
282
|
+
file_sizes_range = expand_range(
|
|
283
|
+
start=file_size_start_kib,
|
|
284
|
+
end=file_size_end_kib,
|
|
285
|
+
step=file_size_step_kib,
|
|
286
|
+
value_name="file size",
|
|
287
|
+
)
|
|
288
|
+
try:
|
|
289
|
+
file_counts_values = (
|
|
290
|
+
file_counts_range
|
|
291
|
+
if file_counts_range is not None
|
|
292
|
+
else parse_int_list(file_counts)
|
|
293
|
+
)
|
|
294
|
+
file_size_values = (
|
|
295
|
+
file_sizes_range
|
|
296
|
+
if file_sizes_range is not None
|
|
297
|
+
else parse_int_list(file_sizes_kib)
|
|
298
|
+
)
|
|
299
|
+
except ValueError as err:
|
|
300
|
+
raise typer.BadParameter(str(err)) from err
|
|
301
|
+
|
|
302
|
+
require_command("uv")
|
|
303
|
+
require_command("hyperfine")
|
|
304
|
+
|
|
305
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
306
|
+
run_dir = output_dir / timestamp
|
|
307
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
308
|
+
dataset_root = run_dir / "datasets"
|
|
309
|
+
dataset_root.mkdir(parents=True, exist_ok=True)
|
|
310
|
+
raw_dir = run_dir / "hyperfine-json"
|
|
311
|
+
raw_dir.mkdir(parents=True, exist_ok=True)
|
|
312
|
+
|
|
313
|
+
ryl_bin = resolve_tool_path("ryl")
|
|
314
|
+
yamllint_bin = resolve_tool_path("yamllint")
|
|
315
|
+
ryl_version = run_checked([str(ryl_bin), "--version"]).stdout.strip()
|
|
316
|
+
yamllint_version = run_checked([str(yamllint_bin), "--version"]).stdout.strip()
|
|
317
|
+
|
|
318
|
+
cases = iter_cases(file_counts_values, file_size_values, dataset_root)
|
|
319
|
+
rows: list[dict[str, float | int | str]] = []
|
|
320
|
+
for case_index, case in enumerate(tqdm(cases, desc="Benchmark cases", unit="case")):
|
|
321
|
+
materialize_case(case, seed=seed + case_index * 100_000)
|
|
322
|
+
case_json = (
|
|
323
|
+
raw_dir / f"files_{case.file_count}__size_{case.file_size_kib}kib.json"
|
|
324
|
+
)
|
|
325
|
+
results = benchmark_case(
|
|
326
|
+
case,
|
|
327
|
+
ryl_bin=ryl_bin,
|
|
328
|
+
yamllint_bin=yamllint_bin,
|
|
329
|
+
runs=runs,
|
|
330
|
+
warmup=warmup,
|
|
331
|
+
output_json_path=case_json,
|
|
332
|
+
)
|
|
333
|
+
for tool in ("ryl", "yamllint"):
|
|
334
|
+
row = {
|
|
335
|
+
"tool": tool,
|
|
336
|
+
"file_count": case.file_count,
|
|
337
|
+
"file_size_kib": case.file_size_kib,
|
|
338
|
+
"mean_seconds": float(results[tool]["mean"]),
|
|
339
|
+
"stddev_seconds": float(results[tool]["stddev"]),
|
|
340
|
+
"min_seconds": float(results[tool]["min"]),
|
|
341
|
+
"max_seconds": float(results[tool]["max"]),
|
|
342
|
+
}
|
|
343
|
+
rows.append(row)
|
|
344
|
+
|
|
345
|
+
results_df = pl.DataFrame(rows).select(
|
|
346
|
+
[
|
|
347
|
+
"tool",
|
|
348
|
+
"file_count",
|
|
349
|
+
"file_size_kib",
|
|
350
|
+
"mean_seconds",
|
|
351
|
+
"stddev_seconds",
|
|
352
|
+
"min_seconds",
|
|
353
|
+
"max_seconds",
|
|
354
|
+
]
|
|
355
|
+
)
|
|
356
|
+
csv_path = run_dir / "summary.csv"
|
|
357
|
+
results_df.write_csv(csv_path)
|
|
358
|
+
|
|
359
|
+
meta_path = run_dir / "meta.json"
|
|
360
|
+
meta_path.write_bytes(
|
|
361
|
+
orjson.dumps(
|
|
362
|
+
{
|
|
363
|
+
"generated_at_utc": timestamp,
|
|
364
|
+
"ryl_version": ryl_version,
|
|
365
|
+
"yamllint_version": yamllint_version,
|
|
366
|
+
"runs": runs,
|
|
367
|
+
"warmup": warmup,
|
|
368
|
+
"file_counts": file_counts_values,
|
|
369
|
+
"file_sizes_kib": file_size_values,
|
|
370
|
+
},
|
|
371
|
+
option=orjson.OPT_INDENT_2,
|
|
372
|
+
)
|
|
373
|
+
+ b"\n"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
plot_png = run_dir / "benchmark.png"
|
|
377
|
+
plot_svg = run_dir / "benchmark.svg"
|
|
378
|
+
plot_results(
|
|
379
|
+
results_df,
|
|
380
|
+
plot_png,
|
|
381
|
+
plot_svg,
|
|
382
|
+
runs=runs,
|
|
383
|
+
ryl_version=ryl_version,
|
|
384
|
+
yamllint_version=yamllint_version,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if not keep_datasets:
|
|
388
|
+
shutil.rmtree(dataset_root)
|
|
389
|
+
|
|
390
|
+
print(f"Benchmark complete. Artifacts: {run_dir}")
|
|
391
|
+
print(f"Versions: {ryl_version}; {yamllint_version}")
|
|
392
|
+
print(f"Plot: {plot_png}")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
if __name__ == "__main__":
|
|
396
|
+
try:
|
|
397
|
+
app()
|
|
398
|
+
except KeyboardInterrupt:
|
|
399
|
+
print("Interrupted.", file=sys.stderr)
|
|
400
|
+
raise SystemExit(130)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
Set-StrictMode -Version Latest
|
|
3
|
+
$ErrorActionPreference = 'Stop'
|
|
4
|
+
|
|
5
|
+
function Fail([string]$Message) {
|
|
6
|
+
[Console]::Error.WriteLine($Message)
|
|
7
|
+
exit 1
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function Require-Command([string]$Name) {
|
|
11
|
+
if (-not (Get-Command -Name $Name -ErrorAction SilentlyContinue)) {
|
|
12
|
+
Fail "$Name is required to run this script"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
17
|
+
if (-not $scriptRoot) { $scriptRoot = Get-Location }
|
|
18
|
+
$projectRoot = (Resolve-Path (Join-Path $scriptRoot '..')).Path
|
|
19
|
+
$normalizedRoot = $projectRoot -replace '\\', '/'
|
|
20
|
+
if (-not $normalizedRoot.EndsWith('/')) { $normalizedRoot += '/' }
|
|
21
|
+
|
|
22
|
+
Require-Command 'cargo'
|
|
23
|
+
Require-Command 'jq'
|
|
24
|
+
|
|
25
|
+
$tmp = $null
|
|
26
|
+
$locationPushed = $false
|
|
27
|
+
try {
|
|
28
|
+
Push-Location $projectRoot
|
|
29
|
+
$locationPushed = $true
|
|
30
|
+
|
|
31
|
+
& cargo llvm-cov nextest --summary-only *> $null
|
|
32
|
+
if ($LASTEXITCODE -ne 0) {
|
|
33
|
+
Fail 'cargo llvm-cov nextest --summary-only failed; inspect the output above for details.'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
$tmp = [System.IO.Path]::GetTempFileName()
|
|
37
|
+
|
|
38
|
+
& cargo llvm-cov report --json --output-path $tmp *> $null
|
|
39
|
+
if ($LASTEXITCODE -ne 0) {
|
|
40
|
+
Fail 'Failed to generate coverage report.'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
$jqFilter = @'
|
|
44
|
+
def ranges:
|
|
45
|
+
sort
|
|
46
|
+
| unique
|
|
47
|
+
| reduce .[] as $line ([];
|
|
48
|
+
if length > 0 and $line == (.[-1][1] + 1) then
|
|
49
|
+
(.[-1] = [.[-1][0], $line])
|
|
50
|
+
else
|
|
51
|
+
. + [[ $line, $line ]]
|
|
52
|
+
end)
|
|
53
|
+
| map(if .[0] == .[1] then (.[0] | tostring) else "\(.[0])-\(.[1])" end);
|
|
54
|
+
|
|
55
|
+
.data[].files[]
|
|
56
|
+
| select(.summary.regions.percent < 100)
|
|
57
|
+
| {file: (.filename | gsub("\\\\"; "/") | sub("^" + $prefix; "")),
|
|
58
|
+
uncovered: [ .segments[]
|
|
59
|
+
| select(.[2] == 0 and .[3] == true and .[5] == false)
|
|
60
|
+
| .[0]
|
|
61
|
+
] }
|
|
62
|
+
| select(.uncovered | length > 0)
|
|
63
|
+
| "\(.file):\(.uncovered | ranges | join(","))"
|
|
64
|
+
'@
|
|
65
|
+
|
|
66
|
+
$report = (& jq -r --arg prefix $normalizedRoot $jqFilter $tmp)
|
|
67
|
+
|
|
68
|
+
if ([string]::IsNullOrWhiteSpace($report)) {
|
|
69
|
+
Write-Output 'Coverage OK: no uncovered regions.'
|
|
70
|
+
} else {
|
|
71
|
+
Write-Output 'Uncovered regions (file:path line ranges):'
|
|
72
|
+
$report | ForEach-Object { Write-Output $_ }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
if ($tmp -and (Test-Path -LiteralPath $tmp)) {
|
|
77
|
+
Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
|
|
78
|
+
}
|
|
79
|
+
if ($locationPushed) { Pop-Location }
|
|
80
|
+
}
|