Split kondo ratchet budgets into separate prod and test files (#83383)
* Split kondo ratchet budgets into separate prod and test files Prod and test kondo suppressions shared one .clj-kondo/ratchets.edn budget per linter, so a pile of test-code ignores could mask a prod-code regression (or vice versa) inside the same number. Adds .clj-kondo/ratchets-test.edn, tracking :ignore-counts for anything under a `test` directory, independent of the prod file's budget. :config-counts (.clj-kondo/config.edn suppressions) stays combined in the prod file -- config.edn's own scoping mixes test and prod namespaces in several groups, so it can't be reliably split per suppression. Updates every consumer: dev.kondo-ratchet's fix!/check, bin/merge-kondo-ratchets, the backport and release-cut disable scripts, the shrink workflow, and file-paths.yaml/.gitignore. * Fix test-ratchets validation and exemption-suggestion bugs from roborev fix!/check read the test ratchets file with *ratchets-file* rebound to *test-ratchets-file*, but validated it after that binding ended, so an unknown linter there was reported against the prod path instead. Validation now runs inside the binding, and both now reject a nonempty :config-counts on the test file instead of letting fix! silently drop it. kondo-insert-ignores also only ever suggested adding a new linter's :comment-exempt to the prod file, even when every unjustified occurrence was test-only. It now checks prod and test occurrences against each file's own exemptions independently and names whichever file(s) actually need it. * Dedupe test-ratchets read+validate, simplify test-occurrence? fix! and check each rebuilt the same read-validate-test-file sequence inline; extracted into read-validated-test-ratchets. test-occurrence? no longer builds a throwaway set just to check one membership. * Note what splitting :config-counts later would actually cost * Fix Eastwood reflection warning in new test helper empty-test-ratchets-file! returned an untyped value, so .getPath on it needed reflection at each call site. Hinting the return type as java.io.File fixes it -- CI's changed-tests Eastwood run caught this. * Move return type hint onto the arg vector Clj-Kondo prefers the hint on the arg vector to the one on the fn name. * Move shrink PR body file reading into dev.kondo-ratchet The bb task only parses its arguments now; reading and combining the ratchet files lives next to shrink-pr-body, with a test. * Give the test ratchets file its own header and drop its :config-counts The test file was rendered with the prod header, whose last lines point readers at ratchets-test.edn and say :config-counts "stays here" -- both wrong inside the test file. It also carried an empty :config-counts that the check rejects whenever it's nonempty. render-test writes the test file with its own header and no :config-counts, and throws on a nonempty one rather than dropping it. fix!, the normalization check, and the three-way merge (a new "test" kind) all use it.
This commit is contained in:
35
.clj-kondo/ratchets-test.edn
Normal file
35
.clj-kondo/ratchets-test.edn
Normal file
@@ -0,0 +1,35 @@
|
||||
;; Budgets for kondo suppressions in test code: inline `:clj-kondo/ignore` forms per linter
|
||||
;; (:ignore-counts). Production code has its own budgets in .clj-kondo/ratchets.edn, which also holds
|
||||
;; every config-level waiver (:config-counts) -- config.edn's scoping doesn't cleanly split.
|
||||
;; Each :ignore-counts value is a non-negative integer budget, or :unlimited for no ceiling.
|
||||
;; Checks fail when a count exceeds its numeric budget; unused budget is allowed.
|
||||
;; Any ignore outside :comment-exempt needs an explanatory comment directly above or trailing on its line.
|
||||
;; `./bin/mage kondo-ratchets-shrink` lowers budgets unless `--seed` explicitly adds or raises one.
|
||||
;; The workflow runs it on master, so feature branches do not need to record reductions.
|
||||
;; Raising or adding a budget (`--seed` for inline ignores, a manual edit otherwise) or widening the
|
||||
;; exemptions must be explained in the PR.
|
||||
;; :all is the vector-less ignore form, which suppresses every linter on the next form.
|
||||
{:ignore-counts {:case-symbol-test 1
|
||||
:clojure-lsp/unused-public-var 1
|
||||
:deprecated-namespace 66
|
||||
:deprecated-var 32
|
||||
:discouraged-namespace 8
|
||||
:discouraged-var 59
|
||||
:equals-true 1
|
||||
:metabase/disallow-hardcoded-driver-names-in-tests 34
|
||||
:metabase/discourage-dynamic-vars 41
|
||||
:metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests 13
|
||||
:metabase/namespace-name 1
|
||||
:metabase/prefer-with-dynamic-fn-redefs :unlimited
|
||||
:metabase/test-helpers-use-non-thread-safe-functions 18
|
||||
:metabase/validate-deftest 9
|
||||
:missing-protocol-method 3
|
||||
:type-mismatch 6
|
||||
:uninitialized-var 1
|
||||
:unresolved-symbol 2
|
||||
:unused-binding 6
|
||||
:unused-private-var 2}
|
||||
:comment-exempt #{:deprecated-namespace
|
||||
:discouraged-namespace
|
||||
:discouraged-var
|
||||
:metabase/discourage-dynamic-vars}}
|
||||
@@ -8,49 +8,43 @@
|
||||
;; Raising or adding a budget (`--seed` for inline ignores, a manual edit otherwise) or widening the
|
||||
;; exemptions must be explained in the PR.
|
||||
;; :all is the vector-less ignore form, which suppresses every linter on the next form.
|
||||
{:ignore-counts {:aliased-namespace-symbol 3
|
||||
:case-symbol-test 1
|
||||
:clojure-lsp/unused-public-var 9
|
||||
:consistent-alias 2
|
||||
:def-fn 1
|
||||
:deprecated-namespace 95
|
||||
:deprecated-var 85
|
||||
:discouraged-java-method 4
|
||||
:discouraged-namespace 68
|
||||
:discouraged-var 132
|
||||
:duplicate-require 1
|
||||
:equals-true 1
|
||||
:main-without-gen-class 1
|
||||
:metabase/disallow-hardcoded-driver-names-in-tests 34
|
||||
:metabase/discourage-dynamic-vars 364
|
||||
:metabase/discourage-millis-duration 1
|
||||
:metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests 13
|
||||
:metabase/modules 23
|
||||
:metabase/namespace-name 4
|
||||
:metabase/prefer-with-dynamic-fn-redefs :unlimited
|
||||
:metabase/test-helpers-use-non-thread-safe-functions 18
|
||||
:metabase/validate-defendpoint-has-response-schema 427
|
||||
:metabase/validate-defendpoint-query-params-use-kebab-case 48
|
||||
:metabase/validate-defendpoint-route-uses-kebab-case 44
|
||||
:metabase/validate-deftest 9
|
||||
:missing-docstring 4
|
||||
:missing-protocol-method 3
|
||||
:non-arg-vec-return-type-hint 1
|
||||
:reduce-without-init 3
|
||||
:redundant-do 1
|
||||
:syntax 2
|
||||
:type-mismatch 8
|
||||
:uninitialized-var 1
|
||||
:unquote-not-syntax-quoted 1
|
||||
:unresolved-namespace 3
|
||||
:unresolved-symbol 11
|
||||
:unsorted-required-namespaces :unlimited
|
||||
:unused-alias 2
|
||||
:unused-binding 9
|
||||
:unused-excluded-var 2
|
||||
:unused-namespace 2
|
||||
:unused-private-var 8
|
||||
:unused-value 1}
|
||||
;;
|
||||
;; Test code's :ignore-counts has its own budget in .clj-kondo/ratchets-test.edn.
|
||||
;; :config-counts stays here either way -- config.edn's scoping doesn't cleanly split.
|
||||
{:ignore-counts {:aliased-namespace-symbol 3
|
||||
:clojure-lsp/unused-public-var 8
|
||||
:consistent-alias 2
|
||||
:def-fn 1
|
||||
:deprecated-namespace 29
|
||||
:deprecated-var 53
|
||||
:discouraged-java-method 4
|
||||
:discouraged-namespace 60
|
||||
:discouraged-var 73
|
||||
:duplicate-require 1
|
||||
:main-without-gen-class 1
|
||||
:metabase/discourage-dynamic-vars 325
|
||||
:metabase/discourage-millis-duration 1
|
||||
:metabase/modules 23
|
||||
:metabase/namespace-name 3
|
||||
:metabase/validate-defendpoint-has-response-schema 427
|
||||
:metabase/validate-defendpoint-query-params-use-kebab-case 48
|
||||
:metabase/validate-defendpoint-route-uses-kebab-case 44
|
||||
:missing-docstring 4
|
||||
:non-arg-vec-return-type-hint 1
|
||||
:reduce-without-init 3
|
||||
:redundant-do 1
|
||||
:syntax 2
|
||||
:type-mismatch 2
|
||||
:unquote-not-syntax-quoted 1
|
||||
:unresolved-namespace 3
|
||||
:unresolved-symbol 9
|
||||
:unsorted-required-namespaces :unlimited
|
||||
:unused-alias 2
|
||||
:unused-binding 3
|
||||
:unused-excluded-var 2
|
||||
:unused-namespace 2
|
||||
:unused-private-var 6
|
||||
:unused-value 1}
|
||||
:config-counts {:constant-condition 1
|
||||
:deprecated-namespace 7
|
||||
:deprecated-var 16
|
||||
|
||||
@@ -34,6 +34,7 @@ disabled_ratchets=';; Kondo ignore ratchets apply only to master; this release b
|
||||
# divergent ratchets ratchets (on a pty) none finish without opening an editor
|
||||
# active ratchets unrelated file app.txt leave the conflict for manual resolution
|
||||
# divergent ratchets ratchets + app.txt app.txt resolve only ratchets
|
||||
# active ratchets test ratchets only none disable only the test ratchets file
|
||||
# release cut ratchets none cut and backport commit the same opt-out
|
||||
# n/a cut-release workflow n/a the workflow calls the shared writer
|
||||
# any invalid commit n/a propagate the cherry-pick failure
|
||||
@@ -155,29 +156,52 @@ test_mixed_conflict_resolves_only_ratchets() (
|
||||
assert_cherry_pick_pending
|
||||
)
|
||||
|
||||
test_test_ratchets_only_backport_disables_only_that_file() (
|
||||
local repo="$test_root/test-ratchet-only"
|
||||
init_repo "$repo"
|
||||
local commit
|
||||
commit=$(feature_commit "$repo" change_test_ratchets)
|
||||
git -C "$repo" checkout -q release
|
||||
|
||||
cd "$repo"
|
||||
cherry_pick_backport "$commit"
|
||||
|
||||
assert_ratchets "$active_ratchets"
|
||||
assert_test_ratchets_disabled
|
||||
assert_cherry_pick_completed
|
||||
assert_worktree_clean
|
||||
)
|
||||
|
||||
test_release_cut_and_backport_commit_the_same_opt_out() (
|
||||
local cut_repo="$test_root/release-cut"
|
||||
init_repo "$cut_repo"
|
||||
cd "$cut_repo"
|
||||
git checkout -qb release-x.99.x
|
||||
write_disabled_ratchets
|
||||
git add --sparse .clj-kondo/ratchets.edn
|
||||
write_disabled_ratchets .clj-kondo/ratchets.edn
|
||||
write_disabled_ratchets .clj-kondo/ratchets-test.edn
|
||||
git add --sparse .clj-kondo/ratchets.edn .clj-kondo/ratchets-test.edn
|
||||
git commit -q --allow-empty --no-verify -m "Cut release-x.99.x branch"
|
||||
git show HEAD:.clj-kondo/ratchets.edn > "$test_root/release-cut.edn"
|
||||
git show HEAD:.clj-kondo/ratchets-test.edn > "$test_root/release-cut-test.edn"
|
||||
|
||||
local backport_repo="$test_root/release-cut-backport"
|
||||
init_repo "$backport_repo"
|
||||
local commit
|
||||
commit=$(feature_commit "$backport_repo" change_ratchets)
|
||||
commit=$(feature_commit "$backport_repo" change_both_ratchets)
|
||||
git -C "$backport_repo" checkout -q release
|
||||
cd "$backport_repo"
|
||||
cherry_pick_backport "$commit"
|
||||
git show HEAD:.clj-kondo/ratchets.edn > "$test_root/backport.edn"
|
||||
git show HEAD:.clj-kondo/ratchets-test.edn > "$test_root/backport-test.edn"
|
||||
|
||||
cmp -s "$test_root/release-cut.edn" "$test_root/backport.edn" ||
|
||||
fail "the release cut and the backport committed different opt-out files"
|
||||
fail "the release cut and the backport committed different prod opt-out files"
|
||||
cmp -s "$test_root/release-cut-test.edn" "$test_root/backport-test.edn" ||
|
||||
fail "the release cut and the backport committed different test opt-out files"
|
||||
assert_eq "$disabled_ratchets" "$(cat "$test_root/release-cut.edn")" \
|
||||
"the release cut did not install the expected opt-out"
|
||||
"the release cut did not install the expected prod opt-out"
|
||||
assert_eq "$disabled_ratchets" "$(cat "$test_root/release-cut-test.edn")" \
|
||||
"the release cut did not install the expected test opt-out"
|
||||
)
|
||||
|
||||
test_cut_release_workflow_uses_the_shared_writer() (
|
||||
@@ -253,6 +277,14 @@ assert_ratchets_disabled() {
|
||||
assert_ratchets "$disabled_ratchets"
|
||||
}
|
||||
|
||||
assert_test_ratchets() {
|
||||
assert_eq "$1" "$(cat .clj-kondo/ratchets-test.edn)" "unexpected test-ratchet state"
|
||||
}
|
||||
|
||||
assert_test_ratchets_disabled() {
|
||||
assert_test_ratchets "$disabled_ratchets"
|
||||
}
|
||||
|
||||
assert_cherry_pick_completed() {
|
||||
! git rev-parse -q --verify CHERRY_PICK_HEAD >/dev/null || fail "the cherry-pick is still pending"
|
||||
}
|
||||
@@ -285,6 +317,7 @@ init_repo() {
|
||||
printf 'base\n' > "$repo/app.txt"
|
||||
mkdir -p "$repo/.clj-kondo"
|
||||
printf '%s\n' "$active_ratchets" > "$repo/.clj-kondo/ratchets.edn"
|
||||
printf '%s\n' "$active_ratchets" > "$repo/.clj-kondo/ratchets-test.edn"
|
||||
git -C "$repo" add .
|
||||
git -C "$repo" commit -qm "Base"
|
||||
}
|
||||
@@ -317,6 +350,15 @@ change_ratchets() {
|
||||
printf '{:ignore-counts {:example 2}}\n' > "$1/.clj-kondo/ratchets.edn"
|
||||
}
|
||||
|
||||
change_test_ratchets() {
|
||||
printf '{:ignore-counts {:example 2}}\n' > "$1/.clj-kondo/ratchets-test.edn"
|
||||
}
|
||||
|
||||
change_both_ratchets() {
|
||||
change_ratchets "$1"
|
||||
change_test_ratchets "$1"
|
||||
}
|
||||
|
||||
change_app_and_ratchets() {
|
||||
change_app "$1"
|
||||
change_ratchets "$1"
|
||||
@@ -335,9 +377,9 @@ change_release_app_and_ratchets() {
|
||||
change_release_ratchets "$1"
|
||||
}
|
||||
|
||||
# Setup writes the opt-out through the shared writer; the only copy is the expected text at the top.
|
||||
# Setup writes the opt-out through the shared writer; the only copies are the expected text at the top.
|
||||
disable_release_ratchets() {
|
||||
(cd "$1" && write_disabled_ratchets)
|
||||
(cd "$1" && write_disabled_ratchets .clj-kondo/ratchets.edn && write_disabled_ratchets .clj-kondo/ratchets-test.edn)
|
||||
}
|
||||
|
||||
commit_release_change() {
|
||||
@@ -369,6 +411,7 @@ run_test "ratchet-only conflict is resolved"
|
||||
run_test "continuation never opens an editor"
|
||||
run_test "non-ratchet conflict remains manual"
|
||||
run_test "mixed conflict resolves only ratchets"
|
||||
run_test "test ratchets only backport disables only that file"
|
||||
run_test "release cut and backport commit the same opt-out"
|
||||
run_test "cut-release workflow uses the shared writer"
|
||||
run_test "invalid commit failure is propagated"
|
||||
|
||||
@@ -5,22 +5,28 @@ set -euo pipefail
|
||||
# The only place that knows what a release branch's opt-out looks like. The cut-release workflow, the
|
||||
# backport helper, and the tests all go through this function.
|
||||
write_disabled_ratchets() {
|
||||
mkdir -p .clj-kondo
|
||||
local file=$1
|
||||
mkdir -p "$(dirname "$file")"
|
||||
printf '%s\n' \
|
||||
';; Kondo ignore ratchets apply only to master; this release branch opts out.' \
|
||||
';; The test and fixer recognize this explicit opt-out.' \
|
||||
'{:disabled true}' \
|
||||
> .clj-kondo/ratchets.edn
|
||||
> "$file"
|
||||
}
|
||||
|
||||
disable_ratchets() {
|
||||
local commit=$1
|
||||
local file=.clj-kondo/ratchets.edn
|
||||
[ -n "$(git diff-tree --root --no-commit-id --name-only -r "$commit" -- "$file")" ] || return 0
|
||||
local file
|
||||
local disabled_any=
|
||||
for file in .clj-kondo/ratchets.edn .clj-kondo/ratchets-test.edn; do
|
||||
[ -n "$(git diff-tree --root --no-commit-id --name-only -r "$commit" -- "$file")" ] || continue
|
||||
|
||||
echo "$file: disabling kondo ratchets on the release branch"
|
||||
write_disabled_ratchets
|
||||
git add -- "$file"
|
||||
echo "$file: disabling kondo ratchets on the release branch"
|
||||
write_disabled_ratchets "$file"
|
||||
git add -- "$file"
|
||||
disabled_any=1
|
||||
done
|
||||
[ -n "$disabled_any" ] || return 0
|
||||
|
||||
if ! git rev-parse -q --verify CHERRY_PICK_HEAD >/dev/null; then
|
||||
if ! git diff --cached --quiet HEAD; then
|
||||
@@ -30,7 +36,7 @@ disable_ratchets() {
|
||||
fi
|
||||
|
||||
if [ -z "$(git ls-files -u)" ]; then
|
||||
echo "$file was the only conflict, finishing the cherry-pick"
|
||||
echo "disabled ratchet file(s) were the only conflict, finishing the cherry-pick"
|
||||
if git diff --cached --quiet HEAD; then
|
||||
echo "nothing left to apply, committing the backport empty"
|
||||
git commit --allow-empty --no-edit
|
||||
|
||||
3
.github/file-paths.yaml
vendored
3
.github/file-paths.yaml
vendored
@@ -152,7 +152,7 @@ backend_sources: &backend_sources
|
||||
# Ratchet files drive project_ratchet_checks alone, so automation PRs that lower budgets
|
||||
# run the Project tests job by themselves. Patterns are OR-ed, so a separate "!..." entry would match
|
||||
# every other path instead of excluding this one.
|
||||
- ".clj-kondo/**/!(ratchets.edn)"
|
||||
- ".clj-kondo/**/!(ratchets.edn|ratchets-test.edn)"
|
||||
|
||||
backend_specs: &backend_specs
|
||||
- *shared_specs
|
||||
@@ -203,6 +203,7 @@ project_backend_checks:
|
||||
# Only the ratchet check reads the budgets, so a diff that touches nothing else runs that suite alone.
|
||||
project_ratchet_checks:
|
||||
- ".clj-kondo/ratchets.edn"
|
||||
- ".clj-kondo/ratchets-test.edn"
|
||||
- ".clj-kondo/config/modules/ratchets.edn"
|
||||
|
||||
project_migration_checks:
|
||||
|
||||
4
.github/scripts/file-paths.unit.spec.ts
vendored
4
.github/scripts/file-paths.unit.spec.ts
vendored
@@ -49,6 +49,7 @@ describe("file-paths.yaml", () => {
|
||||
"%s skips a ratchets-only change and still follows the rest of .clj-kondo",
|
||||
(name) => {
|
||||
expect(matches(name, ".clj-kondo/ratchets.edn")).toBe(false);
|
||||
expect(matches(name, ".clj-kondo/ratchets-test.edn")).toBe(false);
|
||||
expect(matches(name, ".clj-kondo/config/modules/ratchets.edn")).toBe(
|
||||
false,
|
||||
);
|
||||
@@ -94,6 +95,9 @@ describe("file-paths.yaml", () => {
|
||||
expect(matches("project_ratchet_checks", ".clj-kondo/ratchets.edn")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
matches("project_ratchet_checks", ".clj-kondo/ratchets-test.edn"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
matches(
|
||||
"project_ratchet_checks",
|
||||
|
||||
14
.github/workflows/cut-release-branch.yml
vendored
14
.github/workflows/cut-release-branch.yml
vendored
@@ -61,10 +61,11 @@ jobs:
|
||||
# to upload the markdown to #writing.
|
||||
#
|
||||
# Kondo ignore ratchets apply only to master. The cut commit replaces the
|
||||
# budgets with an explicit opt-out so future budget changes are not applied
|
||||
# to the release branch. Keeping the opt-out in the canonical file avoids
|
||||
# introducing a separate marker that could drift out of sync, and the
|
||||
# backport helper writes the same opt-out so both paths stay identical.
|
||||
# budgets in both ratchet files with an explicit opt-out so future budget
|
||||
# changes are not applied to the release branch. Keeping the opt-out in the
|
||||
# canonical files avoids introducing a separate marker that could drift out
|
||||
# of sync, and the backport helper writes the same opt-out so both paths
|
||||
# stay identical.
|
||||
- name: Cut the branch
|
||||
id: cut
|
||||
run: |
|
||||
@@ -73,8 +74,9 @@ jobs:
|
||||
git config user.email github-actions@github.com
|
||||
git checkout -b "$BRANCH"
|
||||
source .github/actions/create-backport/kondo-ratchets.sh
|
||||
write_disabled_ratchets
|
||||
git add --sparse .clj-kondo/ratchets.edn
|
||||
write_disabled_ratchets .clj-kondo/ratchets.edn
|
||||
write_disabled_ratchets .clj-kondo/ratchets-test.edn
|
||||
git add --sparse .clj-kondo/ratchets.edn .clj-kondo/ratchets-test.edn
|
||||
git commit --allow-empty --no-verify -m "Cut $BRANCH branch"
|
||||
git push -u origin "$BRANCH"
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
12
.github/workflows/kondo-ratchets-shrink.yml
vendored
12
.github/workflows/kondo-ratchets-shrink.yml
vendored
@@ -33,6 +33,7 @@ concurrency:
|
||||
env:
|
||||
AUTOMATION_BRANCH: automation/shrink-ratchets
|
||||
KONDO_RATCHETS_FILE: .clj-kondo/ratchets.edn
|
||||
TEST_KONDO_RATCHETS_FILE: .clj-kondo/ratchets-test.edn
|
||||
MODULE_RATCHETS_FILE: .clj-kondo/config/modules/ratchets.edn
|
||||
|
||||
jobs:
|
||||
@@ -61,6 +62,7 @@ jobs:
|
||||
run: |
|
||||
cp "$KONDO_RATCHETS_FILE" "$RUNNER_TEMP/kondo-ratchets-before.edn"
|
||||
cp "$MODULE_RATCHETS_FILE" "$RUNNER_TEMP/module-ratchets-before.edn"
|
||||
cp "$TEST_KONDO_RATCHETS_FILE" "$RUNNER_TEMP/test-kondo-ratchets-before.edn"
|
||||
./bin/mage kondo-ratchets-shrink
|
||||
# Two independent outputs: `push` says the branch needs a new commit, `pr` names the open automation
|
||||
# PR if there is one. Approval and auto-merge key on `pr` alone, so a run that failed after opening
|
||||
@@ -79,7 +81,7 @@ jobs:
|
||||
--jq '.[0].number // empty')
|
||||
echo "pr=$open_pr" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if git diff --quiet -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE"; then
|
||||
if git diff --quiet -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE" "$TEST_KONDO_RATCHETS_FILE"; then
|
||||
echo "Ratchet budgets already match master $(git rev-parse --short HEAD)"
|
||||
[ -z "$open_pr" ] || echo "The open automation PR #$open_pr is re-approved below"
|
||||
echo "push=false" >> "$GITHUB_OUTPUT"
|
||||
@@ -95,7 +97,7 @@ jobs:
|
||||
# lazily, so a pending UNKNOWN reads as not conflicted and the next run settles it.
|
||||
if [ -n "$open_pr" ] \
|
||||
&& git fetch --quiet --depth=1 origin "refs/heads/$AUTOMATION_BRANCH" 2>/dev/null \
|
||||
&& git diff --quiet FETCH_HEAD -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE" \
|
||||
&& git diff --quiet FETCH_HEAD -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE" "$TEST_KONDO_RATCHETS_FILE" \
|
||||
&& [ "$(gh pr view "$open_pr" --repo "$GITHUB_REPOSITORY" \
|
||||
--json mergeable --jq .mergeable)" != CONFLICTING ]; then
|
||||
echo "The automation PR #$open_pr already carries this result"
|
||||
@@ -104,7 +106,7 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git diff --stat -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE"
|
||||
git diff --stat -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE" "$TEST_KONDO_RATCHETS_FILE"
|
||||
echo "push=true" >> "$GITHUB_OUTPUT"
|
||||
- name: Update the automation PR
|
||||
id: deliver
|
||||
@@ -122,12 +124,14 @@ jobs:
|
||||
"$KONDO_RATCHETS_FILE" \
|
||||
"$RUNNER_TEMP/module-ratchets-before.edn" \
|
||||
"$MODULE_RATCHETS_FILE" \
|
||||
"$RUNNER_TEMP/test-kondo-ratchets-before.edn" \
|
||||
"$TEST_KONDO_RATCHETS_FILE" \
|
||||
"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
> "$RUNNER_TEMP/ratchets-pr-body.md"
|
||||
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
git add -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE"
|
||||
git add -- "$KONDO_RATCHETS_FILE" "$MODULE_RATCHETS_FILE" "$TEST_KONDO_RATCHETS_FILE"
|
||||
git commit --quiet -m "Tighten ratchets"
|
||||
git push --force --quiet \
|
||||
"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -159,6 +159,7 @@ mise.local.toml
|
||||
!**/.clj-kondo/README.md
|
||||
!**/.clj-kondo/config.edn
|
||||
!**/.clj-kondo/ratchets.edn
|
||||
!**/.clj-kondo/ratchets-test.edn
|
||||
!**/.clj-kondo/config/
|
||||
!**/.clj-kondo/src/
|
||||
!**/.clj-kondo/test/
|
||||
|
||||
@@ -104,9 +104,16 @@ interactive explorer with a dependency graph.
|
||||
After changing Clojure suppressions or module-boundary escape hatches, run `./bin/mage kondo-ratchets`.
|
||||
Fix the underlying issue when possible; suppressions are a last resort and need a nearby explanation.
|
||||
|
||||
Inline-ignore budgets (`:ignore-counts`) are split by prod/test so one side's suppressions can never mask
|
||||
or look equivalent to the other's: `.clj-kondo/ratchets.edn` covers production code, and
|
||||
`.clj-kondo/ratchets-test.edn` covers anything under a `test` directory. `:config-counts`
|
||||
(`.clj-kondo/config.edn` suppressions) stays combined in `.clj-kondo/ratchets.edn` regardless -- see that
|
||||
file's own header for why. `.clj-kondo/config/modules/ratchets.edn` (module-boundary escape hatches) is a
|
||||
separate, unsplit concern.
|
||||
|
||||
Explain budget increases in the PR. Do not record reductions in feature PRs: post-merge automation opens
|
||||
a `Tighten ratchets` PR for them. Use `./bin/mage kondo-ratchets-shrink --seed :linter` only when adding an
|
||||
inline-ignore budget. If either ratchet file conflicts, run `./bin/merge-kondo-ratchets`.
|
||||
inline-ignore budget. If any ratchet file conflicts, run `./bin/merge-kondo-ratchets`.
|
||||
|
||||
## Tool Preferences
|
||||
|
||||
|
||||
31
bb.edn
31
bb.edn
@@ -433,7 +433,7 @@
|
||||
-ratchets-merge
|
||||
{:doc "Internal three-way merge for ratchet files. Called by bin/merge-kondo-ratchets."
|
||||
:arg-schema [:tuple
|
||||
[:enum {:name "kind"} "kondo" "modules"]
|
||||
[:enum {:name "kind"} "kondo" "test" "modules"]
|
||||
[:string {:name "base" :description "Ratchets file from the merge base"}]
|
||||
[:string {:name "ours" :description "Git stage 2: ratchets file from the branch receiving the changes"}]
|
||||
[:string {:name "theirs" :description "Git stage 3: ratchets file from the commits being applied"}]
|
||||
@@ -453,10 +453,11 @@
|
||||
(spit output-path
|
||||
(dev.kondo-ratchet/render-module-ratchets
|
||||
(dev.kondo-ratchet/merge-module-ratchets base ours theirs)))
|
||||
(let [merged (dev.kondo-ratchet/merge-ratchets base ours theirs)]
|
||||
(let [merged (dev.kondo-ratchet/merge-ratchets base ours theirs)
|
||||
render (if (= kind "test") dev.kondo-ratchet/render-test dev.kondo-ratchet/render)]
|
||||
(spit output-path (if (dev.kondo-ratchet/disabled? merged)
|
||||
(slurp ours-path)
|
||||
(dev.kondo-ratchet/render merged)))))
|
||||
(render merged)))))
|
||||
(println (str "merged ratchets into " output-path))))}
|
||||
|
||||
-kondo-ratchets-pr-body
|
||||
@@ -466,21 +467,21 @@
|
||||
[:string {:name "after" :description "Ratchets file after shrinking"}]
|
||||
[:string {:name "modules-before" :description "Module ratchets before shrinking"}]
|
||||
[:string {:name "modules-after" :description "Module ratchets after shrinking"}]
|
||||
[:string {:name "test-before" :description "Test ratchets file before shrinking"}]
|
||||
[:string {:name "test-after" :description "Test ratchets file after shrinking"}]
|
||||
[:string {:name "workflow-url" :description "URL of the workflow run"}]]
|
||||
:requires [[dev.kondo-ratchet]]
|
||||
:task (task!
|
||||
(let [[before-path after-path modules-before-path modules-after-path workflow-url] (:arguments parsed)
|
||||
read-ratchets (fn [path]
|
||||
(binding [dev.kondo-ratchet/*ratchets-file* path]
|
||||
(dev.kondo-ratchet/read-ratchets)))
|
||||
read-modules (fn [path]
|
||||
(binding [dev.kondo-ratchet/*module-ratchets-file* path]
|
||||
(dev.kondo-ratchet/read-module-ratchets)))]
|
||||
(print (dev.kondo-ratchet/shrink-pr-body (assoc (read-ratchets before-path)
|
||||
:module-counts (read-modules modules-before-path))
|
||||
(assoc (read-ratchets after-path)
|
||||
:module-counts (read-modules modules-after-path))
|
||||
workflow-url))))}
|
||||
(let [[before after modules-before modules-after test-before test-after workflow-url]
|
||||
(:arguments parsed)]
|
||||
(print (dev.kondo-ratchet/shrink-pr-body-from-files
|
||||
{:before before
|
||||
:after after
|
||||
:modules-before modules-before
|
||||
:modules-after modules-after
|
||||
:test-before test-before
|
||||
:test-after test-after}
|
||||
workflow-url))))}
|
||||
|
||||
-driver-decisions
|
||||
{:doc "Determine which driver tests should run based on PR context. Outputs decisions for all drivers."
|
||||
|
||||
@@ -11,6 +11,7 @@ cd -- "$(git rev-parse --show-toplevel)"
|
||||
|
||||
ratchet_files=(
|
||||
.clj-kondo/ratchets.edn
|
||||
.clj-kondo/ratchets-test.edn
|
||||
.clj-kondo/config/modules/ratchets.edn
|
||||
)
|
||||
conflicted_files=()
|
||||
@@ -28,7 +29,7 @@ for ratchets_file in "${ratchet_files[@]}"; do
|
||||
done
|
||||
|
||||
if [ "${#conflicted_files[@]}" -eq 0 ]; then
|
||||
echo "neither ratchet file is conflicted" >&2
|
||||
echo "no ratchet file is conflicted" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -47,11 +48,11 @@ for i in "${!conflicted_files[@]}"; do
|
||||
printf '{}\n' > "$tmp_dir/base-$i"
|
||||
fi
|
||||
|
||||
if [ "$ratchets_file" = .clj-kondo/ratchets.edn ]; then
|
||||
kind=kondo
|
||||
else
|
||||
kind=modules
|
||||
fi
|
||||
case $ratchets_file in
|
||||
.clj-kondo/config/modules/ratchets.edn) kind=modules ;;
|
||||
.clj-kondo/ratchets-test.edn) kind=test ;;
|
||||
*) kind=kondo ;;
|
||||
esac
|
||||
"$bin_dir/mage" -ratchets-merge "$kind" \
|
||||
"$tmp_dir/base-$i" \
|
||||
"$tmp_dir/ours-$i" \
|
||||
|
||||
@@ -15,9 +15,17 @@
|
||||
|
||||
#_{:clj-kondo/ignore [:metabase/discourage-dynamic-vars]}
|
||||
(def ^:dynamic *ratchets-file*
|
||||
"Kondo budgets, relative to the repo root. Rebind to read a merge stage or test fixture."
|
||||
"Kondo budgets for production code, relative to the repo root. Rebind to read a merge stage or test
|
||||
fixture."
|
||||
".clj-kondo/ratchets.edn")
|
||||
|
||||
#_{:clj-kondo/ignore [:metabase/discourage-dynamic-vars]}
|
||||
(def ^:dynamic *test-ratchets-file*
|
||||
"Kondo budgets for test code, relative to the repo root. Separate from [[*ratchets-file*]] so test-code
|
||||
ignores can't mask a prod-code count; only :ignore-counts is tracked here (see [[config-suppressions]]
|
||||
for why). Rebind to read a merge stage or test fixture."
|
||||
".clj-kondo/ratchets-test.edn")
|
||||
|
||||
#_{:clj-kondo/ignore [:metabase/discourage-dynamic-vars]}
|
||||
(def ^:dynamic *module-ratchets-file*
|
||||
"Module-boundary budgets, relative to the repo root. Rebind to read a merge stage or test fixture."
|
||||
@@ -262,7 +270,8 @@
|
||||
(str/join ", " (sort-by str external-linters))))
|
||||
|
||||
(defn validate-linters!
|
||||
"Throw one error naming every policy key in `ratchets` that is not a known linter."
|
||||
"Throw one error naming every policy key in `ratchets` that is not a known linter. Names
|
||||
[[*ratchets-file*]] in the message, so call this with it bound to whichever file `ratchets` came from."
|
||||
[ratchets known]
|
||||
(let [unknown (unknown-linters ratchets known)]
|
||||
(when (seq unknown)
|
||||
@@ -274,6 +283,28 @@
|
||||
(known-linter-hint))
|
||||
{:unknown (vec unknown)})))))
|
||||
|
||||
(defn- validate-test-config-counts!
|
||||
"Throw unless `test-ratchets` has an empty :config-counts -- config-level suppressions are tracked only
|
||||
in `prod-file` (see [[config-suppressions]]), and [[fix!]] silently drops anything here otherwise.
|
||||
Call with [[*ratchets-file*]] bound to [[*test-ratchets-file*]], so the message names the right file."
|
||||
[test-ratchets prod-file]
|
||||
(when (seq (:config-counts test-ratchets))
|
||||
(throw (ex-info (format "%s must not set :config-counts -- config-level suppressions are tracked only in %s"
|
||||
*ratchets-file* prod-file)
|
||||
{:file *ratchets-file*}))))
|
||||
|
||||
(defn- read-validated-test-ratchets
|
||||
"[[read-ratchets]] for [[*test-ratchets-file*]], validated against `known` linters and for a nonempty
|
||||
:config-counts -- both skipped when the file is `{:disabled true}`. `prod-file` should be
|
||||
[[*ratchets-file*]]'s value from before the call, so errors name the right file."
|
||||
[known prod-file]
|
||||
(binding [*ratchets-file* *test-ratchets-file*]
|
||||
(let [test-ratchets (read-ratchets)]
|
||||
(when-not (disabled? test-ratchets)
|
||||
(validate-linters! test-ratchets known)
|
||||
(validate-test-config-counts! test-ratchets prod-file))
|
||||
test-ratchets)))
|
||||
|
||||
(defn validate-seed!
|
||||
"Throw when a linter in `seeded` is not known, so `--seed` never writes a policy the check rejects."
|
||||
[seeded known]
|
||||
@@ -484,6 +515,13 @@
|
||||
:linters (:linters m)
|
||||
:justified? (:justified? m)})))
|
||||
|
||||
(defn test-occurrence?
|
||||
"Does `occurrence` (as returned by [[scan]]) sit under a directory literally named `test`
|
||||
(`test/`, `enterprise/backend/test/`, `modules/drivers/*/test/`, ...)? Decides whether its budget
|
||||
belongs to [[*ratchets-file*]] or [[*test-ratchets-file*]]."
|
||||
[{:keys [file]}]
|
||||
(boolean (some #{"test"} (str/split file #"/"))))
|
||||
|
||||
(defn actual-counts
|
||||
"Per-linter occurrence counts for `occurrences`, as returned by [[scan]]."
|
||||
[occurrences]
|
||||
@@ -566,7 +604,14 @@
|
||||
"Per-linter counts of config-level suppressions in `config` (default: `.clj-kondo/config.edn`):
|
||||
top-level `:linters` entries, `:config-in-comment`, and every `:config-in-ns` / `:config-in-call`
|
||||
group. Scoped groups count too, or a linter could be switched off inside one and never show up.
|
||||
Entries that add discouragements or turn linters on count nothing — only weakening counts."
|
||||
Entries that add discouragements or turn linters on count nothing — only weakening counts.
|
||||
|
||||
Tracked only in [[*ratchets-file*]], not split per test/prod like :ignore-counts is: `config.edn`'s
|
||||
`test-namespaces`/`source-namespaces` groups separate cleanly, but others (e.g. `driver-namespaces`)
|
||||
cover both, so there's no reliable per-suppression attribution without restructuring `config.edn`.
|
||||
Doing that later would mean splitting those mixed groups apart, changing this function to track which
|
||||
group an entry came from instead of summing by linter name, and top-level `:linters`/`:config-in-comment`
|
||||
entries would still have no test/prod home -- real effort for a softer guarantee than :ignore-counts."
|
||||
([]
|
||||
(config-suppressions (edn/read-string (slurp kondo-config-file))))
|
||||
([config]
|
||||
@@ -601,10 +646,8 @@
|
||||
:when (> actual recorded)]
|
||||
[linter entry])))
|
||||
|
||||
(def ^:private header
|
||||
(str ";; Budgets for kondo suppressions: inline `" ignore-marker "` forms per linter (:ignore-counts), and\n"
|
||||
";; config-level waivers in .clj-kondo/config.edn (:config-counts -- :off switches and :exclude entries).\n"
|
||||
";; Each :ignore-counts value is a non-negative integer budget, or :unlimited for no ceiling.\n"
|
||||
(def ^:private shared-header
|
||||
(str ";; Each :ignore-counts value is a non-negative integer budget, or :unlimited for no ceiling.\n"
|
||||
";; Checks fail when a count exceeds its numeric budget; unused budget is allowed.\n"
|
||||
";; Any ignore outside :comment-exempt needs an explanatory comment directly above or trailing on its line.\n"
|
||||
";; `./bin/mage kondo-ratchets-shrink` lowers budgets unless `--seed` explicitly adds or raises one.\n"
|
||||
@@ -613,6 +656,20 @@
|
||||
";; exemptions must be explained in the PR.\n"
|
||||
";; :all is the vector-less ignore form, which suppresses every linter on the next form.\n"))
|
||||
|
||||
(def ^:private header
|
||||
(str ";; Budgets for kondo suppressions: inline `" ignore-marker "` forms per linter (:ignore-counts), and\n"
|
||||
";; config-level waivers in .clj-kondo/config.edn (:config-counts -- :off switches and :exclude entries).\n"
|
||||
shared-header
|
||||
";;\n"
|
||||
";; Test code's :ignore-counts has its own budget in .clj-kondo/ratchets-test.edn.\n"
|
||||
";; :config-counts stays here either way -- config.edn's scoping doesn't cleanly split.\n"))
|
||||
|
||||
(def ^:private test-header
|
||||
(str ";; Budgets for kondo suppressions in test code: inline `" ignore-marker "` forms per linter\n"
|
||||
";; (:ignore-counts). Production code has its own budgets in .clj-kondo/ratchets.edn, which also holds\n"
|
||||
";; every config-level waiver (:config-counts) -- config.edn's scoping doesn't cleanly split.\n"
|
||||
shared-header))
|
||||
|
||||
(def ^:private module-header
|
||||
(str ";; Budgets for escape hatches in config.edn. Counts may fall but not exceed these values.\n"
|
||||
";; Leave reductions to the post-merge shrink workflow; explain increases in the PR.\n"))
|
||||
@@ -629,14 +686,14 @@
|
||||
(format (str "%-" width "s %s") (str linter) (str n))))
|
||||
"}"))))
|
||||
|
||||
(defn render
|
||||
"Canonical Kondo ratchet-file text."
|
||||
[{:keys [ignore-counts config-counts comment-exempt]}]
|
||||
(defn- render-policies
|
||||
[header {:keys [ignore-counts config-counts comment-exempt]} config-counts?]
|
||||
(let [counts-indent (apply str (repeat (count "{:ignore-counts {") \space))
|
||||
exempt-indent (apply str (repeat (count " :comment-exempt #{") \space))]
|
||||
(str header
|
||||
"{:ignore-counts " (render-counts ignore-counts counts-indent)
|
||||
"\n :config-counts " (render-counts config-counts counts-indent)
|
||||
(when config-counts?
|
||||
(str "\n :config-counts " (render-counts config-counts counts-indent)))
|
||||
"\n :comment-exempt "
|
||||
(if (empty? comment-exempt)
|
||||
"#{}"
|
||||
@@ -646,6 +703,21 @@
|
||||
"}"))
|
||||
"}\n")))
|
||||
|
||||
(defn render
|
||||
"Canonical text for the prod ratchets file, [[*ratchets-file*]]."
|
||||
[ratchets]
|
||||
(render-policies header ratchets true))
|
||||
|
||||
(defn render-test
|
||||
"Canonical text for the test ratchets file, [[*test-ratchets-file*]], which has no :config-counts.
|
||||
Throws on a nonempty :config-counts rather than dropping it."
|
||||
[{:keys [config-counts] :as ratchets}]
|
||||
(when (seq config-counts)
|
||||
(throw (ex-info (str *test-ratchets-file* " must not set :config-counts -- config-level suppressions are"
|
||||
" tracked only in " *ratchets-file*)
|
||||
{:config-counts config-counts})))
|
||||
(render-policies test-header ratchets false))
|
||||
|
||||
(defn render-module-ratchets
|
||||
"Canonical module ratchet-file text."
|
||||
[ratchets]
|
||||
@@ -782,11 +854,13 @@
|
||||
(str "WARNING: :comment-exempt is no longer needed for these linters: " (str/join ", " linters)
|
||||
" -- delete the stale entries by hand"))))
|
||||
|
||||
(defn change-report
|
||||
"The lines [[fix!]] prints for budget changes, budget violations, unused `:unlimited` policies, and
|
||||
stale comment exemptions."
|
||||
[{:keys [ignore-counts config-counts comment-exempt]} module-ratchets
|
||||
occurrences config-actual module-actual seeded]
|
||||
(defn- ignore-change-report
|
||||
"Change-report lines for one file's :ignore-counts, scoped to `occurrences`: seeding, lowered/dropped/
|
||||
over-budget counts, unused `:unlimited` policies, and ignores with no budget entry. Its :comment-exempt
|
||||
is reported separately, via [[stale-exemptions-warning]], so callers can place it relative to other
|
||||
sections (e.g. after config/module counts, as [[change-report]] does).
|
||||
Shared by [[change-report]] (the prod ratchets file) and [[fix!]]'s pass over the test ratchets file."
|
||||
[ignore-counts occurrences seeded]
|
||||
(let [actual (actual-counts occurrences)]
|
||||
(concat
|
||||
(for [linter seeded
|
||||
@@ -809,30 +883,39 @@
|
||||
(some-> (unexercised-unlimited-warning (apply dissoc ignore-counts seeded) actual) vector)
|
||||
(for [[linter n] (sort-by (comp str first) (apply dissoc actual (concat seeded (keys ignore-counts))))]
|
||||
(format "WARNING: %s has %d ignores but no budget entry -- seed one with `./bin/mage kondo-ratchets-shrink --seed %s`"
|
||||
linter n linter))
|
||||
(for [[linter {:keys [recorded actual]}] (count-drift config-counts config-actual)]
|
||||
(cond
|
||||
(zero? actual) (format "dropped config %s (no suppressions left)" linter)
|
||||
(< actual recorded) (format "lowered config %s %d -> %d" linter recorded actual)
|
||||
:else (format "WARNING: config suppressions for %s are over budget (%d recorded, %d actual) -- remove one from .clj-kondo/config.edn or raise the budget by hand"
|
||||
linter recorded actual)))
|
||||
(for [[metric {:keys [recorded actual]}] (count-drift module-ratchets module-actual)]
|
||||
(cond
|
||||
(zero? actual) (format "dropped module %s (no escape hatches left)" metric)
|
||||
(< actual recorded) (format "lowered module %s %d -> %d" metric recorded actual)
|
||||
:else (format (str "WARNING: module %s is over budget (%d recorded, %d actual)"
|
||||
" -- remove one from " module-config-file " or raise the budget by hand")
|
||||
metric recorded actual)))
|
||||
(some-> (stale-exemptions-warning comment-exempt occurrences) vector))))
|
||||
linter n linter)))))
|
||||
|
||||
(defn change-report
|
||||
"The lines [[fix!]] prints for budget changes, budget violations, unused `:unlimited` policies, and
|
||||
stale comment exemptions."
|
||||
[{:keys [ignore-counts config-counts comment-exempt]} module-ratchets
|
||||
occurrences config-actual module-actual seeded]
|
||||
(concat
|
||||
(ignore-change-report ignore-counts occurrences seeded)
|
||||
(for [[linter {:keys [recorded actual]}] (count-drift config-counts config-actual)]
|
||||
(cond
|
||||
(zero? actual) (format "dropped config %s (no suppressions left)" linter)
|
||||
(< actual recorded) (format "lowered config %s %d -> %d" linter recorded actual)
|
||||
:else (format "WARNING: config suppressions for %s are over budget (%d recorded, %d actual) -- remove one from .clj-kondo/config.edn or raise the budget by hand"
|
||||
linter recorded actual)))
|
||||
(for [[metric {:keys [recorded actual]}] (count-drift module-ratchets module-actual)]
|
||||
(cond
|
||||
(zero? actual) (format "dropped module %s (no escape hatches left)" metric)
|
||||
(< actual recorded) (format "lowered module %s %d -> %d" metric recorded actual)
|
||||
:else (format (str "WARNING: module %s is over budget (%d recorded, %d actual)"
|
||||
" -- remove one from " module-config-file " or raise the budget by hand")
|
||||
metric recorded actual)))
|
||||
(some-> (stale-exemptions-warning comment-exempt occurrences) vector)))
|
||||
|
||||
(def ^:private count-fields
|
||||
[:ignore-counts :config-counts :module-counts])
|
||||
[:ignore-counts :config-counts :module-counts :test-ignore-counts])
|
||||
|
||||
(defn- display-name
|
||||
[field linter]
|
||||
(case field
|
||||
:config-counts (str ":config/" (subs (str linter) 1))
|
||||
:module-counts (str ":module/" (subs (str linter) 1))
|
||||
:config-counts (str ":config/" (subs (str linter) 1))
|
||||
:module-counts (str ":module/" (subs (str linter) 1))
|
||||
:test-ignore-counts (str ":test/" (subs (str linter) 1))
|
||||
(str linter)))
|
||||
|
||||
(defn- shrink-changes
|
||||
@@ -898,12 +981,31 @@
|
||||
"Generated by [`./bin/mage kondo-ratchets-shrink`](" workflow-url
|
||||
")\n"))
|
||||
|
||||
(defn- read-ratchet-snapshots
|
||||
"The budgets in a prod, module, and test ratchet file triple, combined into the shape
|
||||
[[shrink-summary]] compares."
|
||||
[ratchets-path module-ratchets-path test-ratchets-path]
|
||||
(let [read-ratchets* #(binding [*ratchets-file* %] (read-ratchets))]
|
||||
(assoc (read-ratchets* ratchets-path)
|
||||
:module-counts (binding [*module-ratchets-file* module-ratchets-path]
|
||||
(read-module-ratchets))
|
||||
:test-ignore-counts (:ignore-counts (read-ratchets* test-ratchets-path)))))
|
||||
|
||||
(defn shrink-pr-body-from-files
|
||||
"[[shrink-pr-body]] for ratchet files on disk, before and after shrinking."
|
||||
[{:keys [before after modules-before modules-after test-before test-after]} workflow-url]
|
||||
(shrink-pr-body (read-ratchet-snapshots before modules-before test-before)
|
||||
(read-ratchet-snapshots after modules-after test-after)
|
||||
workflow-url))
|
||||
|
||||
(defn fix!
|
||||
"Lower budgets and normalize both ratchet files.
|
||||
"Lower budgets and normalize the prod, test, and module ratchet files.
|
||||
Refuses to touch a file containing an unknown linter or to seed one, rather than silently dropping it.
|
||||
`--seed LINTER` (`{:seed \"...\"}` here) sets that budget to the actual count and bounds an unlimited one.
|
||||
Prints the [[change-report]], or `unchanged` on a no-op.
|
||||
Does nothing, including seeding, when the file sets `:disabled` to `true`."
|
||||
`--seed LINTER` (`{:seed \"...\"}` here) sets that budget to the actual count and bounds an unlimited
|
||||
one, independently in whichever file(s) actually have occurrences for it.
|
||||
Prints a [[change-report]] per file, or `unchanged` on a no-op.
|
||||
Does nothing, including seeding, when [[*ratchets-file*]] sets `:disabled` to `true`; the test ratchets
|
||||
file honors its own `:disabled` independently, skipping only its own pass."
|
||||
([]
|
||||
(fix! nil))
|
||||
([{:keys [seed]}]
|
||||
@@ -913,35 +1015,48 @@
|
||||
(let [module-ratchets (read-module-ratchets)
|
||||
known (known-linters)
|
||||
_ (validate-linters! ratchets known)
|
||||
test-ratchets (read-validated-test-ratchets known *ratchets-file*)
|
||||
test-disabled? (disabled? test-ratchets)
|
||||
seeded (if seed [(keyword (str/replace-first seed #"^:" ""))] [])
|
||||
_ (validate-seed! seeded known)
|
||||
occurrences (scan)
|
||||
actual (actual-counts occurrences)
|
||||
{test-occ true, prod-occ false} (group-by test-occurrence? occurrences)
|
||||
actual (actual-counts prod-occ)
|
||||
test-actual (actual-counts test-occ)
|
||||
config-actual (config-suppressions)
|
||||
module-actual (module-escape-hatches)
|
||||
outputs [[*ratchets-file*
|
||||
(render {:ignore-counts (lowered-counts ignore-counts actual seeded)
|
||||
:config-counts (lowered-counts config-counts config-actual [])
|
||||
:comment-exempt comment-exempt})]
|
||||
[*module-ratchets-file*
|
||||
(render-module-ratchets
|
||||
(lowered-counts module-ratchets module-actual []))]]
|
||||
outputs (cond-> [[*ratchets-file*
|
||||
(render {:ignore-counts (lowered-counts ignore-counts actual seeded)
|
||||
:config-counts (lowered-counts config-counts config-actual [])
|
||||
:comment-exempt comment-exempt})]
|
||||
[*module-ratchets-file*
|
||||
(render-module-ratchets
|
||||
(lowered-counts module-ratchets module-actual []))]]
|
||||
(not test-disabled?)
|
||||
(conj [*test-ratchets-file*
|
||||
(render-test {:ignore-counts (lowered-counts (:ignore-counts test-ratchets)
|
||||
test-actual seeded)
|
||||
:comment-exempt (:comment-exempt test-ratchets)})]))
|
||||
changed (filterv (fn [[path text]] (not= (slurp path) text)) outputs)]
|
||||
(run! println (change-report ratchets module-ratchets occurrences config-actual module-actual seeded))
|
||||
(run! println (change-report ratchets module-ratchets prod-occ config-actual module-actual seeded))
|
||||
(when-not test-disabled?
|
||||
(run! println (concat (ignore-change-report (:ignore-counts test-ratchets) test-occ seeded)
|
||||
(some-> (stale-exemptions-warning (:comment-exempt test-ratchets) test-occ)
|
||||
vector))))
|
||||
(if (empty? changed)
|
||||
(println "unchanged")
|
||||
(doseq [[path text] changed]
|
||||
(spit path text)
|
||||
(println (str "wrote " path)))))))))
|
||||
|
||||
(defn check-report
|
||||
"Diagnostics for budget violations, unjustified ignores, and noncanonical ratchet text.
|
||||
Lower counts are valid; post-merge automation records them."
|
||||
[{:keys [ignore-counts config-counts comment-exempt] :or {comment-exempt #{}} :as ratchets}
|
||||
module-ratchets occurrences config-actual module-actual ratchets-text module-ratchets-text]
|
||||
(defn- ignore-check-lines
|
||||
"Budget, justification, and formatting diagnostics for one ratchets file's :ignore-counts and
|
||||
:comment-exempt, scoped to `occurrences`. `ratchets-file` names the file in the not-normalized message, and
|
||||
`render-fn` gives its canonical text.
|
||||
Shared by [[check-report]] (the prod ratchets file) and [[check]]'s pass over the test ratchets file."
|
||||
[ratchets-file render-fn {:keys [ignore-counts comment-exempt] :or {comment-exempt #{}} :as ratchets}
|
||||
occurrences ratchets-text]
|
||||
(let [over (over-budget ignore-counts occurrences)
|
||||
config-over (counts-over-budget config-counts config-actual)
|
||||
module-over (counts-over-budget module-ratchets module-actual)
|
||||
uncommented (unjustified comment-exempt occurrences)
|
||||
budget-line (fn [[k {:keys [recorded actual]}]]
|
||||
(format " %s: %d recorded, %d actual" k recorded actual))]
|
||||
@@ -952,6 +1067,27 @@
|
||||
(mapcat (fn [[_ {:keys [examples]} :as entry]]
|
||||
(cons (budget-line entry) (map #(str " " %) examples)))
|
||||
over)))
|
||||
(when (seq uncommented)
|
||||
(cons (str "ignores without required comments -- add a `;;` comment above the form or at the end"
|
||||
" of the same line, explaining why the suppression is necessary:")
|
||||
(map (fn [{:keys [file line linters]}]
|
||||
(format " %s:%d %s" file line (vec linters)))
|
||||
uncommented)))
|
||||
(when (not= ratchets-text (render-fn ratchets))
|
||||
[(str ratchets-file " is not normalized -- run `./bin/mage kondo-ratchets-shrink`"
|
||||
" to fix the formatting")]))))
|
||||
|
||||
(defn check-report
|
||||
"Diagnostics for budget violations, unjustified ignores, and noncanonical ratchet text.
|
||||
Lower counts are valid; post-merge automation records them."
|
||||
[{:keys [config-counts] :as ratchets}
|
||||
module-ratchets occurrences config-actual module-actual ratchets-text module-ratchets-text]
|
||||
(let [config-over (counts-over-budget config-counts config-actual)
|
||||
module-over (counts-over-budget module-ratchets module-actual)
|
||||
budget-line (fn [[k {:keys [recorded actual]}]]
|
||||
(format " %s: %d recorded, %d actual" k recorded actual))]
|
||||
(concat
|
||||
(ignore-check-lines *ratchets-file* render ratchets occurrences ratchets-text)
|
||||
(when (seq config-over)
|
||||
(cons (str "config suppressions over budget -- remove the entry from " kondo-config-file
|
||||
", or raise the budget manually and explain the increase in the PR:")
|
||||
@@ -960,15 +1096,6 @@
|
||||
(cons (str "module escape hatches over budget -- remove one from " module-config-file
|
||||
", or raise the budget manually and explain the increase in the PR:")
|
||||
(map budget-line module-over)))
|
||||
(when (seq uncommented)
|
||||
(cons (str "ignores without required comments -- add a `;;` comment above the form or at the end"
|
||||
" of the same line, explaining why the suppression is necessary:")
|
||||
(map (fn [{:keys [file line linters]}]
|
||||
(format " %s:%d %s" file line (vec linters)))
|
||||
uncommented)))
|
||||
(when (not= ratchets-text (render ratchets))
|
||||
[(str *ratchets-file* " is not normalized -- run `./bin/mage kondo-ratchets-shrink`"
|
||||
" to fix the formatting")])
|
||||
(when (not= module-ratchets-text (render-module-ratchets module-ratchets))
|
||||
[(str *module-ratchets-file* " is not normalized -- run `./bin/mage kondo-ratchets-shrink`"
|
||||
" to fix the formatting")]))))
|
||||
@@ -985,27 +1112,42 @@
|
||||
(exit! message))
|
||||
|
||||
(defn check
|
||||
"Validate the ratchet files and current counts. Fail on missing or malformed configuration, unknown
|
||||
linters, budget violations, or unjustified ignores. Report stale unlimited policies and exemptions
|
||||
without failing. Only `{:disabled true}` disables enforcement."
|
||||
"Validate the prod, test, and module ratchet files and current counts. Fail on missing or malformed
|
||||
configuration, unknown linters, budget violations, or unjustified ignores. Report stale unlimited
|
||||
policies and exemptions without failing. `{:disabled true}` in [[*ratchets-file*]] disables checking
|
||||
entirely; the test ratchets file honors its own `:disabled` independently, skipping only its own checks."
|
||||
[]
|
||||
;; Turn validation errors into concise Mage output instead of a Babashka stack trace.
|
||||
(try
|
||||
(let [ratchets (read-ratchets)]
|
||||
(if (disabled? ratchets)
|
||||
(println (str *ratchets-file* " is disabled -- nothing to check"))
|
||||
(do
|
||||
(validate-linters! ratchets (known-linters))
|
||||
(let [known (known-linters)]
|
||||
(validate-linters! ratchets known)
|
||||
(let [module-ratchets (read-module-ratchets)
|
||||
test-ratchets (read-validated-test-ratchets known *ratchets-file*)
|
||||
test-disabled? (disabled? test-ratchets)
|
||||
occurrences (scan)
|
||||
lines (check-report ratchets module-ratchets occurrences
|
||||
(config-suppressions) (module-escape-hatches)
|
||||
(slurp *ratchets-file*) (slurp *module-ratchets-file*))]
|
||||
(some-> (unexercised-unlimited-warning (:ignore-counts ratchets) (actual-counts occurrences)) println)
|
||||
(some-> (stale-exemptions-warning (:comment-exempt ratchets) occurrences) println)
|
||||
{test-occ true, prod-occ false} (group-by test-occurrence? occurrences)
|
||||
lines (concat
|
||||
(check-report ratchets module-ratchets prod-occ
|
||||
(config-suppressions) (module-escape-hatches)
|
||||
(slurp *ratchets-file*) (slurp *module-ratchets-file*))
|
||||
(when-not test-disabled?
|
||||
(ignore-check-lines *test-ratchets-file* render-test test-ratchets test-occ
|
||||
(slurp *test-ratchets-file*))))]
|
||||
(some-> (unexercised-unlimited-warning (:ignore-counts ratchets) (actual-counts prod-occ)) println)
|
||||
(some-> (stale-exemptions-warning (:comment-exempt ratchets) prod-occ) println)
|
||||
(when-not test-disabled?
|
||||
(some-> (unexercised-unlimited-warning (:ignore-counts test-ratchets) (actual-counts test-occ)) println)
|
||||
(some-> (stale-exemptions-warning (:comment-exempt test-ratchets) test-occ) println))
|
||||
(if (empty? lines)
|
||||
(println (format "ok -- %d ignore forms within %d policies"
|
||||
(count occurrences) (count (:ignore-counts ratchets))))
|
||||
(do
|
||||
(println (format "ok -- %d ignore forms within %d policies"
|
||||
(count prod-occ) (count (:ignore-counts ratchets))))
|
||||
(when-not test-disabled?
|
||||
(println (format "ok -- %d test ignore forms within %d test policies"
|
||||
(count test-occ) (count (:ignore-counts test-ratchets))))))
|
||||
(do (run! println lines)
|
||||
(exit! "ratchet files drifted from the source tree")))))))
|
||||
(catch clojure.lang.ExceptionInfo e
|
||||
|
||||
@@ -90,20 +90,29 @@
|
||||
|
||||
(defn- check-with!
|
||||
"Output lines of [[kondo-ratchet/check]] against `ratchets` written to a temp file, with `occurrences`
|
||||
standing in for the tree scan; `:thrown?` says whether it failed."
|
||||
standing in for the tree scan; `:thrown?` says whether it failed. `:test-counts` stands in for the test
|
||||
ratchets file, defaulting to an empty (clean) budget; `{:disabled true}` there is honored."
|
||||
[ratchets occurrences]
|
||||
(let [dir (.toFile (java.nio.file.Files/createTempDirectory
|
||||
"kondo-ratchet-check-test"
|
||||
(make-array java.nio.file.attribute.FileAttribute 0)))
|
||||
budgets (doto (io/file dir "ratchets.edn")
|
||||
(spit (kondo-ratchet/render (merge {:config-counts {}, :comment-exempt #{}}
|
||||
(dissoc ratchets :module-counts)))))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets (:module-counts ratchets {}))))
|
||||
thrown? (atom false)]
|
||||
(let [dir (.toFile (java.nio.file.Files/createTempDirectory
|
||||
"kondo-ratchet-check-test"
|
||||
(make-array java.nio.file.attribute.FileAttribute 0)))
|
||||
test-ratchets (:test-counts ratchets {})
|
||||
budgets (doto (io/file dir "ratchets.edn")
|
||||
(spit (kondo-ratchet/render (merge {:config-counts {}, :comment-exempt #{}}
|
||||
(dissoc ratchets :module-counts :test-counts)))))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets (:module-counts ratchets {}))))
|
||||
test-budgets (doto (io/file dir "ratchets-test.edn")
|
||||
(spit (if (:disabled test-ratchets)
|
||||
"{:disabled true}\n"
|
||||
(kondo-ratchet/render-test (merge {:ignore-counts {}, :comment-exempt #{}}
|
||||
test-ratchets)))))
|
||||
thrown? (atom false)]
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath budgets)
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly (set (keys (:ignore-counts ratchets))))
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)
|
||||
kondo-ratchet/*test-ratchets-file* (.getPath test-budgets)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly (into (set (keys (:ignore-counts ratchets)))
|
||||
(keys (:ignore-counts test-ratchets))))
|
||||
kondo-ratchet/config-suppressions (constantly {})
|
||||
kondo-ratchet/module-escape-hatches (constantly {})
|
||||
kondo-ratchet/scan (constantly occurrences)]
|
||||
@@ -118,7 +127,8 @@
|
||||
(deftest check-reports-empty-unlimited-test
|
||||
(let [ratchets {:ignore-counts {:z-empty :unlimited, :a-empty :unlimited, :free :unlimited, :over 1}}]
|
||||
(is (= {:lines ["WARNING: :unlimited policies with no ignores left: :a-empty, :z-empty -- delete an entry by hand once its linter no longer needs one"
|
||||
"ok -- 2 ignore forms within 4 policies"]
|
||||
"ok -- 2 ignore forms within 4 policies"
|
||||
"ok -- 0 test ignore forms within 0 test policies"]
|
||||
:thrown? false}
|
||||
(check-with! ratchets (occurrences {:free 1, :over 1})))
|
||||
"the warning comes first, sorted, and does not fail the check")
|
||||
@@ -130,7 +140,8 @@
|
||||
:thrown? true}
|
||||
(check-with! ratchets (occurrences {:free 1, :over 2})))
|
||||
"a real failure still fails, after the warning")
|
||||
(is (= {:lines ["ok -- 4 ignore forms within 4 policies"]
|
||||
(is (= {:lines ["ok -- 4 ignore forms within 4 policies"
|
||||
"ok -- 0 test ignore forms within 0 test policies"]
|
||||
:thrown? false}
|
||||
(check-with! ratchets (occurrences {:z-empty 1, :a-empty 1, :free 1, :over 1})))
|
||||
"no warning when every unlimited policy is in use")))
|
||||
@@ -141,11 +152,40 @@
|
||||
occurrences [{:file "f.clj", :line 1, :linters [:grandfathered], :justified? true}
|
||||
{:file "g.clj", :line 1, :linters [:still-needed], :justified? false}]]
|
||||
(is (= {:lines ["WARNING: :comment-exempt is no longer needed for these linters: :grandfathered, :none-left -- delete the stale entries by hand"
|
||||
"ok -- 2 ignore forms within 3 policies"]
|
||||
"ok -- 2 ignore forms within 3 policies"
|
||||
"ok -- 0 test ignore forms within 0 test policies"]
|
||||
:thrown? false}
|
||||
(check-with! ratchets occurrences))
|
||||
"exemptions with only commented ignores or no ignores are named, and do not fail the check")))
|
||||
|
||||
;;;; ---------------------------------------------------------------------------
|
||||
;;;; Prod/test split
|
||||
;;;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest check-splits-prod-and-test-occurrences-test
|
||||
(let [ratchets {:ignore-counts {:a 1}
|
||||
:test-counts {:ignore-counts {:a 1}}}
|
||||
occurrences [{:file "src/f.clj", :line 1, :linters [:a], :justified? true}
|
||||
{:file "test/g.clj", :line 1, :linters [:a], :justified? true}
|
||||
{:file "test/h.clj", :line 2, :linters [:a], :justified? true}]]
|
||||
(is (= {:lines ["over budget -- remove an ignore, or seed the budget with `./bin/mage kondo-ratchets-shrink --seed <linter>` and explain the increase in the PR:"
|
||||
" :a: 1 recorded, 2 actual"
|
||||
" test/g.clj:1"
|
||||
" test/h.clj:2"]
|
||||
:thrown? true}
|
||||
(check-with! ratchets occurrences))
|
||||
"one prod occurrence stays within the prod budget; the two test occurrences alone blow the
|
||||
test budget, independent of the prod count")))
|
||||
|
||||
(deftest check-test-ratchets-disabled-test
|
||||
(let [ratchets {:ignore-counts {}
|
||||
:test-counts {:disabled true}}
|
||||
occurrences [{:file "test/g.clj", :line 1, :linters [:unbudgeted], :justified? true}]]
|
||||
(is (= {:lines [(str "ok -- 0 ignore forms within 0 policies")], :thrown? false}
|
||||
(check-with! ratchets occurrences))
|
||||
"a disabled test-ratchets file opts the test tree out of enforcement, even with an unbudgeted
|
||||
test-only linter")))
|
||||
|
||||
(deftest ^:parallel stale-test
|
||||
(let [ratchets {:ignore-counts {:a 5, :gone 2}}]
|
||||
(is (= []
|
||||
@@ -208,3 +248,69 @@
|
||||
#"only \{:disabled true\} opts out"
|
||||
(kondo-ratchet/check))))
|
||||
"is missing -- only {:disabled true} opts out of enforcement"))))
|
||||
|
||||
(deftest check-unknown-linter-in-test-file-names-the-right-file-test
|
||||
(let [dir (.toFile (java.nio.file.Files/createTempDirectory
|
||||
"kondo-ratchet-check-test"
|
||||
(make-array java.nio.file.attribute.FileAttribute 0)))
|
||||
budgets (doto (io/file dir "ratchets.edn")
|
||||
(spit (kondo-ratchet/render {:ignore-counts {}, :config-counts {}, :comment-exempt #{}})))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets {})))
|
||||
test-budgets (doto (io/file dir "ratchets-test.edn")
|
||||
(spit (kondo-ratchet/render-test {:ignore-counts {:bogus 1}, :comment-exempt #{}})))]
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath budgets)
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)
|
||||
kondo-ratchet/*test-ratchets-file* (.getPath test-budgets)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly #{})]
|
||||
(let [out (with-out-str
|
||||
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"names 1 unknown linter: :bogus"
|
||||
(kondo-ratchet/check))))]
|
||||
(is (str/includes? out (str (.getPath test-budgets) " names 1 unknown linter: :bogus"))
|
||||
"the test file is named, not the prod file")
|
||||
(is (not (str/includes? out (.getPath budgets)))
|
||||
"the prod file is never mentioned"))))))
|
||||
|
||||
(deftest check-rejects-nonempty-test-config-counts-test
|
||||
(let [dir (.toFile (java.nio.file.Files/createTempDirectory
|
||||
"kondo-ratchet-check-test"
|
||||
(make-array java.nio.file.attribute.FileAttribute 0)))
|
||||
budgets (doto (io/file dir "ratchets.edn")
|
||||
(spit (kondo-ratchet/render {:ignore-counts {}, :config-counts {}, :comment-exempt #{}})))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets {})))
|
||||
test-budgets (doto (io/file dir "ratchets-test.edn")
|
||||
(spit (kondo-ratchet/render {:ignore-counts {}, :config-counts {:a 1}, :comment-exempt #{}})))]
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath budgets)
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)
|
||||
kondo-ratchet/*test-ratchets-file* (.getPath test-budgets)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly #{:a})]
|
||||
(is (thrown-with-msg? clojure.lang.ExceptionInfo
|
||||
(re-pattern (str (java.util.regex.Pattern/quote (.getPath test-budgets))
|
||||
" must not set :config-counts -- config-level suppressions"
|
||||
" are tracked only in "
|
||||
(java.util.regex.Pattern/quote (.getPath budgets))))
|
||||
(kondo-ratchet/check)))))))
|
||||
|
||||
(deftest check-flags-a-test-file-in-prod-format-test
|
||||
(let [dir (.toFile (java.nio.file.Files/createTempDirectory
|
||||
"kondo-ratchet-check-test"
|
||||
(make-array java.nio.file.attribute.FileAttribute 0)))
|
||||
empty-policy {:ignore-counts {}, :config-counts {}, :comment-exempt #{}}
|
||||
budgets (doto (io/file dir "ratchets.edn") (spit (kondo-ratchet/render empty-policy)))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets {})))
|
||||
test-budgets (doto (io/file dir "ratchets-test.edn") (spit (kondo-ratchet/render empty-policy)))]
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath budgets)
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)
|
||||
kondo-ratchet/*test-ratchets-file* (.getPath test-budgets)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly #{})
|
||||
kondo-ratchet/scan (constantly [])
|
||||
kondo-ratchet/config-suppressions (constantly {})
|
||||
kondo-ratchet/module-escape-hatches (constantly {})]
|
||||
(let [out (with-out-str
|
||||
(is (thrown? clojure.lang.ExceptionInfo (kondo-ratchet/check))))]
|
||||
(is (= [(str (.getPath test-budgets) " is not normalized -- run `./bin/mage kondo-ratchets-shrink`"
|
||||
" to fix the formatting")]
|
||||
(str/split-lines out))
|
||||
"the prod header and an empty :config-counts are not the test file's canonical text"))))))
|
||||
|
||||
@@ -208,6 +208,20 @@
|
||||
(is (str/ends-with? (kondo-ratchet/render {:ignore-counts {}, :config-counts {}, :comment-exempt #{}})
|
||||
"{:ignore-counts {}\n :config-counts {}\n :comment-exempt #{}}\n"))))
|
||||
|
||||
(deftest ^:parallel render-test-test
|
||||
(let [ratchets {:ignore-counts {:discouraged-var 3}
|
||||
:comment-exempt #{:discouraged-var}}
|
||||
text (kondo-ratchet/render-test (assoc ratchets :config-counts {}))]
|
||||
(is (str/starts-with? text ";; Budgets for kondo suppressions in test code"))
|
||||
(is (str/ends-with? text (str "{:ignore-counts {:discouraged-var 3}\n"
|
||||
" :comment-exempt #{:discouraged-var}}\n"))
|
||||
"no :config-counts -- only the prod file tracks them")
|
||||
(is (= ratchets (edn/read-string text)))
|
||||
(is (= text (kondo-ratchet/render-test (edn/read-string text)))))
|
||||
(is (thrown-with-msg? clojure.lang.ExceptionInfo #"must not set :config-counts"
|
||||
(kondo-ratchet/render-test {:ignore-counts {}, :config-counts {:a 1}, :comment-exempt #{}}))
|
||||
"a nonempty :config-counts is an error, not silently dropped"))
|
||||
|
||||
(deftest ^:parallel render-module-ratchets-test
|
||||
(let [ratchets {:uses-any 4, :api-any 1}
|
||||
text (kondo-ratchet/render-module-ratchets ratchets)]
|
||||
@@ -349,6 +363,13 @@
|
||||
(with-out-str (kondo-ratchet/fix! {:seed "whatever"})))))
|
||||
(is (= "{:disabled true}\n" (slurp budgets)))))))
|
||||
|
||||
(defn- empty-test-ratchets-file!
|
||||
"A temp `.clj-kondo/ratchets-test.edn` stand-in, already clean (no budgets) -- so a [[kondo-ratchet/fix!]]
|
||||
run with no test-path occurrences writes nothing and reports nothing for it."
|
||||
^java.io.File [dir]
|
||||
(doto (io/file dir "ratchets-test.edn")
|
||||
(spit (kondo-ratchet/render-test {:ignore-counts {}, :comment-exempt #{}}))))
|
||||
|
||||
(deftest ^:synchronized seed-unlimited-linter-test
|
||||
(let [dir (.toFile (java.nio.file.Files/createTempDirectory
|
||||
"kondo-ratchet-test"
|
||||
@@ -357,17 +378,22 @@
|
||||
budgets (doto (io/file dir "ratchets.edn") (spit (kondo-ratchet/render ratchets)))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets {})))
|
||||
test-budgets (empty-test-ratchets-file! dir)
|
||||
occurrences [{:file "f.clj", :line 1, :linters [:free]}
|
||||
{:file "f.clj", :line 2, :linters [:free]}]]
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath budgets)
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)]
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)
|
||||
kondo-ratchet/*test-ratchets-file* (.getPath test-budgets)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly #{:free})
|
||||
kondo-ratchet/scan (constantly occurrences)
|
||||
kondo-ratchet/config-suppressions (constantly {})
|
||||
kondo-ratchet/module-escape-hatches (constantly {})]
|
||||
(is (= ["seeded :free at 2"
|
||||
"WARNING: :free has no inline ignores -- nothing to seed"
|
||||
(str "wrote " (.getPath budgets))]
|
||||
(str/split-lines (with-out-str (kondo-ratchet/fix! {:seed "free"}))))))
|
||||
(str/split-lines (with-out-str (kondo-ratchet/fix! {:seed "free"}))))
|
||||
"the prod occurrences seed the prod budget; the seeded linter has none under `test/`, so the
|
||||
test report says so and the test file is left untouched"))
|
||||
(is (= {:ignore-counts {:free 2}
|
||||
:config-counts {}
|
||||
:comment-exempt #{}}
|
||||
@@ -380,9 +406,11 @@
|
||||
text (kondo-ratchet/render {:ignore-counts {:free :unlimited}, :config-counts {}, :comment-exempt #{}})
|
||||
budgets (doto (io/file dir "ratchets.edn") (spit text))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets {})))]
|
||||
(spit (kondo-ratchet/render-module-ratchets {})))
|
||||
test-budgets (empty-test-ratchets-file! dir)]
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath budgets)
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)]
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)
|
||||
kondo-ratchet/*test-ratchets-file* (.getPath test-budgets)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly #{:free})
|
||||
kondo-ratchet/scan (fn [] (throw (AssertionError. "scanned before validating the seed")))]
|
||||
(is (thrown-with-msg? clojure.lang.ExceptionInfo
|
||||
@@ -401,10 +429,12 @@
|
||||
budgets (doto (io/file dir "ratchets.edn") (spit (kondo-ratchet/render ratchets)))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets {})))
|
||||
test-budgets (empty-test-ratchets-file! dir)
|
||||
occurrences [{:file "f.clj", :line 1, :linters [:free]}]
|
||||
run! #(str/split-lines (with-out-str (kondo-ratchet/fix!)))]
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath budgets)
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)]
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)
|
||||
kondo-ratchet/*test-ratchets-file* (.getPath test-budgets)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly #{:free :empty :gone :zero})
|
||||
kondo-ratchet/scan (constantly occurrences)
|
||||
kondo-ratchet/config-suppressions (constantly {})
|
||||
@@ -434,9 +464,11 @@
|
||||
budgets (doto (io/file dir "ratchets.edn") (spit (kondo-ratchet/render ratchets)))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets
|
||||
{:api-any 2, :friend-edges 5, :uses-any 1})))]
|
||||
{:api-any 2, :friend-edges 5, :uses-any 1})))
|
||||
test-budgets (empty-test-ratchets-file! dir)]
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath budgets)
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)]
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)
|
||||
kondo-ratchet/*test-ratchets-file* (.getPath test-budgets)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly #{})
|
||||
kondo-ratchet/scan (constantly [])
|
||||
kondo-ratchet/config-suppressions (constantly {})
|
||||
@@ -449,6 +481,80 @@
|
||||
(is (= {:api-any 1, :friend-edges 4}
|
||||
(kondo-ratchet/read-module-ratchets)))))))
|
||||
|
||||
(deftest ^:synchronized fix-splits-prod-and-test-budgets-test
|
||||
(let [dir (.toFile (java.nio.file.Files/createTempDirectory
|
||||
"kondo-ratchet-test"
|
||||
(make-array java.nio.file.attribute.FileAttribute 0)))
|
||||
ratchets {:ignore-counts {:a 5}, :config-counts {}, :comment-exempt #{}}
|
||||
budgets (doto (io/file dir "ratchets.edn") (spit (kondo-ratchet/render ratchets)))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets {})))
|
||||
test-budgets (doto (io/file dir "ratchets-test.edn")
|
||||
(spit (kondo-ratchet/render-test (dissoc ratchets :config-counts))))
|
||||
occurrences [{:file "src/f.clj", :line 1, :linters [:a]}
|
||||
{:file "src/f.clj", :line 2, :linters [:a]}
|
||||
{:file "test/g.clj", :line 1, :linters [:a]}]]
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath budgets)
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)
|
||||
kondo-ratchet/*test-ratchets-file* (.getPath test-budgets)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly #{:a})
|
||||
kondo-ratchet/scan (constantly occurrences)
|
||||
kondo-ratchet/config-suppressions (constantly {})
|
||||
kondo-ratchet/module-escape-hatches (constantly {})]
|
||||
(is (= ["lowered :a 5 -> 2"
|
||||
"lowered :a 5 -> 1"
|
||||
(str "wrote " (.getPath budgets))
|
||||
(str "wrote " (.getPath test-budgets))]
|
||||
(str/split-lines (with-out-str (kondo-ratchet/fix!))))
|
||||
"the two src occurrences lower the prod budget to 2; the one test occurrence lowers the test
|
||||
budget to 1, independently -- neither count masks the other"))
|
||||
(is (= {:ignore-counts {:a 2}, :config-counts {}, :comment-exempt #{}}
|
||||
(kondo-ratchet/read-ratchets)))
|
||||
(is (= {:ignore-counts {:a 1}, :config-counts {}, :comment-exempt #{}}
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath test-budgets)]
|
||||
(kondo-ratchet/read-ratchets)))))))
|
||||
|
||||
(deftest ^:synchronized fix-validates-test-ratchets-against-the-right-file-test
|
||||
(let [dir (.toFile (java.nio.file.Files/createTempDirectory
|
||||
"kondo-ratchet-test"
|
||||
(make-array java.nio.file.attribute.FileAttribute 0)))
|
||||
budgets (doto (io/file dir "ratchets.edn")
|
||||
(spit (kondo-ratchet/render {:ignore-counts {}, :config-counts {}, :comment-exempt #{}})))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets {})))
|
||||
test-budgets (doto (io/file dir "ratchets-test.edn")
|
||||
(spit (kondo-ratchet/render-test {:ignore-counts {:bogus 1}, :comment-exempt #{}})))]
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath budgets)
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)
|
||||
kondo-ratchet/*test-ratchets-file* (.getPath test-budgets)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly #{})]
|
||||
(is (thrown-with-msg? clojure.lang.ExceptionInfo
|
||||
(re-pattern (str "^" (java.util.regex.Pattern/quote (.getPath test-budgets))
|
||||
" names 1 unknown linter: :bogus"))
|
||||
(kondo-ratchet/fix! nil))
|
||||
"the test file is named, not the prod file")))))
|
||||
|
||||
(deftest ^:synchronized fix-rejects-nonempty-test-config-counts-test
|
||||
(let [dir (.toFile (java.nio.file.Files/createTempDirectory
|
||||
"kondo-ratchet-test"
|
||||
(make-array java.nio.file.attribute.FileAttribute 0)))
|
||||
budgets (doto (io/file dir "ratchets.edn")
|
||||
(spit (kondo-ratchet/render {:ignore-counts {}, :config-counts {}, :comment-exempt #{}})))
|
||||
modules (doto (io/file dir "module-ratchets.edn")
|
||||
(spit (kondo-ratchet/render-module-ratchets {})))
|
||||
test-budgets (doto (io/file dir "ratchets-test.edn")
|
||||
(spit (kondo-ratchet/render {:ignore-counts {}, :config-counts {:a 1}, :comment-exempt #{}})))]
|
||||
(binding [kondo-ratchet/*ratchets-file* (.getPath budgets)
|
||||
kondo-ratchet/*module-ratchets-file* (.getPath modules)
|
||||
kondo-ratchet/*test-ratchets-file* (.getPath test-budgets)]
|
||||
(mt/with-dynamic-fn-redefs [kondo-ratchet/known-linters (constantly #{:a})]
|
||||
(is (thrown-with-msg? clojure.lang.ExceptionInfo
|
||||
(re-pattern (str (java.util.regex.Pattern/quote (.getPath test-budgets))
|
||||
" must not set :config-counts -- config-level suppressions"
|
||||
" are tracked only in "
|
||||
(java.util.regex.Pattern/quote (.getPath budgets))))
|
||||
(kondo-ratchet/fix! nil)))))))
|
||||
|
||||
(deftest read-ratchets-requires-one-map-test
|
||||
(doseq [[content message] [["" #"is empty; expected one map"]
|
||||
[" \n;; only a comment\n" #"is empty; expected one map"]
|
||||
@@ -830,3 +936,23 @@
|
||||
"# TODO\n . TODONE"))
|
||||
(is (str/includes? (body ["# still our problem" ". fixed, so no longer our problem"])
|
||||
"[`./bin/mage kondo-ratchets-shrink`](https://example.test/run/1)"))))
|
||||
|
||||
(deftest ^:synchronized shrink-pr-body-from-files-test
|
||||
(let [dir (.toFile (java.nio.file.Files/createTempDirectory
|
||||
"kondo-ratchet-test"
|
||||
(make-array java.nio.file.attribute.FileAttribute 0)))
|
||||
write (fn [file-name text] (.getPath (doto (io/file dir file-name) (spit text))))
|
||||
prod (fn [n] (kondo-ratchet/render {:ignore-counts {:a n}, :config-counts {}, :comment-exempt #{}}))
|
||||
paths {:before (write "before.edn" (prod 3))
|
||||
:after (write "after.edn" (prod 2))
|
||||
:modules-before (write "modules-before.edn" (kondo-ratchet/render-module-ratchets {:friend-edges 5}))
|
||||
:modules-after (write "modules-after.edn" (kondo-ratchet/render-module-ratchets {:friend-edges 4}))
|
||||
:test-before (write "test-before.edn" (prod 7))
|
||||
:test-after (write "test-after.edn" (prod 6))}]
|
||||
(try
|
||||
(let [body (kondo-ratchet/shrink-pr-body-from-files paths "https://example.test/run/1")]
|
||||
(is (re-find #":a\s+3 => 2" body))
|
||||
(is (re-find #":module/friend-edges\s+5 => 4" body))
|
||||
(is (re-find #":test/a\s+7 => 6" body)))
|
||||
(finally
|
||||
(run! io/delete-file (reverse (file-seq dir)))))))
|
||||
|
||||
@@ -471,6 +471,31 @@
|
||||
;;;; kondo-insert-ignores
|
||||
;;;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn- exemption-suggestion
|
||||
"The `insert-ignores` follow-up message recommending where to add `linter` to :comment-exempt, given its
|
||||
`occurrences` (already found) -- naming the prod file, the test file, both, or nil when neither needs
|
||||
it. An existing comment above a flagged form may already justify its ignore, so a file is only named
|
||||
when the scanner still finds uncommented ignores there that its own :comment-exempt doesn't already
|
||||
cover; otherwise it is unnecessary and the ratchet check reports it as stale. Prod and test track
|
||||
:comment-exempt independently, so each is checked against its own file."
|
||||
[linter occurrences]
|
||||
(let [{test-occ true, prod-occ false} (group-by kondo-ratchet/test-occurrence? occurrences)
|
||||
needs-exempt? (fn [ratchets-file occs]
|
||||
(and (seq occs)
|
||||
(not (contains? (:comment-exempt (binding [kondo-ratchet/*ratchets-file* ratchets-file]
|
||||
(kondo-ratchet/read-ratchets)))
|
||||
linter))
|
||||
(seq (kondo-ratchet/unjustified #{} occs))))
|
||||
prod? (needs-exempt? kondo-ratchet/*ratchets-file* prod-occ)
|
||||
test? (needs-exempt? kondo-ratchet/*test-ratchets-file* test-occ)]
|
||||
(cond
|
||||
(and prod? test?) (format "Also add %s to :comment-exempt in both %s and %s -- the inserted ignores have no comments."
|
||||
linter kondo-ratchet/*ratchets-file* kondo-ratchet/*test-ratchets-file*)
|
||||
prod? (format "Also add %s to :comment-exempt in %s -- the inserted ignores have no comments."
|
||||
linter kondo-ratchet/*ratchets-file*)
|
||||
test? (format "Also add %s to :comment-exempt in %s -- the inserted ignores have no comments."
|
||||
linter kondo-ratchet/*test-ratchets-file*))))
|
||||
|
||||
(defn insert-ignores
|
||||
"Insert an inline ignore above every site `linter` flags, so a new linter can land without a big-bang
|
||||
fix. Args: `LINTER [PATHS...]`; paths default to the usual lint roots."
|
||||
@@ -494,11 +519,6 @@
|
||||
(count (distinct (map (juxt :filename :row) findings)))
|
||||
(count by-file)
|
||||
linter))
|
||||
;; An existing comment above the flagged form may justify the inserted ignore. Suggest an
|
||||
;; exemption only when the scanner still finds uncommented ignores; otherwise it is unnecessary
|
||||
;; and the ratchet check reports it as stale.
|
||||
(when-not (contains? (:comment-exempt (kondo-ratchet/read-ratchets)) linter)
|
||||
(when (seq (kondo-ratchet/unjustified #{} (filter #(some #{linter} (:linters %))
|
||||
(kondo-ratchet/scan roots))))
|
||||
(println (format "Also add %s to :comment-exempt in %s -- the inserted ignores have no comments."
|
||||
linter kondo-ratchet/*ratchets-file*))))))))
|
||||
(some-> (exemption-suggestion linter (filter #(some #{linter} (:linters %))
|
||||
(kondo-ratchet/scan roots)))
|
||||
println)))))
|
||||
|
||||
@@ -1,12 +1,59 @@
|
||||
(ns mage.kondo-ratchet-test
|
||||
(:require
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[clojure.test :refer [deftest is testing]]
|
||||
[dev.kondo-ratchet :as dev-ratchet]
|
||||
[mage.kondo-ratchet :as kondo-ratchet]))
|
||||
|
||||
;; Referenced by core_test.clj to ensure namespace is loaded
|
||||
(def keep-me :loaded)
|
||||
|
||||
(defn- with-ratchets-files!
|
||||
"Run `f` with [[dev.kondo-ratchet/*ratchets-file*]] and [[dev.kondo-ratchet/*test-ratchets-file*]]
|
||||
bound to temp files carrying `prod-exempt`/`test-exempt` as their only budgets."
|
||||
[prod-exempt test-exempt f]
|
||||
(let [dir (.toFile (java.nio.file.Files/createTempDirectory
|
||||
"kondo-ratchet-test" (make-array java.nio.file.attribute.FileAttribute 0)))
|
||||
prod (doto (io/file dir "ratchets.edn")
|
||||
(spit (dev-ratchet/render {:ignore-counts {}, :config-counts {}, :comment-exempt prod-exempt})))
|
||||
test (doto (io/file dir "ratchets-test.edn")
|
||||
(spit (dev-ratchet/render-test {:ignore-counts {}, :comment-exempt test-exempt})))]
|
||||
(binding [dev-ratchet/*ratchets-file* (.getPath prod)
|
||||
dev-ratchet/*test-ratchets-file* (.getPath test)]
|
||||
(f))))
|
||||
|
||||
(deftest exemption-suggestion-test
|
||||
(let [prod-occ {:file "src/f.clj", :line 1, :linters [:new], :justified? false}
|
||||
test-occ {:file "test/g.clj", :line 1, :linters [:new], :justified? false}]
|
||||
(testing "an unjustified occurrence only in prod code names only the prod file"
|
||||
(with-ratchets-files! #{} #{}
|
||||
#(is (= (format "Also add :new to :comment-exempt in %s -- the inserted ignores have no comments."
|
||||
dev-ratchet/*ratchets-file*)
|
||||
(#'kondo-ratchet/exemption-suggestion :new [prod-occ])))))
|
||||
(testing "an unjustified occurrence only in test code names only the test file"
|
||||
(with-ratchets-files! #{} #{}
|
||||
#(is (= (format "Also add :new to :comment-exempt in %s -- the inserted ignores have no comments."
|
||||
dev-ratchet/*test-ratchets-file*)
|
||||
(#'kondo-ratchet/exemption-suggestion :new [test-occ])))))
|
||||
(testing "unjustified occurrences in both name both files"
|
||||
(with-ratchets-files! #{} #{}
|
||||
#(is (= (format "Also add :new to :comment-exempt in both %s and %s -- the inserted ignores have no comments."
|
||||
dev-ratchet/*ratchets-file* dev-ratchet/*test-ratchets-file*)
|
||||
(#'kondo-ratchet/exemption-suggestion :new [prod-occ test-occ])))))
|
||||
(testing "a file whose own :comment-exempt already covers the linter is not named, even if the other needs it"
|
||||
(with-ratchets-files! #{:new} #{}
|
||||
#(is (= (format "Also add :new to :comment-exempt in %s -- the inserted ignores have no comments."
|
||||
dev-ratchet/*test-ratchets-file*)
|
||||
(#'kondo-ratchet/exemption-suggestion :new [prod-occ test-occ])))))
|
||||
(testing "no suggestion once every occurrence is justified"
|
||||
(with-ratchets-files! #{} #{}
|
||||
#(is (nil? (#'kondo-ratchet/exemption-suggestion
|
||||
:new [(assoc prod-occ :justified? true) (assoc test-occ :justified? true)])))))
|
||||
(testing "no suggestion when there are no occurrences at all"
|
||||
(with-ratchets-files! #{} #{}
|
||||
#(is (nil? (#'kondo-ratchet/exemption-suggestion :new [])))))))
|
||||
|
||||
(deftest insert-ignore-lines-test
|
||||
(testing "inserts at the flagged line's indentation"
|
||||
(is (= "(defn f [x]\n #_{:clj-kondo/ignore [:equals-true]}\n (= true x))\n"
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
(def ^:private ratchets-file
|
||||
".clj-kondo/ratchets.edn")
|
||||
|
||||
(def ^:private test-ratchets-file
|
||||
".clj-kondo/ratchets-test.edn")
|
||||
|
||||
(def ^:private module-ratchets-file
|
||||
".clj-kondo/config/modules/ratchets.edn")
|
||||
|
||||
@@ -141,6 +144,30 @@
|
||||
(is (= #{"M .clj-kondo/ratchets.edn" "UU app.txt"}
|
||||
(status dir)))))
|
||||
|
||||
(deftest resolves-test-ratchets-test
|
||||
(with-conflict [dir {:base {test-ratchets-file (ratchets {:a 5, :b :unlimited, :ours-drop 2} {} #{:b})}
|
||||
:ours {test-ratchets-file (ratchets {:a 5, :b 3} {} #{})}
|
||||
:theirs {test-ratchets-file (ratchets {:a 4, :b :unlimited, :ours-drop 1} {} #{:b})}}]
|
||||
(let [{:keys [exit out]} (run dir script)]
|
||||
(is (= 0 exit))
|
||||
(is (str/includes? out (str "staged merged " test-ratchets-file))))
|
||||
(let [text (slurp (str (fs/path dir test-ratchets-file)))]
|
||||
(is (= {:ignore-counts {:a 4, :b 3}
|
||||
:comment-exempt #{}}
|
||||
(edn/read-string text))
|
||||
"merged with the same rules as the prod file, and written without :config-counts")
|
||||
(is (str/starts-with? text ";; Budgets for kondo suppressions in test code")))
|
||||
(is (= #{(str "M " test-ratchets-file) "UU app.txt"}
|
||||
(status dir))))
|
||||
(testing "a stage with :config-counts is refused rather than having them dropped"
|
||||
(with-conflict [dir {:base {test-ratchets-file (ratchets {:a 5} {} #{})}
|
||||
:ours {test-ratchets-file (ratchets {:a 4} {} #{})}
|
||||
:theirs {test-ratchets-file (ratchets {:a 3} {:c 1} #{})}}]
|
||||
(let [{:keys [exit out err]} (run dir script)]
|
||||
(is (pos? exit))
|
||||
(is (str/includes? (str out err) "must not set :config-counts")))
|
||||
(is (contains? (status dir) (str "UU " test-ratchets-file))))))
|
||||
|
||||
(deftest resolves-module-ratchets-test
|
||||
(with-conflict [dir {:base {module-ratchets-file "{:api-any 3, :friend-edges 5}\n"}
|
||||
:ours {module-ratchets-file "{:api-any 2, :friend-edges 5}\n"}
|
||||
@@ -229,4 +256,4 @@
|
||||
(with-conflict [dir {:base {ratchets-file base-ratchets}}]
|
||||
(let [{:keys [exit err]} (run dir script)]
|
||||
(is (= 1 exit))
|
||||
(is (str/includes? err "neither ratchet file is conflicted")))))
|
||||
(is (str/includes? err "no ratchet file is conflicted")))))
|
||||
|
||||
Reference in New Issue
Block a user