-
Notifications
You must be signed in to change notification settings - Fork 4.6k
balancer/randomsubsetting: Extend the unit tests in the randomsubsetting package. #8781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
balancer/randomsubsetting: Extend the unit tests in the randomsubsetting package. #8781
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #8781 +/- ##
==========================================
- Coverage 83.47% 83.28% -0.19%
==========================================
Files 419 414 -5
Lines 32595 32805 +210
==========================================
+ Hits 27208 27321 +113
- Misses 4017 4076 +59
- Partials 1370 1408 +38 🚀 New features to boost your workflow:
|
| _ "google.golang.org/grpc/balancer/roundrobin" // For round_robin LB policy in tests | ||
| ) | ||
|
|
||
| func (s) TestSubsettingEndpointsDomain(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is similar to the existing test TestCalculateSubset_Simple.
You can remove this and add more test cases in TestCalculateSubset_Simple if you think of any.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
Yes, the only possible case that could be added to TestCalculateSubset_Simple is the case where the number of endpoints is strictly greater than the subset size.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed it is only one test case missing, i had renamed function and added to test set.
| } | ||
| } | ||
|
|
||
| func (s) TestUniformDistributionOfEndpoints(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a descriptive comment for this test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this test to the existing test file randomsubsetting_test.go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to both the above comments. Please add comments about why the math is the way it is in the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Appropriate commentary added.
| } | ||
| } | ||
|
|
||
| func (s) TestUniformDistributionOfEndpoints(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you run the test ~10k times on your local to verify that the test is not flaky.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How often does this flake currently? We do not want to add tests that can flake. We want a test that is guaranteed to pass every time that it runs, and when it fails, it should really mean that there is a bug in the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added two examples of tests for which the uniform distribution verification is negative.
|
|
||
| endpoints := makeEndpoints(16) | ||
| expected := iteration / len(endpoints) * subsetSize | ||
| diff = expected / 7 // allow ~14% difference |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain the reasoning behind 14% error-rate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you look at this and modify the test to calculate how many iterations are needed for statistical significance, instead of arbitrarily adding huge error rate to satisfy small range of iterations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I carefully analyzed the test acceptance criteria and came to the conclusion that it was too naive. Finally I use the Chi-square goodness-of-fit test, standard statistical method used to validate whether a dataset follows a uniform distribution. It assesses if the observed frequencies in your data align with the expected frequencies of a theoretical distribution where every outcome has an equal chance of occurring.
marek-szews
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All comments were addressed. Additionally, each test of distribution concludes with a short report.
=== NAME Test/UniformDistributionOfEndpoints
randomsubsetting_test.go:404: Test Case: Endpoints=16, SubsetSize=4, Iterations=10
randomsubsetting_test.go:409: Distribution check passed:
Endpoint | ExpValue | Diff from E | Status
---------------------------------------------------------------------------
endpoint-5 | 2.50 | 1.50 | ! > E ± σ (Noticeable)
endpoint-9 | 2.50 | 1.50 | ! > E ± σ (Noticeable)
endpoint-12 | 2.50 | 1.50 | ! > E ± σ (Noticeable)
endpoint-13 | 2.50 | 1.50 | ! > E ± σ (Noticeable)
endpoint-0 | 2.50 | 2.50 | ! > E ± σ (Noticeable)
endpoint-4 | 2.50 | 0.50 | E ± σ (Normal)
endpoint-10 | 2.50 | 0.50 | E ± σ (Normal)
endpoint-11 | 2.50 | 0.50 | E ± σ (Normal)
endpoint-1 | 2.50 | 1.50 | ! > E ± σ (Noticeable)
endpoint-6 | 2.50 | 0.50 | E ± σ (Normal)
endpoint-2 | 2.50 | 2.50 | ! > E ± σ (Noticeable)
endpoint-7 | 2.50 | 0.50 | E ± σ (Normal)
endpoint-8 | 2.50 | 0.50 | E ± σ (Normal)
endpoint-14 | 2.50 | 0.50 | E ± σ (Normal)
endpoint-15 | 2.50 | 1.50 | ! > E ± σ (Noticeable)
endpoint-3 | 2.50 | 1.50 | ! > E ± σ (Noticeable)
---------------------------------------------------------------------------
Distribution is uniform (χ²=12.00 <= critical value=24.99)
|
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request adds valuable unit tests for the randomsubsetting balancer, significantly increasing test coverage. The new tests include simple subset size validation and a statistical test for uniform endpoint distribution. My review focuses on improving the implementation of these tests for better clarity, correctness, and adherence to idiomatic Go practices. The suggestions include simplifying comparison logic, correcting the use of t.Run in a statistical test loop, refactoring complex boolean logic, and using idiomatic function signatures for maps.
| t.Run(fmt.Sprint(L), func(t *testing.T) { | ||
| t.Helper() | ||
| lb := &subsettingBalancer{ | ||
| cfg: &lbConfig{SubsetSize: uint32(L)}, | ||
| hashSeed: uint64(i ^ 3 + K*i + L), | ||
| hashDigest: xxhash.New(), | ||
| } | ||
| subsetOfEndpoints := lb.calculateSubset(endpoints) | ||
|
|
||
| for _, ep := range subsetOfEndpoints { | ||
| EndpointCount[ep.Addresses[0].Addr]++ | ||
| } | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of t.Run inside this for loop is incorrect. The purpose of this loop is to perform multiple iterations to gather statistics for a single test, not to define many sub-tests. This approach creates a large number of sub-tests (equal to K) with identical names, which clutters the test output and is not the intended use of t.Run. The subset calculation logic should be executed directly within the loop.
lb := &subsettingBalancer{
cfg: &lbConfig{SubsetSize: uint32(L)},
hashSeed: uint64(i ^ 3 + K*i + L),
hashDigest: xxhash.New(),
}
subsetOfEndpoints := lb.calculateSubset(endpoints)
for _, ep := range subsetOfEndpoints {
EndpointCount[ep.Addresses[0].Addr]++
}| if fmt.Sprint(got) != fmt.Sprint(tc.want) { | ||
| t.Fatalf("subset size=%v; endpoints(%v) = %v; want %v", tc.want, tc.endpoints, got, tc.want) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using fmt.Sprint for numeric comparison is inefficient and not idiomatic. A direct integer comparison is clearer and more performant. Additionally, the failure message can be improved to be more concise and avoid printing a potentially large list of endpoints.
| if fmt.Sprint(got) != fmt.Sprint(tc.want) { | |
| t.Fatalf("subset size=%v; endpoints(%v) = %v; want %v", tc.want, tc.endpoints, got, tc.want) | |
| } | |
| if got != int(tc.want) { | |
| t.Fatalf("len(subsetOfEndpoints) = %d; want %d", got, tc.want) | |
| } |
| if chi2 > criticalValue { | ||
| if dd.expectSuccess { | ||
| testPassed = false | ||
| } else { | ||
| testPassed = true | ||
| } | ||
| reportBuilder.WriteString(fmt.Sprintf("Distribution is not uniform (χ²=%.2f > critical value=%.2f)", chi2, criticalValue)) | ||
| } else { | ||
| if !dd.expectSuccess { | ||
| testPassed = false | ||
| } | ||
| reportBuilder.WriteString(fmt.Sprintf("Distribution is uniform (χ²=%.2f <= critical value=%.2f)", chi2, criticalValue)) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for setting testPassed is correct but unnecessarily complex and verbose. It can be simplified to a single boolean expression, which makes the code more readable and maintainable.
isUniform := chi2 <= criticalValue
testPassed = isUniform == dd.expectSuccess
if isUniform {
reportBuilder.WriteString(fmt.Sprintf("Distribution is uniform (χ²=%.2f <= critical value=%.2f)", chi2, criticalValue))
} else {
reportBuilder.WriteString(fmt.Sprintf("Distribution is not uniform (χ²=%.2f > critical value=%.2f)", chi2, criticalValue))
}| func initEndpointCount(epCount *map[string]int, eps []resolver.Endpoint) { | ||
| for _, ep := range eps { | ||
| (*epCount)[ep.Addresses[0].Addr] = 0 | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function initEndpointCount takes a pointer to a map (*map[string]int). In Go, maps are reference types, so passing them by value is sufficient to allow the function to modify their contents. Using a pointer is unidiomatic unless the function needs to re-assign the map variable itself. Please change the signature to accept a map by value. Note that the call to this function on line 387 will also need to be updated from initEndpointCount(&EndpointCount, endpoints) to initEndpointCount(EndpointCount, endpoints).
func initEndpointCount(epCount map[string]int, eps []resolver.Endpoint) {
for _, ep := range eps {
epCount[ep.Addresses[0].Addr] = 0
}
}
Add more unit tests to increase test coverage of 'randomsubsetting' package.
RELEASE NOTES: