Skip to content

Conversation

@haoqing0110
Copy link
Member

@haoqing0110 haoqing0110 commented Feb 9, 2026

  • Fix event recording to only record when decisions actually change
  • Extract recordDecisionEvents helper to avoid duplicated event recording
  • Add comprehensive event recording tests (100% coverage for new logic)
  • Refactor test helpers to reduce code duplication:
    • Add newTestSchedulingController to create test controllers
    • Add assertion helpers (assertClusterSetBindingNames, assertClusterSetNames, assertClusterNames)
    • Add verifyEvents and containsEventType for event verification

🤖 Generated with Claude Code

Summary

Related issue(s)

Fixes #1323

Summary by CodeRabbit

  • Bug Fixes

    • Events now reliably reflect placement decision status changes; label-only updates no longer emit status events.
    • Score events are emitted only when scores exist, use deterministic ordering, and are truncated when too long.
    • Event attribution corrected to reference the specific placement decision.
  • Tests

    • Added comprehensive event-recording tests covering create/update/label/warning scenarios.
    • Refactored test setup with helpers to reduce duplication and simplify assertions.

@openshift-ci openshift-ci bot requested review from elgnay and qiujian16 February 9, 2026 01:47
@openshift-ci
Copy link
Contributor

openshift-ci bot commented Feb 9, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: haoqing0110

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci bot added the approved label Feb 9, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Walkthrough

Extracted PlacementDecision event emission into a new helper and updated create/update flows to call it; events now use the PlacementDecision as the event source, score messages are deterministically ordered and truncated, and new unit tests and test helpers were added for event verification.

Changes

Cohort / File(s) Summary
Scheduling controller
pkg/placement/controllers/scheduling/scheduling_controller.go
Added recordDecisionEvents(...) method on schedulingController; replaced inline event emission with calls to this helper; score events are constructed from sorted cluster names and truncated when too long; event source uses the placementDecision.
Event tests
pkg/placement/controllers/scheduling/scheduling_controller_event_test.go
New unit tests TestCreateOrUpdatePlacementDecision_EventRecording covering create/update/no-op/label-change and warning scenarios; uses a FakeRecorder and helpers to collect and verify emitted events (DecisionCreate, DecisionUpdate, ScoreUpdate, Warning).
Scheduling test helpers / refactor
pkg/placement/controllers/scheduling/scheduling_controller_test.go
Replaced per-test setup with newTestSchedulingController helper; added internal assertion helpers (assertClusterSetBindingNames, assertClusterSetNames, assertClusterNames) and updated tests to use them.
Misc
go.mod, manifest_file
Minor manifest and module file adjustments (lines changed reported).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

lgtm

Suggested reviewers

  • elgnay
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main changes: improving event recording logic and test maintainability through refactoring and adding comprehensive tests.
Description check ✅ Passed The description includes summary of changes, related issue reference (#1323), and clearly lists the improvements made including bug fix, helper extraction, test coverage, and code refactoring.
Linked Issues check ✅ Passed The PR fully addresses issue #1323 by fixing event recording to only emit when decisions actually change, extracting the recordDecisionEvents helper, and adding comprehensive test coverage for the new logic.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue: event recording improvements, helper function extraction, comprehensive tests, and test helper refactoring to improve maintainability.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
pkg/placement/controllers/scheduling/scheduling_controller.go (1)

724-769: Clean extraction of event recording logic; past review feedback addressed.

The empty-scores guard (lines 750–752) and deterministic ordering via sets.List(sets.KeySet(...)) are good improvements. The truncation logic correctly prevents exceeding the event message length limit.

One minor nit: the format string "%s:%d " on line 757 leaves a trailing space on the last score entry. This is cosmetic and won't affect functionality, but if you'd like a clean message:

Optional: trim trailing space
-	scoreStr := ""
-	for _, name := range sortedClusterNames {
-		tmpScore := fmt.Sprintf("%s:%d ", name, clusterScores[name])
-		if len(scoreStr)+len(tmpScore) > maxEventMessageLength {
-			scoreStr += "......"
-			break
-		}
-		scoreStr += tmpScore
-	}
+	var parts []string
+	totalLen := 0
+	for _, name := range sortedClusterNames {
+		part := fmt.Sprintf("%s:%d", name, clusterScores[name])
+		if totalLen+len(part)+1 > maxEventMessageLength { // +1 for space separator
+			parts = append(parts, "......")
+			break
+		}
+		parts = append(parts, part)
+		totalLen += len(part) + 1
+	}
+	scoreStr := strings.Join(parts, " ")

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

@codecov
Copy link

codecov bot commented Feb 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 61.79%. Comparing base (fd6a0aa) to head (e2dca62).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1376      +/-   ##
==========================================
+ Coverage   61.71%   61.79%   +0.07%     
==========================================
  Files         226      226              
  Lines       18610    18616       +6     
==========================================
+ Hits        11486    11504      +18     
+ Misses       5904     5894      -10     
+ Partials     1220     1218       -2     
Flag Coverage Δ
unit 61.79% <100.00%> (+0.07%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@haoqing0110
Copy link
Member Author

/assign @qiujian16


// update the event with warning
// Record events only when there are changes (status updated or labels updated)
if labelUpdated {
Copy link
Member

Choose a reason for hiding this comment

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

why do we need to record event when label is changed? Is anything different in event under this case?

Copy link
Member Author

@haoqing0110 haoqing0110 Feb 11, 2026

Choose a reason for hiding this comment

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

PlacementDecision label contains the index and decision group info; there's a possibility that select clusters do not change, but only the label (decision group name) changes, and this should be traced by event.

Copy link
Member

Choose a reason for hiding this comment

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

but we do not record decision group name in the event I think?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we might need a different event to record the label changes.

Copy link
Member

Choose a reason for hiding this comment

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

if we do not have different events for now, could we just avoid event recording when label changes.

Copy link
Member Author

Choose a reason for hiding this comment

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

Got it, removed the logic to call recordDecisionEvents when label changes.

@haoqing0110 haoqing0110 force-pushed the refactor/improve-event-recording-logic branch 2 times, most recently from bcfb01e to 57891e6 Compare February 11, 2026 09:49
Copy link

@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: 1

🤖 Fix all issues with AI agents
In `@pkg/placement/controllers/scheduling/scheduling_controller.go`:
- Around line 724-766: The ScoreUpdate event is emitted even when clusterScores
is empty, producing noisy empty events; in recordDecisionEvents check for empty
scores (e.g., if len(clusterScores) == 0 or len(sortedClusterNames) == 0 after
computing sortedClusterNames from sets.KeySet(clusterScores)) and skip emitting
the ScoreUpdate Eventf via c.eventsRecorder.Eventf; leave the DecisionUpdate
event behavior unchanged so only the score-specific Eventf call (the one using
scoreStr) is suppressed when there are no cluster scores.
🧹 Nitpick comments (2)
pkg/placement/controllers/scheduling/scheduling_controller_event_test.go (2)

98-114: Missing test case: warning status with no decision change should not emit events.

The current "warning status event" test combines a warning status with a decision change. Consider adding a test where status is Warning but decisions are unchanged — this would verify that warning status alone doesn't trigger spurious events.


150-165: collectEvents may leave unexpected extra events undetected in the channel.

If the production code emits more events than expectedCount, this function stops collecting after hitting the expected count, and the surplus is silently ignored. The verifyEvents check at line 169 only validates len(events) == expectEventCount on what was collected.

Consider draining the channel briefly after the main collection loop to detect unexpected extra events:

Suggested improvement
 func collectEvents(recorder *kevents.FakeRecorder, expectedCount int, timeout time.Duration) []string {
 	var events []string
 	deadline := time.After(timeout)
 
 	for i := 0; i < expectedCount; i++ {
 		select {
 		case event := <-recorder.Events:
 			events = append(events, event)
 		case <-deadline:
 			return events
 		}
 	}
 
+	// Drain any unexpected extra events
+	for {
+		select {
+		case event := <-recorder.Events:
+			events = append(events, event)
+		case <-time.After(100 * time.Millisecond):
+			return events
+		}
+	}
+
 	return events
 }

This way, verifyEvents will catch unexpected surplus events via the count check.

@haoqing0110 haoqing0110 force-pushed the refactor/improve-event-recording-logic branch from 57891e6 to 061d2ce Compare February 11, 2026 10:40
Copy link

@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: 1

🤖 Fix all issues with AI agents
In `@pkg/placement/controllers/scheduling/scheduling_controller_event_test.go`:
- Around line 150-165: collectEvents currently returns immediately when
expectedCount==0 and therefore misses spurious events; modify collectEvents (the
function and its use of recorder.Events and expectedCount) to always wait for
the configured timeout and drain any events emitted during that period (or after
collecting the expectedCount) so tests catch unexpected extras — implement a
drain loop using the same deadline (select on recorder.Events vs deadline)
either when expectedCount==0 or after the main collection loop to append any
remaining events before returning.
🧹 Nitpick comments (2)
pkg/placement/controllers/scheduling/scheduling_controller_event_test.go (1)

98-114: "warning status event" expects 2 events but expectWarning: true — verify the Warning event is one of the 2, not a 3rd.

expectEventCount: 2 means collectEvents will collect at most 2 events. verifyEvents then checks for DecisionUpdate, ScoreUpdate, and Warning. Since the Warning check uses containsEventType(events, "Warning") which is a substring search on the same 2 collected events, this works if the DecisionUpdate event string contains "Warning" (which it will, since the event type is corev1.EventTypeWarning). This is correct but somewhat implicit — worth a brief inline comment for future maintainers.

pkg/placement/controllers/scheduling/scheduling_controller_test.go (1)

1446-1479: Assertion helpers are useful and correct.

Minor inconsistency: assertClusterSetBindingNames checks length separately then deletes from a set, while assertClusterSetNames and assertClusterNames use sets.Equal. Consider unifying the approach for consistency, but this is a nitpick.

Optional: align assertClusterSetBindingNames with the other helpers
 func assertClusterSetBindingNames(t *testing.T, bindings []*clusterapiv1beta2.ManagedClusterSetBinding, expectedNames []string) {
-	expectedBindingNames := sets.NewString(expectedNames...)
-	if len(bindings) != expectedBindingNames.Len() {
-		t.Errorf("expected %d bindings but got %d", expectedBindingNames.Len(), len(bindings))
-	}
-	for _, binding := range bindings {
-		expectedBindingNames.Delete(binding.Name)
-	}
-	if expectedBindingNames.Len() > 0 {
-		t.Errorf("expected bindings: %s", strings.Join(expectedBindingNames.List(), ","))
-	}
+	actual := sets.NewString()
+	for _, binding := range bindings {
+		actual.Insert(binding.Name)
+	}
+	expected := sets.NewString(expectedNames...)
+	if !actual.Equal(expected) {
+		t.Errorf("expected bindings %v, but got %v", expected.List(), actual.List())
+	}
 }

Comment on lines +150 to +165
// collectEvents collects events from FakeRecorder with a timeout
func collectEvents(recorder *kevents.FakeRecorder, expectedCount int, timeout time.Duration) []string {
var events []string
deadline := time.After(timeout)

for i := 0; i < expectedCount; i++ {
select {
case event := <-recorder.Events:
events = append(events, event)
case <-deadline:
return events
}
}

return events
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

collectEvents won't detect spurious events when expectedCount is 0.

When expectedCount is 0, the loop body never executes, so the function returns immediately without checking whether any unexpected events were emitted. This means the "labels changed only" and "no changes" test cases would still pass even if events were erroneously recorded — defeating the purpose of testing the core bug fix (issue #1323).

Consider adding a brief drain after collecting the expected events to catch unexpected extras:

Proposed fix
 func collectEvents(recorder *kevents.FakeRecorder, expectedCount int, timeout time.Duration) []string {
 	var events []string
 	deadline := time.After(timeout)
 
 	for i := 0; i < expectedCount; i++ {
 		select {
 		case event := <-recorder.Events:
 			events = append(events, event)
 		case <-deadline:
 			return events
 		}
 	}
 
+	// Drain any unexpected extra events within a short window
+	drainDeadline := time.After(100 * time.Millisecond)
+	for {
+		select {
+		case event := <-recorder.Events:
+			events = append(events, event)
+		case <-drainDeadline:
+			return events
+		}
+	}
+
 	return events
 }
🤖 Prompt for AI Agents
In `@pkg/placement/controllers/scheduling/scheduling_controller_event_test.go`
around lines 150 - 165, collectEvents currently returns immediately when
expectedCount==0 and therefore misses spurious events; modify collectEvents (the
function and its use of recorder.Events and expectedCount) to always wait for
the configured timeout and drain any events emitted during that period (or after
collecting the expectedCount) so tests catch unexpected extras — implement a
drain loop using the same deadline (select on recorder.Events vs deadline)
either when expectedCount==0 or after the main collection loop to append any
remaining events before returning.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Signed-off-by: Qing Hao <[email protected]>
@haoqing0110 haoqing0110 force-pushed the refactor/improve-event-recording-logic branch from 061d2ce to f015d36 Compare February 12, 2026 00:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PlacementDecision events are recorded even when there are no changes

2 participants