Skip to content

amtool: add --test-file flag to config routes test for batch routing and inhibition testing#5175

Open
alliasgher wants to merge 2 commits into
prometheus:mainfrom
alliasgher:feat/amtool-batch-routes-test
Open

amtool: add --test-file flag to config routes test for batch routing and inhibition testing#5175
alliasgher wants to merge 2 commits into
prometheus:mainfrom
alliasgher:feat/amtool-batch-routes-test

Conversation

@alliasgher
Copy link
Copy Markdown

@alliasgher alliasgher commented Apr 15, 2026

Summary

Extends amtool config routes test with a --test-file flag that accepts a YAML file of named test cases. Each case fires a set of alerts together and asserts per-alert expectations, making it possible to test both routing and inhibition in a repeatable, CI-friendly way.

Closes #5167

Usage

# routing-tests.yaml
tests:
  - name: "Unmatched alert routes to default"
    alerts:
      - labels: {alertname: SomeAlert}
        expected_receivers: [default]

  - name: "critical suppresses warning"
    alerts:
      - labels: {alertname: SomeAlert, severity: critical}
        expected_receivers: [default]
      - labels: {alertname: SomeAlert, severity: warning}
        expected_inhibited: true
$ amtool config routes test --config.file alertmanager.yml --test-file routing-tests.yaml
PASS Unmatched alert routes to default
PASS critical suppresses warning

Exit code 0 on all pass, 1 on any failure.

Implementation

  • cli/routes_test_file.go — YAML structs, fakeAlertsProvider (minimal provider.Alerts that pre-loads a fixed alert set for inhibition), and executeRoutesTestFile / runRouteTestCase runner
  • cli/test_routing.go — adds --test-file flag; delegates to executeRoutesTestFile when set
  • cli/routes_test_file_test.go — 13 new tests (routing pass/fail, multi-receiver continue: true, inhibition pass/fail, error cases, end-to-end integration)

Routing uses the existing dispatch.Route.Match() path. Inhibition constructs an inhibit.Inhibitor with a fakeAlertsProvider that returns all alerts in the case via Subscribe(), calls ih.Run() + ih.WaitForLoading() to let the inhibitor process the feed, then checks ih.Mutes() per alert.

Summary by CodeRabbit

  • New Features

    • Add batch YAML-driven testing for alert routing and inhibition, including a CLI flag to run test files and clear pass/fail diagnostics.
    • Validates expected receiver order and inhibition behavior across multiple named test cases.
  • Tests

    • New unit and integration tests exercising routing, inhibition, helpers, and end-to-end test-file execution with accompanying test fixtures.

Add `--test-file` flag to `amtool config routes test` that accepts a
YAML file containing named test cases.  Each test case fires a set of
alerts together and asserts per-alert expectations:

  expected_receivers: [list]   – route must resolve to these receivers
  expected_inhibited: true     – alert must be muted by an inhibit rule

Running all alerts in a case together allows inhibition to be tested
because the fakeAlertsProvider pre-loads the full case set so the
Inhibitor's initial slurp sees source and target alerts at once.

YAML format:
  tests:
    - name: "Unmatched alert routes to default"
      alerts:
        - labels: {alertname: SomeAlert}
          expected_receivers: [default]
    - name: "critical suppresses warning"
      alerts:
        - labels: {alertname: SomeAlert, severity: critical}
          expected_receivers: [default]
        - labels: {alertname: SomeAlert, severity: warning}
          expected_inhibited: true

Exit code is 0 when all cases pass, 1 when any fail.

Closes prometheus#5167

Signed-off-by: Ali <alliasgher123@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

Adds YAML-driven batch routing test-file support to the CLI command amtool config routes test, enabling multiple named test cases that assert per-alert expected receivers or expected inhibition by integrating route matching and inhibitor evaluation.

Changes

Cohort / File(s) Summary
Batch Test Runner
cli/routes_test_file.go
New runner that reads YAML test files, builds deterministic alerts per case, optionally starts an inhibitor backed by a fake alerts provider, matches routes, validates expected_receivers or expected_inhibited, and prints PASS/FAIL with details.
Unit & Integration Tests
cli/routes_test_file_test.go, cli/testdata/conf.inhibit.yml
Adds comprehensive tests for label conversions, alert construction, routing behavior (including continue:true ordering), inhibition behavior, fake provider behavior, and two end-to-end test-file execution scenarios; includes fixture config with an inhibit rule.
CLI Flags & Wiring
cli/test_routing.go, cli/routing.go
Adds --test-file flag to config routes test; when set, runs batch test-file path via executeRoutesTestFile(ctx, testFile, configFile) and exits non-zero on failures. Also adds testFile string field to routingShow struct.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as amtool CLI
    participant FS as File System
    participant Parser as YAML Parser
    participant ConfigLoader as Alertmanager Config
    participant Inhibitor
    participant Router as Route Matcher
    participant Results as Test Results

    User->>CLI: Run `config routes test --test-file <file>`
    CLI->>FS: Read test file
    FS-->>CLI: YAML content
    CLI->>Parser: Unmarshal test definitions
    Parser-->>CLI: Test cases
    CLI->>ConfigLoader: Load Alertmanager config
    ConfigLoader-->>CLI: Route tree & inhibit rules

    loop For each test case
        CLI->>CLI: Build deterministic alert objects
        alt inhibit rules present
            CLI->>Inhibitor: Create with fake alerts provider and Run()
            Inhibitor-->>CLI: Ready after processing feed
        end
        loop For each alert
            alt expected_inhibited = true
                CLI->>Inhibitor: Mutes(labelset)?
                Inhibitor-->>CLI: muted / not muted
                CLI->>Results: Record PASS/FAIL
            else expected_receivers set
                CLI->>Router: Match(labelset)
                Router-->>CLI: ordered receivers
                CLI->>Results: Compare & record PASS/FAIL
            end
        end
    end

    Results-->>CLI: overall pass/fail
    CLI->>User: exit 0 if all pass else exit 1
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: adding a --test-file flag to the config routes test command for batch testing of routing and inhibition.
Description check ✅ Passed The PR description is comprehensive and well-structured. It includes a clear summary, usage example with YAML format, implementation details, and references the closed issue.
Linked Issues check ✅ Passed The code changes fully implement all objectives from issue #5167: adds --test-file flag, supports batch YAML test cases, enables inhibition assertions, uses dispatch.Route.Match() for routing, implements fakeAlertsProvider for inhibitor testing, and provides CI-friendly exit codes.
Out of Scope Changes check ✅ Passed All changes directly support the feature objectives. The implementation is focused and confined to the routing test functionality, with no extraneous modifications unrelated to batch routing and inhibition testing.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
cli/test_routing.go (1)

76-88: Reject mixed --test-file and single-alert inputs.

When this branch runs, positional labels, --verify.receivers, and --tree are silently ignored. That makes bad CI invocations hard to spot; return a usage error instead of discarding those options.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/test_routing.go` around lines 76 - 88, In routingTestAction, when
c.testFile != "" add validation to reject mixed usage: if a test file is
provided while positional labels are present or flags --verify.receivers or
--tree are set, return a usage error instead of silently ignoring them. Check
the presence of c.labels (or equivalent positional slice), c.verify.Receivers
(or the struct field used), and c.tree, and return a clear error (via
kingpin.Fatalf or an error return) indicating that --test-file cannot be
combined with positional labels, --verify.receivers, or --tree so callers see
the misuse immediately.
cli/routes_test_file_test.go (1)

128-174: Add regressions for invalid alert specs and inhibited receiver assertions.

The happy-path coverage is good, but the suite does not pin the two edge cases most likely to regress here: invalid YAML entries that set both/neither expectation fields, and alerts that are inhibited while the test asserts expected_receivers. Once the runner validates those cases explicitly, please add coverage for them here.

Also applies to: 191-238

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/routes_test_file_test.go` around lines 128 - 174, Add two regression
tests and/or input-validation to catch invalid alert specs and mismatched
inhibited/receiver assertions: (1) add a test (e.g.,
TestRunRouteTestCase_InvalidAlertSpec) using runRouteTestCase with a
testFileAlertDef that sets both ExpectedInhibited and non-empty
ExpectedReceivers (and another with neither set) and assert the runner returns
failure with an error message about invalid expectation fields; (2) add a test
(e.g., TestRunRouteTestCase_InhibitedButExpectedReceivers) where an alert is
actually inhibited (use inhibitRules from loadTestRoute) but the
testFileAlertDef includes ExpectedReceivers, and assert runRouteTestCase fails
with a message indicating "inhibited but expected_receivers" (or similar). Also
update runRouteTestCase to validate testFileAlertDef (fields ExpectedInhibited
and ExpectedReceivers) and return clear failures when both/none are set or when
an inhibited alert is asserted to have receivers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/routes_test_file.go`:
- Around line 194-221: The test incorrectly checks inhibition only when
ad.ExpectedInhibited is true, allowing muted alerts to still validate receiver
expectations; change the logic in the alert assertion block so you always check
inhibition first by calling ih.Mutes(context.Background(), lset) (handle ih ==
nil as before) and if muted, treat the alert as inhibited (fail receiver
assertions or compare against ExpectedInhibited), otherwise proceed to call
mainRoute.Match(lset) and validate ad.ExpectedReceivers; update the branch that
uses ad.ExpectedInhibited, ih.Mutes, mainRoute.Match, and equalStringSlices to
ensure receiver assertions are skipped or fail when ih.Mutes reports muted.
- Around line 37-45: Change testFileAlertDef.ExpectedInhibited from bool to
*bool (tri-state) and add validation to ensure exactly one of ExpectedReceivers
(non-empty) vs ExpectedInhibited (non-nil) is provided; implement a Validate()
method on testFileAlertDef that returns an error when (len(ExpectedReceivers) >
0) == (ExpectedInhibited != nil) (covers both both-present and both-absent
cases) and call this Validate() after parsing the YAML and before executing
assertions so invalid specs are rejected.

---

Nitpick comments:
In `@cli/routes_test_file_test.go`:
- Around line 128-174: Add two regression tests and/or input-validation to catch
invalid alert specs and mismatched inhibited/receiver assertions: (1) add a test
(e.g., TestRunRouteTestCase_InvalidAlertSpec) using runRouteTestCase with a
testFileAlertDef that sets both ExpectedInhibited and non-empty
ExpectedReceivers (and another with neither set) and assert the runner returns
failure with an error message about invalid expectation fields; (2) add a test
(e.g., TestRunRouteTestCase_InhibitedButExpectedReceivers) where an alert is
actually inhibited (use inhibitRules from loadTestRoute) but the
testFileAlertDef includes ExpectedReceivers, and assert runRouteTestCase fails
with a message indicating "inhibited but expected_receivers" (or similar). Also
update runRouteTestCase to validate testFileAlertDef (fields ExpectedInhibited
and ExpectedReceivers) and return clear failures when both/none are set or when
an inhibited alert is asserted to have receivers.

In `@cli/test_routing.go`:
- Around line 76-88: In routingTestAction, when c.testFile != "" add validation
to reject mixed usage: if a test file is provided while positional labels are
present or flags --verify.receivers or --tree are set, return a usage error
instead of silently ignoring them. Check the presence of c.labels (or equivalent
positional slice), c.verify.Receivers (or the struct field used), and c.tree,
and return a clear error (via kingpin.Fatalf or an error return) indicating that
--test-file cannot be combined with positional labels, --verify.receivers, or
--tree so callers see the misuse immediately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: eee5c203-4bea-4437-a6c1-b05054e52b69

📥 Commits

Reviewing files that changed from the base of the PR and between dcbdc8c and 59b15c3.

📒 Files selected for processing (5)
  • cli/routes_test_file.go
  • cli/routes_test_file_test.go
  • cli/routing.go
  • cli/test_routing.go
  • cli/testdata/conf.inhibit.yml

Comment thread cli/routes_test_file.go Outdated
Comment thread cli/routes_test_file.go Outdated
- Use *bool for ExpectedInhibited to distinguish 'not set' from false;
  validate that exactly one of expected_receivers/expected_inhibited is
  set per alert entry
- When an inhibitor is active, check mute status before receiver
  matching so that a receiver assertion on an actually-inhibited alert
  fails rather than incorrectly passing
- Add TestRunRouteTestCase_ReceiverExpectedButMuted to cover the new
  failure path

Signed-off-by: Ali <alliasgher123@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
cli/routes_test_file_test.go (1)

211-258: Add invalid/empty spec regression tests after validation hardening.

Once runtime validation is added, include explicit tests for tests: [] and for a case with alerts: [] so false-green behavior cannot regress.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/routes_test_file_test.go` around lines 211 - 258, Add two regression
tests that call executeRoutesTestFile with an empty test spec and with an alert
that has an empty alerts list to prevent false-green behavior: create
TestExecuteRoutesTestFile_EmptyTests where the YAML contains "tests: []" and
assert the call returns an error and/or passed == false (use require.Error and
require.False), and create TestExecuteRoutesTestFile_EmptyAlerts where a test
entry has "alerts: []" and similarly assert validation fails via require.Error
and require.False; reference executeRoutesTestFile and the YAML keys tests: []
and alerts: [] when adding these tests near the existing
TestExecuteRoutesTestFile_* functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/routes_test_file.go`:
- Around line 172-177: The test currently skips assertions when a case defines
no alerts; add an explicit failure when tc.Alerts is empty to avoid false
passes—before the loop that builds allAlerts (where tc.Alerts and buildTestAlert
are used), check if len(tc.Alerts) == 0 and call t.Fatalf or t.Errorf with a
clear message including the test case identifier (e.g., tc.Name or index) so
cases that supply no alerts fail immediately and do not return (true, "")
erroneously.
- Around line 148-159: The current logic sets allPassed := true and if tf.Tests
is empty the function returns success; change this to explicitly reject empty
test files by checking if len(tf.Tests) == 0 before the loop and returning
(false, error) or an appropriate non-success result; alternatively initialize
allPassed := false and only set true if at least one test ran, but preferred is
to add a pre-loop guard that inspects tf.Tests and returns a clear error
(mention tf.Tests, allPassed, runRouteTestCase, mainRoute, inhibitRules) so
empty test collections are treated as failure rather than silent success.

---

Nitpick comments:
In `@cli/routes_test_file_test.go`:
- Around line 211-258: Add two regression tests that call executeRoutesTestFile
with an empty test spec and with an alert that has an empty alerts list to
prevent false-green behavior: create TestExecuteRoutesTestFile_EmptyTests where
the YAML contains "tests: []" and assert the call returns an error and/or passed
== false (use require.Error and require.False), and create
TestExecuteRoutesTestFile_EmptyAlerts where a test entry has "alerts: []" and
similarly assert validation fails via require.Error and require.False; reference
executeRoutesTestFile and the YAML keys tests: [] and alerts: [] when adding
these tests near the existing TestExecuteRoutesTestFile_* functions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bc309204-c8f6-4529-93b4-c2988c05f900

📥 Commits

Reviewing files that changed from the base of the PR and between 59b15c3 and 332d16b.

📒 Files selected for processing (2)
  • cli/routes_test_file.go
  • cli/routes_test_file_test.go

Comment thread cli/routes_test_file.go
Comment on lines +148 to +159
allPassed := true
for _, tc := range tf.Tests {
passed, detail := runRouteTestCase(tc, mainRoute, inhibitRules)
if passed {
fmt.Printf("PASS %s\n", tc.Name)
} else {
fmt.Printf("FAIL %s / %s\n", tc.Name, detail)
allPassed = false
}
}

return allPassed, nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject empty tests files instead of returning success.

At Line 148, allPassed defaults to true; if tf.Tests is empty, the loop never runs and the command returns success with zero executed assertions.

Proposed fix
  allPassed := true
+ if len(tf.Tests) == 0 {
+ 	return false, fmt.Errorf("test file %q must contain at least one test case", testFilePath)
+ }
  for _, tc := range tf.Tests {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
allPassed := true
for _, tc := range tf.Tests {
passed, detail := runRouteTestCase(tc, mainRoute, inhibitRules)
if passed {
fmt.Printf("PASS %s\n", tc.Name)
} else {
fmt.Printf("FAIL %s / %s\n", tc.Name, detail)
allPassed = false
}
}
return allPassed, nil
allPassed := true
if len(tf.Tests) == 0 {
return false, fmt.Errorf("test file %q must contain at least one test case", testFilePath)
}
for _, tc := range tf.Tests {
passed, detail := runRouteTestCase(tc, mainRoute, inhibitRules)
if passed {
fmt.Printf("PASS %s\n", tc.Name)
} else {
fmt.Printf("FAIL %s / %s\n", tc.Name, detail)
allPassed = false
}
}
return allPassed, nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/routes_test_file.go` around lines 148 - 159, The current logic sets
allPassed := true and if tf.Tests is empty the function returns success; change
this to explicitly reject empty test files by checking if len(tf.Tests) == 0
before the loop and returning (false, error) or an appropriate non-success
result; alternatively initialize allPassed := false and only set true if at
least one test ran, but preferred is to add a pre-loop guard that inspects
tf.Tests and returns a clear error (mention tf.Tests, allPassed,
runRouteTestCase, mainRoute, inhibitRules) so empty test collections are treated
as failure rather than silent success.

Comment thread cli/routes_test_file.go
Comment on lines +172 to +177
// Build types.Alert slice for all alerts in this case so the inhibitor
// can see the whole set at once.
allAlerts := make([]*types.Alert, 0, len(tc.Alerts))
for _, ad := range tc.Alerts {
allAlerts = append(allAlerts, buildTestAlert(ad.Labels, now))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail test cases that define no alerts.

At Line 172 onward, an empty tc.Alerts executes no assertions and reaches Line 258 returning (true, ""). That creates false PASS cases despite no routed/inhibited behavior being tested.

Proposed fix
 func runRouteTestCase(
 	tc testFileCase,
 	mainRoute *dispatch.Route,
 	inhibitRules []amcommoncfg.InhibitRule,
 ) (bool, string) {
+	if len(tc.Alerts) == 0 {
+		return false, fmt.Sprintf("test case %q must contain at least one alert", tc.Name)
+	}
+
 	now := time.Now()

Also applies to: 258-258

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/routes_test_file.go` around lines 172 - 177, The test currently skips
assertions when a case defines no alerts; add an explicit failure when tc.Alerts
is empty to avoid false passes—before the loop that builds allAlerts (where
tc.Alerts and buildTestAlert are used), check if len(tc.Alerts) == 0 and call
t.Fatalf or t.Errorf with a clear message including the test case identifier
(e.g., tc.Name or index) so cases that supply no alerts fail immediately and do
not return (true, "") erroneously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add batch routing test file support with inhibition assertions to amtool config routes test

2 participants