Add runtime coverage support.

This commit is contained in:
Katharine Berry 2018-08-24 17:12:49 -07:00
parent aa2a7d001a
commit da4bbd421c
4 changed files with 175 additions and 18 deletions

View File

@ -474,32 +474,22 @@ kube::golang::create_coverage_dummy_test() {
local name="$(basename "$package")"
cat <<EOF > $(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

View File

@ -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())
}
}

View File

@ -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() {
}

View File

@ -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
}