From da4bbd421c4c0eb75f730619427c2289d05b4f40 Mon Sep 17 00:00:00 2001 From: Katharine Berry Date: Fri, 24 Aug 2018 17:12:49 -0700 Subject: [PATCH] Add runtime coverage support. --- hack/lib/golang.sh | 27 +++------ pkg/util/coverage/coverage.go | 83 ++++++++++++++++++++++++++ pkg/util/coverage/coverage_disabled.go | 29 +++++++++ pkg/util/coverage/fake_test_deps.go | 54 +++++++++++++++++ 4 files changed, 175 insertions(+), 18 deletions(-) create mode 100644 pkg/util/coverage/coverage.go create mode 100644 pkg/util/coverage/coverage_disabled.go create mode 100644 pkg/util/coverage/fake_test_deps.go diff --git a/hack/lib/golang.sh b/hack/lib/golang.sh index 3184a6a1286..65bf3f58ac7 100755 --- a/hack/lib/golang.sh +++ b/hack/lib/golang.sh @@ -474,32 +474,22 @@ kube::golang::create_coverage_dummy_test() { local name="$(basename "$package")" cat < $(kube::golang::path_for_coverage_dummy_test "$package") package main - import ( - "flag" - "os" - "strconv" - "testing" - "time" +import ( + "testing" + "k8s.io/kubernetes/pkg/util/coverage" ) - 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() +func TestMain(m *testing.M) { + // Get coverage running + coverage.InitCoverage("${name}") - // 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. + // 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. + coverage.FlushCoverage() } EOF } @@ -530,6 +520,7 @@ kube::golang::build_some_binaries() { -covermode count \ -coverpkg k8s.io/... \ "${build_args[@]}" \ + -tags coverage \ "$package" kube::golang::delete_coverage_dummy_test "$package" else diff --git a/pkg/util/coverage/coverage.go b/pkg/util/coverage/coverage.go new file mode 100644 index 00000000000..cfb20c2e025 --- /dev/null +++ b/pkg/util/coverage/coverage.go @@ -0,0 +1,83 @@ +// +build coverage + +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package coverage provides tools for coverage-instrumented binaries to collect and +// flush coverage information. +package coverage + +import ( + "flag" + "github.com/golang/glog" + "k8s.io/apimachinery/pkg/util/wait" + "os" + "testing" + "time" +) + +const flushInterval = 5 * time.Second + +var coverageFile string + +// tempCoveragePath returns a temporary file to write coverage information to. +// The file is in the same directory as the destination, ensuring os.Rename will work. +func tempCoveragePath() string { + return coverageFile + ".tmp" +} + +// InitCoverage is called from the dummy unit test to prepare Go's coverage framework. +// Clients should never need to call it. +func InitCoverage(name string) { + // We read the coverage destination in from the KUBE_COVERAGE_FILE env var, + // or if it's empty we just use a default in /tmp + coverageFile = os.Getenv("KUBE_COVERAGE_FILE") + if coverageFile == "" { + coverageFile = "/tmp/k8s-" + name + ".cov" + } + + // Set up the unit test framework with the arguments we want. + flag.CommandLine.Parse([]string{"-test.coverprofile=" + tempCoveragePath()}) + + // Begin periodic logging + go wait.Forever(FlushCoverage, flushInterval) +} + +// FlushCoverage flushes collected coverage information to disk. +// The destination file is configured at startup and cannot be changed. +// Calling this function also sends a line like "coverage: 5% of statements" to stdout. +func FlushCoverage() { + // We're not actually going to run any tests, but we need Go to think we did so it writes + // coverage information to disk. To achieve this, we create a bunch of empty test suites and + // have it "run" them. + tests := []testing.InternalTest{} + benchmarks := []testing.InternalBenchmark{} + examples := []testing.InternalExample{} + + var deps fakeTestDeps + + dummyRun := testing.MainStart(deps, tests, benchmarks, examples) + dummyRun.Run() + + // Once it writes to the temporary path, we move it to the intended path. + // This gets us atomic updates from the perspective of another process trying to access + // the file. + if err := os.Rename(tempCoveragePath(), coverageFile); err != nil { + // This should never fail, because we're in the same directory. There's also little that + // we can do if it does. + glog.Errorf("Couldn't move coverage file from %s to %s", coverageFile, tempCoveragePath()) + } +} diff --git a/pkg/util/coverage/coverage_disabled.go b/pkg/util/coverage/coverage_disabled.go new file mode 100644 index 00000000000..c10ba69844f --- /dev/null +++ b/pkg/util/coverage/coverage_disabled.go @@ -0,0 +1,29 @@ +// +build !coverage + +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package coverage + +// InitCoverage is a no-op when not running with coverage. +func InitCoverage(name string) { + +} + +// FlushCoverage is a no-op when not running with coverage. +func FlushCoverage() { + +} diff --git a/pkg/util/coverage/fake_test_deps.go b/pkg/util/coverage/fake_test_deps.go new file mode 100644 index 00000000000..8ca0b9b0934 --- /dev/null +++ b/pkg/util/coverage/fake_test_deps.go @@ -0,0 +1,54 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package coverage + +import ( + "io" +) + +// This is an implementation of testing.testDeps. It doesn't need to do anything, because +// no tests are actually run. It does need a concrete implementation of at least ImportPath, +// which is called unconditionally when running tests. +type fakeTestDeps struct{} + +func (fakeTestDeps) ImportPath() string { + return "" +} + +func (fakeTestDeps) MatchString(pat, str string) (bool, error) { + return false, nil +} + +func (fakeTestDeps) StartCPUProfile(io.Writer) error { + return nil +} + +func (fakeTestDeps) StopCPUProfile() {} + +func (fakeTestDeps) StartTestLog(io.Writer) {} + +func (fakeTestDeps) StopTestLog() error { + return nil +} + +func (fakeTestDeps) WriteHeapProfile(io.Writer) error { + return nil +} + +func (fakeTestDeps) WriteProfileTo(string, io.Writer, int) error { + return nil +}