diff --git a/cmd/merge.go b/cmd/merge.go new file mode 100644 index 0000000..64e6495 --- /dev/null +++ b/cmd/merge.go @@ -0,0 +1,76 @@ +/* + Copyright 2020 Qiniu Cloud (qiniu.com) + + 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 cmd + +import ( + log "github.com/sirupsen/logrus" + + "github.com/spf13/cobra" + "golang.org/x/tools/cover" + "k8s.io/test-infra/gopherage/pkg/cov" + "k8s.io/test-infra/gopherage/pkg/util" +) + +var mergeCmd = &cobra.Command{ + Use: "merge [files...]", + Short: "Merge multiple coherent Go coverage files into a single file.", + Long: `merge will merge multiple Go coverage files into a single coverage file. +merge requires that the files are 'coherent', meaning that if they both contain references to the +same paths, then the contents of those source files were identical for the binary that generated +each file. +`, + Run: func(cmd *cobra.Command, args []string) { + runMerge(args, outputMergeProfile) + }, +} + +var outputMergeProfile string + +func init() { + mergeCmd.Flags().StringVarP(&outputMergeProfile, "output", "o", "mergeprofile.cov", "output file") + + rootCmd.AddCommand(mergeCmd) +} + +func runMerge(args []string, output string) { + if len(args) == 0 { + log.Fatalln("Expected at least one coverage file.") + return + } + + profiles := make([][]*cover.Profile, len(args)) + for _, path := range args { + profile, err := util.LoadProfile(path) + if err != nil { + log.Fatalf("failed to open %s: %v", path, err) + return + } + profiles = append(profiles, profile) + } + + merged, err := cov.MergeMultipleProfiles(profiles) + if err != nil { + log.Fatalf("failed to merge files: %v", err) + return + } + + err = util.DumpProfile(output, merged) + if err != nil { + log.Fatalln(err) + return + } +} diff --git a/cmd/merge_test.go b/cmd/merge_test.go new file mode 100644 index 0000000..ee35d79 --- /dev/null +++ b/cmd/merge_test.go @@ -0,0 +1,156 @@ +/* + Copyright 2020 Qiniu Cloud (qiniu.com) + + 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 cmd + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +// use a variable to record if the tested function has failed +// so unittest can only be exectued sequential +var fatal = false +var fatalStr string + +type TestLogHook struct{} + +func (h *TestLogHook) Levels() []log.Level { + return []log.Level{log.FatalLevel} +} + +func (h *TestLogHook) Fire(e *log.Entry) error { + fatalStr = e.Message + return nil +} + +func TestMain(m *testing.M) { + // setup + originalExitFunc := log.StandardLogger().ExitFunc + defer func() { + log.StandardLogger().ExitFunc = originalExitFunc + log.StandardLogger().Hooks = make(log.LevelHooks) + }() + + // replace exit function, so log.Fatal wont exit + log.StandardLogger().ExitFunc = func(int) { fatal = true } + // add hook, so fatal string will be recorded + log.StandardLogger().Hooks.Add(&TestLogHook{}) + + code := m.Run() + + os.Exit(code) +} + +func TestMergeNormalProfiles(t *testing.T) { + profileA := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/a.voc") + profileB := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/b.voc") + mergeprofile := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/merge.cov") + + runMerge([]string{profileA, profileB}, mergeprofile) + + contents, err := ioutil.ReadFile(mergeprofile) + assert.NoError(t, err) + assert.Contains(t, string(contents), "qiniu.com/kodo/apiserver/server/main.go:32.49,33.13 1 60") + assert.Contains(t, string(contents), "qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 2") + assert.Equal(t, fatal, false) +} + +// test with no profiles +func TestMergeWithNoProfiles(t *testing.T) { + mergeprofile := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/merge.cov") + + // clear fatal string in setup + fatalStr = "" + fatal = false + + runMerge([]string{}, mergeprofile) + + // there is fatal + assert.Equal(t, fatal, true) + assert.Equal(t, fatalStr, "Expected at least one coverage file.") +} + +// pass a non-existed profile to runMerge +func TestWithWrongProfileName(t *testing.T) { + profileA := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/notexist.voc") + mergeprofile := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/merge.cov") + + // clear fatal string in setup + fatalStr = "" + fatal = false + + runMerge([]string{profileA}, mergeprofile) + + // there is fatal + assert.Equal(t, fatal, true) + assert.Contains(t, fatalStr, "failed to open") +} + +// merge two different modes' profiles should fail +func TestMergeTwoDifferentModeProfile(t *testing.T) { + profileA := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/a.voc") + profileSet := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/setmode.voc") + mergeprofile := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/merge.cov") + + // clear fatal string in setup + fatalStr = "" + fatal = false + + runMerge([]string{profileA, profileSet}, mergeprofile) + + // there is fatal + assert.Equal(t, fatal, true) + assert.Contains(t, fatalStr, "mode for qiniu.com/kodo/apiserver/server/main.go mismatches") +} + +// merge two overlaped profiles should fail +func TestMergeTwoOverLapProfile(t *testing.T) { + profileA := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/a.voc") + profileOverlap := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/overlap.voc") + mergeprofile := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/merge.cov") + + // clear fatal string in setup + fatalStr = "" + fatal = false + + runMerge([]string{profileA, profileOverlap}, mergeprofile) + + // there is fatal + assert.Equal(t, fatal, true) + assert.Contains(t, fatalStr, "coverage block mismatch") +} + +// merge empty file should fail +func TestMergeEmptyProfile(t *testing.T) { + profileA := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/empty.voc") + mergeprofile := filepath.Join(baseDir, "../tests/samples/merge_profile_samples/merge.cov") + + // clear fatal string in setup + fatalStr = "" + fatal = false + + runMerge([]string{profileA}, mergeprofile) + + // there is fatal + assert.Equal(t, fatal, true) + assert.Contains(t, fatalStr, "failed to dump profile") +} diff --git a/pkg/cover/client.go b/pkg/cover/client.go index 7bdbb44..6567d8b 100644 --- a/pkg/cover/client.go +++ b/pkg/cover/client.go @@ -20,12 +20,13 @@ import ( "fmt" "io" "io/ioutil" - "log" "net" "net/http" "net/url" "strconv" "strings" + + log "github.com/sirupsen/logrus" ) // Action provides methods to contact with the covered service under test diff --git a/tests/merge.bats b/tests/merge.bats new file mode 100755 index 0000000..0b3e4cc --- /dev/null +++ b/tests/merge.bats @@ -0,0 +1,67 @@ +#!/usr/bin/env bats +# Copyright 2020 Qiniu Cloud (七牛云) +# +# 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. + +load util.sh + +setup_file() { + # run centered server + goc server 3>&- & + GOC_PID=$! + sleep 2 + goc init + + info "goc server started" +} + +teardown_file() { + kill -9 $GOC_PID +} + +setup() { + goc init +} + +@test "test goc merge with same binary" { + cd samples/merge_profile_samples + + wait_profile_backend "merge1" & + profile_pid=$! + + # merge two profiles with same binary + run gocc merge a.voc b.voc --output mergeprofile.voc1 --debug --debugcisyncfile ci-sync.bak; + info merge1 output: $output + [ "$status" -eq 0 ] + run cat mergeprofile.voc1 + [[ "$output" == *"qiniu.com/kodo/apiserver/server/main.go:32.49,33.13 1 60"* ]] + [[ "$output" == *"qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 2"* ]] +} + +@test "test goc merge with two binaries, but has some source code in common" { + cd samples/merge_profile_samples + + wait_profile_backend "merge2" & + profile_pid=$! + + # merge two profiles from two binaries, but has some source code in common + run gocc merge a.voc c.voc --output mergeprofile.voc2 --debug --debugcisyncfile ci-sync.bak; + info merge2 output: $output + [ "$status" -eq 0 ] + run cat mergeprofile.voc2 + [[ "$output" == *"qiniu.com/kodo/apiserver/server/main.go:32.49,33.13 1 60"* ]] + [[ "$output" == *"qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 0"* ]] + [[ "$output" == *"qiniu.com/kodo/apiserver/server/wala.go:42.49,43.13 1 0"* ]] + + wait $profile_pid +} diff --git a/tests/run-ci-actions.sh b/tests/run-ci-actions.sh index 48ac66f..14bef93 100755 --- a/tests/run-ci-actions.sh +++ b/tests/run-ci-actions.sh @@ -43,4 +43,6 @@ bats -t cover.bats bats -t agent.bats +bats -t merge.bats + bash <(curl -s https://codecov.io/bash) -f 'filtered*' -F e2e-$GOVERSION \ No newline at end of file diff --git a/tests/samples/merge_profile_samples/a.voc b/tests/samples/merge_profile_samples/a.voc new file mode 100644 index 0000000..5c8b06e --- /dev/null +++ b/tests/samples/merge_profile_samples/a.voc @@ -0,0 +1,3 @@ +mode: atomic +qiniu.com/kodo/apiserver/server/main.go:32.49,33.13 1 30 +qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 0 \ No newline at end of file diff --git a/tests/samples/merge_profile_samples/b.voc b/tests/samples/merge_profile_samples/b.voc new file mode 100644 index 0000000..c361181 --- /dev/null +++ b/tests/samples/merge_profile_samples/b.voc @@ -0,0 +1,3 @@ +mode: atomic +qiniu.com/kodo/apiserver/server/main.go:32.49,33.13 1 30 +qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 2 \ No newline at end of file diff --git a/tests/samples/merge_profile_samples/c.voc b/tests/samples/merge_profile_samples/c.voc new file mode 100644 index 0000000..348ef73 --- /dev/null +++ b/tests/samples/merge_profile_samples/c.voc @@ -0,0 +1,4 @@ +mode: atomic +qiniu.com/kodo/apiserver/server/main.go:32.49,33.13 1 30 +qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 0 +qiniu.com/kodo/apiserver/server/wala.go:42.49,43.13 1 0 \ No newline at end of file diff --git a/tests/samples/merge_profile_samples/empty.voc b/tests/samples/merge_profile_samples/empty.voc new file mode 100644 index 0000000..e69de29 diff --git a/tests/samples/merge_profile_samples/overlap.voc b/tests/samples/merge_profile_samples/overlap.voc new file mode 100644 index 0000000..cc87489 --- /dev/null +++ b/tests/samples/merge_profile_samples/overlap.voc @@ -0,0 +1,3 @@ +mode: atomic +qiniu.com/kodo/apiserver/server/main.go:32.49,33.12 1 30 +qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 0 \ No newline at end of file diff --git a/tests/samples/merge_profile_samples/setmode.voc b/tests/samples/merge_profile_samples/setmode.voc new file mode 100644 index 0000000..8ea14ce --- /dev/null +++ b/tests/samples/merge_profile_samples/setmode.voc @@ -0,0 +1,3 @@ +mode: set +qiniu.com/kodo/apiserver/server/main.go:32.49,33.13 1 30 +qiniu.com/kodo/apiserver/server/main.go:42.49,43.13 1 0 \ No newline at end of file