Skip to content

balancer/randomsubsetting: Extend the unit tests in the randomsubsetting package.#8781

Open
marek-szews wants to merge 8 commits intogrpc:masterfrom
marek-szews:randomsubsetting-lb-unit-testing-extention
Open

balancer/randomsubsetting: Extend the unit tests in the randomsubsetting package.#8781
marek-szews wants to merge 8 commits intogrpc:masterfrom
marek-szews:randomsubsetting-lb-unit-testing-extention

Conversation

@marek-szews
Copy link
Contributor

@marek-szews marek-szews commented Dec 19, 2025

Add more unit tests to increase test coverage of 'randomsubsetting' package.

RELEASE NOTES:

  • balancer/randomsubsetting: Implementation of additional UT.

@codecov
Copy link

codecov bot commented Dec 19, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.40%. Comparing base (2d51986) to head (345055c).
⚠️ Report is 78 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8781      +/-   ##
==========================================
- Coverage   83.47%   81.40%   -2.08%     
==========================================
  Files         419      416       -3     
  Lines       32595    33429     +834     
==========================================
+ Hits        27208    27212       +4     
- Misses       4017     4661     +644     
- Partials     1370     1556     +186     

see 90 files with indirect coverage changes

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

@marek-szews marek-szews changed the title balancer/randomsubsetting: Extend the test coverage of randomsubsetting package. balancer/randomsubsetting: Extend the unit tests in the randomsubsetting package. Dec 19, 2025
_ "google.golang.org/grpc/balancer/roundrobin" // For round_robin LB policy in tests
)

func (s) TestSubsettingEndpointsDomain(t *testing.T) {
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

@marek-szews marek-szews Feb 3, 2026

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) {
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

@marek-szews marek-szews left a 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)

@emil10001
Copy link
Member

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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.

Copy link
Contributor Author

@marek-szews marek-szews left a comment

Choose a reason for hiding this comment

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

All comments left by Gemini Code Assist have been resolved. Test execution time has been reduced from 3 seconds to 0.1 seconds. The test output has also become much clearer.

Copy link
Contributor Author

@marek-szews marek-szews left a comment

Choose a reason for hiding this comment

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

Please check the current shape of implementation.

Copy link
Contributor Author

@marek-szews marek-szews left a comment

Choose a reason for hiding this comment

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

Could you verify my rework

@Pranjali-2501
Copy link
Contributor

Apologies for the delay, I'll take a look at it today.

}
}

func (s) TestSubsettingEndpointsSimply(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Test TestSubsettingEndpointsSimply and existing test TestCalculateSubset_Simple are doing the same think.

As mentioned here we can get rid of TestSubsettingEndpointsSimply completely and added a testcase in TestCalculateSubset_Simple in which the endpoints are strictly greater than subset size.

Something like this

func (s) TestCalculateSubset_Simple(t *testing.T) {
	tests := []struct {
		name       string
		endpoints  []resolver.Endpoint
		subsetSize uint32
		want       []resolver.Endpoint
	}{
		// existing code
		{
			name:       "SubsetSizeLessThanNumberOfEndpoints",
			endpoints:  makeEndpoints(15),
			subsetSize: 5,
			want:       makeEndpoints(5),
		},
	}

	// existing code
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not exactly like that, function makeEndpoints(15) will produce the series of endpoints with a arithmetic order {endpoint_0, endpoint_1, ..., endpoint_14}. Same makeEndpoints(5) give us {endpoint_0, endpoint_1,.., endpoint_4}. But the result of LB.calculateSubset(eps) is unpredictable. One time we can received subset {endpoint_4, endpoint_5, endpoint_8, endpoint_11, endpoint_14} another time completely different. Validation of members of subset 99% will failed. I have left those two tests separated, because the first of them, check the content of set (boundary condition is meet so method always return the unchanged set - with a origin order) while the second test due to randomisation of content of subset, validate only the cardinality of the set (number of elements).

}

func (s) TestUniformDistributionOfEndpoints(t *testing.T) {

Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this extra line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


for _, tc := range testCases {
endpoints := makeEndpoints(tc.eps)
// From a set of N numbers, we randomly select K-times a subset of L numbers,
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of keeping it as an inline comment inside the test loop, we should move it to the official Godoc for TestUniformDistributionOfEndpoints

Something like this in the beginning:

Suggested change
// From a set of N numbers, we randomly select K-times a subset of L numbers,
// TestUniformDistributionOfEndpoints verifies that the random subsetting
// policy achieves a uniform distribution across backends. From a set of N
// numbers, it randomly selects K-times a subset of L numbers, where L < N.
// Then it calculates how many times each number belonging to set N appears,
// compute the variance and standard deviation, and use a Chi-Square test to
// check whether the distribution is uniform.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done and move to the beginning of function declaration.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done and move to the beginning of function declaration.

sigma := math.Sqrt(variance) // Standard Deviation σ(N) = sqrt(σ²(N))

EndpointCount := make(map[string]int, N)
initEndpointCount(EndpointCount, endpoints)
Copy link
Contributor

Choose a reason for hiding this comment

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

Since initEndpointCount() is a very simple helper used only once within TestUniformDistributionOfEndpoints, we could consider inlining this logic directly into the main test function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// ChiSquareCriticalValue calculates the critical value for alpha (e.g., 0.05)
// and degrees of freedom (df).
func chiSquareCriticalValue(alpha float64, df float64) float64 {
// 1. Find the Z-score for the given alpha.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove 1./2. from the comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

func chiSquareCriticalValue(alpha float64, df float64) float64 {
// 1. Find the Z-score for the given alpha.
// For alpha = 0.05 (95% confidence), Z is approx 1.64485
z := getZScore(1 - alpha)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.
getZScore is only used once within chiSquareCriticalValue and serves as a simple lookup table, we can inline this logic too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor

@Pranjali-2501 Pranjali-2501 left a comment

Choose a reason for hiding this comment

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

@marek-szews I have added some comments, please take a look at it.

@Pranjali-2501
Copy link
Contributor

Pranjali-2501 commented Feb 25, 2026

@arjan-bal and @easwars, could you please take a look at the statistical verification logic introduced in TestUniformDistributionOfEndpoints.

Thanks

@easwars
Copy link
Contributor

easwars commented Feb 25, 2026

TestUniformDistributionOfEndpoints

We already have an implementation of this statistical verification logic here:

func pearsonsChiSquareTest(t *testing.T, observedCounts, expectedCounts map[string]float64) error {

@arjan-bal knows this better than anyone else.

But we should try and see if this can reused here instead of reimplementing it.

Copy link
Contributor Author

@marek-szews marek-szews left a comment

Choose a reason for hiding this comment

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

All comments have been taken into account, please check again.

}
}

func (s) TestSubsettingEndpointsSimply(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not exactly like that, function makeEndpoints(15) will produce the series of endpoints with a arithmetic order {endpoint_0, endpoint_1, ..., endpoint_14}. Same makeEndpoints(5) give us {endpoint_0, endpoint_1,.., endpoint_4}. But the result of LB.calculateSubset(eps) is unpredictable. One time we can received subset {endpoint_4, endpoint_5, endpoint_8, endpoint_11, endpoint_14} another time completely different. Validation of members of subset 99% will failed. I have left those two tests separated, because the first of them, check the content of set (boundary condition is meet so method always return the unchanged set - with a origin order) while the second test due to randomisation of content of subset, validate only the cardinality of the set (number of elements).

}

func (s) TestUniformDistributionOfEndpoints(t *testing.T) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


for _, tc := range testCases {
endpoints := makeEndpoints(tc.eps)
// From a set of N numbers, we randomly select K-times a subset of L numbers,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done and move to the beginning of function declaration.

sigma := math.Sqrt(variance) // Standard Deviation σ(N) = sqrt(σ²(N))

EndpointCount := make(map[string]int, N)
initEndpointCount(EndpointCount, endpoints)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


for _, tc := range testCases {
endpoints := makeEndpoints(tc.eps)
// From a set of N numbers, we randomly select K-times a subset of L numbers,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done and move to the beginning of function declaration.

sigma := math.Sqrt(variance) // Standard Deviation σ(N) = sqrt(σ²(N))

EndpointCount := make(map[string]int, N)
initEndpointCount(EndpointCount, endpoints)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// ChiSquareCriticalValue calculates the critical value for alpha (e.g., 0.05)
// and degrees of freedom (df).
func chiSquareCriticalValue(alpha float64, df float64) float64 {
// 1. Find the Z-score for the given alpha.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

func chiSquareCriticalValue(alpha float64, df float64) float64 {
// 1. Find the Z-score for the given alpha.
// For alpha = 0.05 (95% confidence), Z is approx 1.64485
z := getZScore(1 - alpha)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

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.

5 participants