From aa2a7d001a7ecdebc027719e17359e217674d384 Mon Sep 17 00:00:00 2001 From: Katharine Berry Date: Fri, 24 Aug 2018 15:03:03 -0700 Subject: [PATCH] Build relevant binaries with coverage. --- hack/lib/golang.sh | 138 +++++++++++++++++++++++++++++++++++++++++---- hack/lib/util.sh | 14 +++++ 2 files changed, 140 insertions(+), 12 deletions(-) diff --git a/hack/lib/golang.sh b/hack/lib/golang.sh index a7650f401e9..3184a6a1286 100755 --- a/hack/lib/golang.sh +++ b/hack/lib/golang.sh @@ -211,6 +211,15 @@ readonly KUBE_STATIC_LIBRARIES=( kubectl ) +# Fully-qualified package names that we want to instrument for coverage information. +readonly KUBE_COVERAGE_INSTRUMENTED_PACKAGES=( + k8s.io/kubernetes/cmd/kube-apiserver + k8s.io/kubernetes/cmd/kube-controller-manager + k8s.io/kubernetes/cmd/kube-scheduler + k8s.io/kubernetes/cmd/kube-proxy + k8s.io/kubernetes/cmd/kubelet +) + # KUBE_CGO_OVERRIDES is a space-separated list of binaries which should be built # with CGO enabled, assuming CGO is supported on the target platform. # This overrides any entry in KUBE_STATIC_LIBRARIES. @@ -441,6 +450,104 @@ kube::golang::outfile_for_binary() { echo "${output_path}/${bin}" } +# Argument: the name of a Kubernetes package. +# Returns 0 if the binary can be built with coverage, 0 otherwise. +# NB: this ignores whether coverage is globally enabled or not. +kube::golang::is_covered_binary() { + return $(kube::util::array_contains "$1" "${KUBE_COVERAGE_INSTRUMENTED_PACKAGES[@]}") +} + +# Argument: the name of a Kubernetes package (e.g. k8s.io/kubernetes/cmd/kube-scheduler) +# Echos the path to a dummy test used for coverage information. +kube::golang::path_for_coverage_dummy_test() { + local package=$1 + local path="${KUBE_GOPATH}/src/$package" + local name=$(basename "$package") + echo "$path/zz_autogenerated_${name}_test.go" +} + +# Argument: the name of a Kubernetes package (e.g. k8s.io/kubernetes/cmd/kube-scheduler). +# Creates a dummy unit test on disk in the source directory for the given package. +# This unit test will invoke the package's standard entry point when run. +kube::golang::create_coverage_dummy_test() { + local package="$1" + local name="$(basename "$package")" + cat < $(kube::golang::path_for_coverage_dummy_test "$package") +package main + import ( + "flag" + "os" + "strconv" + "testing" + "time" +) + func TestMain(m *testing.M) { + // We need to pass coverage instructions to the unittest framework, so we hijack os.Args + original_args := os.Args + now := time.Now().UnixNano() + test_args := []string{os.Args[0], "-test.coverprofile=/tmp/k8s-${name}-" + strconv.FormatInt(now, 10) + ".cov"} + os.Args = test_args + + // This is sufficient for the unit tests to be set up. + flag.Parse() + + // Restore the original args for use by the program. + os.Args = original_args + // Go! + main() + + // Make sure we actually write the profiling information to disk, if we make it here. + // On long-running services, or anything that calls os.Exit(), this is insufficient, + // so be sure to call this from inside the binary too. + // TODO: actually have some code here. +} +EOF +} + +# Argument: the name of a Kubernetes package (e.g. k8s.io/kubernetes/cmd/kube-scheduler). +# Deletes a test generated by kube::golang::create_coverage_dumy_test. +kube::golang::delete_coverage_dummy_test() { + local package=$1 + rm $(kube::golang::path_for_coverage_dummy_test "$package") +} + +# Arguments: a list of kubernetes packages to build. +# Expected variables: build_args should be set to an array of Go build arguments. +# In addition, the standard build globals are assumed. +# +# Invokes Go to actually build some packages. If coverage is disabled, simply invokes +# go install. If coverage is enabled, builds covered binaries using go test, temporarily +# producing the required unit test files and then cleaning up after itself. +# Non-covered binaries are then built using go install as usual. +kube::golang::build_some_binaries() { + if [[ -n "${build_with_coverage:-}" ]]; then + local -a uncovered=() + for package in "$@"; do + if kube::golang::is_covered_binary "$package"; then + V=2 kube::log::info "Building $package with coverage..." + kube::golang::create_coverage_dummy_test "$package" + go test -c -o "$(kube::golang::outfile_for_binary "$package" "$platform")" \ + -covermode count \ + -coverpkg k8s.io/... \ + "${build_args[@]}" \ + "$package" + kube::golang::delete_coverage_dummy_test "$package" + else + uncovered+=("$package") + fi + done + if [[ "${#uncovered[@]}" != 0 ]]; then + V=2 kube::log::info "Building ${uncovered[@]} without coverage..." + go install "${build_args[@]}" "${uncovered[@]}" + else + V=2 kube::log::info "Nothing to build without coverage." + fi + else + V=2 kube::log::info "Coverage is disabled." + go install "${build_args[@]}" "$@" + fi +} + kube::golang::build_binaries_for_platform() { local platform=$1 @@ -460,18 +567,24 @@ kube::golang::build_binaries_for_platform() { fi done + local -a build_args if [[ "${#statics[@]}" != 0 ]]; then - CGO_ENABLED=0 go install -installsuffix static "${goflags[@]:+${goflags[@]}}" \ - -gcflags "${gogcflags}" \ - -ldflags "${goldflags}" \ - "${statics[@]:+${statics[@]}}" + build_args=( + -installsuffix static + ${goflags:+"${goflags[@]}"} + -gcflags "${gogcflags:-}" + -ldflags "${goldflags:-}" + ) + CGO_ENABLED=0 kube::golang::build_some_binaries "${statics[@]}" fi if [[ "${#nonstatics[@]}" != 0 ]]; then - go install "${goflags[@]:+${goflags[@]}}" \ - -gcflags "${gogcflags}" \ - -ldflags "${goldflags}" \ - "${nonstatics[@]:+${nonstatics[@]}}" + build_args=( + ${goflags:+"${goflags[@]}"} + -gcflags "${gogcflags:-}" + -ldflags "${goldflags:-}" + ) + kube::golang::build_some_binaries "${nonstatics[@]}" fi for test in "${tests[@]:+${tests[@]}}"; do @@ -480,9 +593,9 @@ kube::golang::build_binaries_for_platform() { mkdir -p "$(dirname ${outfile})" go test -c \ - "${goflags[@]:+${goflags[@]}}" \ - -gcflags "${gogcflags}" \ - -ldflags "${goldflags}" \ + ${goflags:+"${goflags[@]}"} \ + -gcflags "${gogcflags:-}" \ + -ldflags "${goldflags:-}" \ -o "${outfile}" \ "${testpkg}" done @@ -535,10 +648,11 @@ kube::golang::build_binaries() { host_platform=$(kube::golang::host_platform) # Use eval to preserve embedded quoted strings. - local goflags goldflags gogcflags + local goflags goldflags gogcflags build_with_coverage eval "goflags=(${GOFLAGS:-})" goldflags="${GOLDFLAGS:-} $(kube::version::ldflags)" gogcflags="${GOGCFLAGS:-}" + build_with_coverage="${KUBE_BUILD_WITH_COVERAGE:-}" local -a targets=() local arg diff --git a/hack/lib/util.sh b/hack/lib/util.sh index a24b1093591..db0842b0c13 100755 --- a/hack/lib/util.sh +++ b/hack/lib/util.sh @@ -18,6 +18,20 @@ kube::util::sortable_date() { date "+%Y%m%d-%H%M%S" } +# arguments: target, item1, item2, item3, ... +# returns 0 if target is in the given items, 1 otherwise. +kube::util::array_contains() { + local search="$1" + local element + shift + for element; do + if [[ "$element" == "$search" ]]; then + return 0 + fi + done + return 1 +} + kube::util::wait_for_url() { local url=$1 local prefix=${2:-}