Skip to content

Commit bdecb15

Browse files
committed
Code coverage for unit tests
1 parent 7a6acd9 commit bdecb15

File tree

3 files changed

+107
-6
lines changed

3 files changed

+107
-6
lines changed

Makefile

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ $(SYNCER_BIN): $(SYNCER_BIN_SRCS) syncer_manifest
148148
@touch $@
149149

150150
# The default build target.
151-
build build-bins: $(CSI_BIN) $(CSI_BIN_WINDOWS) $(SYNCER_BIN)
151+
build build-bins: $(CSI_BIN) $(CSI_BIN_WINDOWS) $(SYNCER_BIN) coverage-check
152152
build-with-docker:
153153
hack/make.sh
154154

@@ -196,7 +196,7 @@ dist: dist-csi dist-syncer
196196
.PHONY: deploy
197197
deploy: | $(DOCKER_SOCK)
198198
$(MAKE) build-bins
199-
$(MAKE) unit-test
199+
$(MAKE) coverage-check
200200
$(MAKE) push-images
201201

202202
################################################################################
@@ -207,7 +207,8 @@ clean:
207207
@rm -f Dockerfile*
208208
rm -rf $(CSI_BIN) vsphere-csi-*.tar.gz vsphere-csi-*.zip \
209209
$(SYNCER_BIN) vsphere-syncer-*.tar.gz vsphere-syncer-*.zip \
210-
image-*.tar image-*.d $(DIST_OUT)/* $(BIN_OUT)/* .build/windows-driver.tar
210+
image-*.tar image-*.d $(DIST_OUT)/* $(BIN_OUT)/* .build/windows-driver.tar \
211+
$(COVERAGE_OUT) $(COVERAGE_HTML) $(BUILD_OUT)/coverage-summary.txt cover.out
211212
GO111MODULE=off go clean -i -x . ./cmd/$(CSI_BIN_NAME) ./cmd/$(SYNCER_BIN_NAME)
212213

213214
.PHONY: clean-d
@@ -276,17 +277,49 @@ endif # ifndef X_BUILD_DISABLED
276277
## TESTING ##
277278
################################################################################
278279
ifndef PKGS_WITH_TESTS
279-
export PKGS_WITH_TESTS := $(sort $(shell find . -path ./tests -prune -o -name "*_test.go" -type f -exec dirname \{\} \;))
280+
export PKGS_WITH_TESTS := $(sort $(shell find . -path ./tests -prune -o -path ./e2e -prune -o -name "*_test.go" -type f -exec dirname \{\} \;))
280281
endif
281282
TEST_FLAGS ?= -v -count=1
283+
COVERAGE_OUT ?= $(BUILD_OUT)/coverage.out
284+
COVERAGE_HTML ?= $(BUILD_OUT)/coverage.html
285+
COVERAGE_THRESHOLD ?= 75
286+
282287
.PHONY: unit build-unit-tests
283288
unit unit-test:
284289
env -u VSPHERE_SERVER -u VSPHERE_DATACENTER -u VSPHERE_PASSWORD -u VSPHERE_USER -u VSPHERE_STORAGE_POLICY_NAME -u KUBECONFIG -u WCP_ENDPOINT -u WCP_PORT -u WCP_NAMESPACE -u TOKEN -u CERTIFICATE go test $(TEST_FLAGS) $(PKGS_WITH_TESTS)
290+
285291
unit-cover:
286292
env -u VSPHERE_SERVER -u VSPHERE_DATACENTER -u VSPHERE_PASSWORD -u VSPHERE_USER -u VSPHERE_STORAGE_POLICY_NAME -u KUBECONFIG -u WCP_ENDPOINT -u WCP_PORT -u WCP_NAMESPACE -u TOKEN -u CERTIFICATE go test $(TEST_FLAGS) $(PKGS_WITH_TESTS) && go tool cover -html=cover.out
293+
287294
build-unit-tests:
288295
$(foreach pkg,$(PKGS_WITH_TESTS),go test $(TEST_FLAGS) -c $(pkg); )
289296

297+
# Coverage targets with threshold checking
298+
.PHONY: test-coverage coverage-report coverage-check coverage-html
299+
test-coverage: build-dirs
300+
@echo "Running unit tests with coverage..."
301+
@mkdir -p $(BUILD_OUT)
302+
env -u VSPHERE_SERVER -u VSPHERE_DATACENTER -u VSPHERE_PASSWORD -u VSPHERE_USER -u VSPHERE_STORAGE_POLICY_NAME -u KUBECONFIG -u WCP_ENDPOINT -u WCP_PORT -u WCP_NAMESPACE -u TOKEN -u CERTIFICATE \
303+
go test $(TEST_FLAGS) -coverprofile=$(COVERAGE_OUT) -covermode=atomic $(PKGS_WITH_TESTS)
304+
305+
coverage-report: test-coverage
306+
@echo "Generating coverage report..."
307+
go tool cover -func=$(COVERAGE_OUT) | tee $(BUILD_OUT)/coverage-summary.txt
308+
309+
coverage-html: test-coverage
310+
@echo "Generating HTML coverage report..."
311+
go tool cover -html=$(COVERAGE_OUT) -o $(COVERAGE_HTML)
312+
@echo "HTML coverage report generated at: $(COVERAGE_HTML)"
313+
314+
coverage-check: coverage-report
315+
@echo "Analyzing coverage quality (excluding 0% functions)..."
316+
@python3 scripts/check-coverage.py $(BUILD_OUT)/coverage-summary.txt $(COVERAGE_THRESHOLD)
317+
318+
# Full coverage workflow with threshold enforcement
319+
.PHONY: coverage-full
320+
coverage-full: coverage-check coverage-html
321+
@echo "Coverage analysis complete. Check $(COVERAGE_HTML) for detailed report."
322+
290323
INTEGRATION_TEST_PKGS ?=
291324
.PHONY: integration-unit-test
292325
integration-unit-test:
@@ -349,7 +382,7 @@ endif
349382
# The default test target.
350383
.PHONY: test test-cover build-tests
351384
test: unit
352-
test-cover: unit-cover
385+
test-cover: coverage-full
353386
build-tests: build-unit-tests
354387

355388
.PHONY: cover

pkg/common/cns-lib/volume/util.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,7 @@ func IsNotSupportedFault(ctx context.Context, fault *types.LocalizedMethodFault)
611611
log.Errorf("observed fault with nil cause")
612612
}
613613
} else {
614-
log.Errorf("can not typecast fault to CnsFault")
614+
log.Errorf("cannot typecast fault to CnsFault")
615615
}
616616
return false
617617
}

scripts/check-coverage.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Coverage checker script for vSphere CSI Driver.
4+
Analyzes coverage excluding 0% functions and enforces quality threshold.
5+
"""
6+
7+
import re
8+
import sys
9+
import os
10+
11+
12+
def main():
13+
if len(sys.argv) < 3:
14+
print("Usage: check-coverage.py <coverage-summary-file> <threshold>")
15+
sys.exit(1)
16+
17+
coverage_file = sys.argv[1]
18+
threshold = float(sys.argv[2])
19+
20+
if not os.path.exists(coverage_file):
21+
print(f"ERROR: Coverage file {coverage_file} not found")
22+
sys.exit(1)
23+
24+
# Read the coverage summary file
25+
with open(coverage_file, 'r') as f:
26+
content = f.read()
27+
28+
# Extract all coverage percentages
29+
lines = content.strip().split('\n')
30+
coverage_data = []
31+
32+
for line in lines:
33+
match = re.search(r'(\d+\.\d+)%$', line)
34+
if match:
35+
percentage = float(match.group(1))
36+
coverage_data.append(percentage)
37+
38+
# Calculate statistics
39+
total_functions = len(coverage_data)
40+
non_zero_functions = [p for p in coverage_data if p > 0.0]
41+
zero_functions = len([p for p in coverage_data if p == 0.0])
42+
total_non_zero = len(non_zero_functions)
43+
44+
# Calculate averages
45+
overall_avg = sum(coverage_data) / total_functions if total_functions > 0 else 0
46+
non_zero_avg = sum(non_zero_functions) / total_non_zero if total_non_zero > 0 else 0
47+
48+
# Print statistics
49+
print(f'Total functions: {total_functions}')
50+
print(f'Functions with 0% coverage: {zero_functions} ({zero_functions/total_functions*100:.1f}%)')
51+
print(f'Functions with >0% coverage: {total_non_zero} ({total_non_zero/total_functions*100:.1f}%)')
52+
print(f'Overall coverage (including 0%): {overall_avg:.1f}%')
53+
print(f'Quality coverage (excluding 0%): {non_zero_avg:.1f}%')
54+
55+
# Check threshold
56+
if non_zero_avg < threshold:
57+
print(f'ERROR: Quality coverage {non_zero_avg:.1f}% is below threshold {threshold}%')
58+
print(f'The functions that have tests are not well-tested enough.')
59+
print(f'Please improve test quality for existing tested functions.')
60+
sys.exit(1)
61+
else:
62+
print(f'SUCCESS: Quality coverage {non_zero_avg:.1f}% meets threshold {threshold}%')
63+
if zero_functions > 0:
64+
print(f'NOTE: Consider adding tests for {zero_functions} untested functions to improve overall coverage.')
65+
66+
67+
if __name__ == '__main__':
68+
main()

0 commit comments

Comments
 (0)