blob: ca9d34e1cc9a3391598ebd36f61f2ed11aaa0582 [file] [log] [blame]
package coverageingest
import (
"context"
"fmt"
"io/ioutil"
"os"
"path"
"testing"
"cloud.google.com/go/storage"
"github.com/stretchr/testify/mock"
assert "github.com/stretchr/testify/require"
"go.skia.org/infra/coverage/go/common"
"go.skia.org/infra/coverage/go/db"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/fileutil"
"go.skia.org/infra/go/mockgcsclient"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/go/vcsinfo"
)
var mockctx = mock.Anything
var callback = mock.AnythingOfType("func(*storage.ObjectAttrs)")
const FILE_CONTENT = "Filler"
func assertFilesExist(t *testing.T, basePath string, files ...string) {
for _, f := range files {
fullPath := path.Join(basePath, f)
if fi, err := os.Stat(fullPath); os.IsNotExist(err) {
assert.Fail(t, "File should have existed and it does not", fullPath)
} else if err != nil {
assert.FailNow(t, "Unexpected error", err.Error())
} else {
assert.Falsef(t, fi.IsDir(), "File %s should be a real file, not a directory", fullPath)
}
}
}
func mockLongCommits(hashes ...string) []*vcsinfo.LongCommit {
retval := []*vcsinfo.LongCommit{}
for _, h := range hashes {
retval = append(retval, &vcsinfo.LongCommit{
ShortCommit: &vcsinfo.ShortCommit{
Hash: h,
Author: "nobody",
},
})
}
return retval
}
func mockProcessingSteps() (*db.MockCoverageCache, context.Context) {
ctx := exec.NewContext(context.Background(), func(c *exec.Command) error {
if c.Name == "tar" {
return nil
}
return fmt.Errorf("Unexpected use of context: calling command %#v", c)
})
calculateCoverage = func(ri renderInfo, folders ...string) (common.CoverageSummary, error) {
return common.CoverageSummary{}, nil
}
mcc := db.MockCoverageCache{}
mcc.On("CheckCache", mock.Anything).Return(common.CoverageSummary{}, false)
mcc.On("StoreToCache", mock.Anything, mock.Anything).Return(nil)
return &mcc, ctx
}
// Tests that we download files correctly from GCS when starting from fresh.
// Doesn't test untarring, coverage calculation, or caching
func TestBlankIngestion(t *testing.T) {
// MediumTest because we write to disk
testutils.MediumTest(t)
tpath, cleanup := testutils.TempDir(t)
defer cleanup()
mcc, ctx := mockProcessingSteps()
calculateCoverage = func(ri renderInfo, folders ...string) (common.CoverageSummary, error) {
return common.CoverageSummary{TotalLines: 40, MissedLines: 7}, nil
}
mg := mockgcsclient.New()
defer mg.AssertExpectations(t)
mg.On("AllFilesInDirectory", mockctx, "commit/abcdefgh/", callback).Run(func(args mock.Arguments) {
f := args.Get(2).(func(item *storage.ObjectAttrs))
f(&storage.ObjectAttrs{Name: "commit/abcdefgh/Some-Config.text.tar"})
f(&storage.ObjectAttrs{Name: "commit/abcdefgh/Some-Config.profraw"})
}).Return(nil).Once()
mg.On("AllFilesInDirectory", mockctx, "commit/d123045/", callback).Run(func(args mock.Arguments) {
f := args.Get(2).(func(item *storage.ObjectAttrs))
f(&storage.ObjectAttrs{Name: "commit/d123045/Some-Config.text.tar"})
f(&storage.ObjectAttrs{Name: "commit/d123045/Some-Config.profraw"})
f(&storage.ObjectAttrs{Name: "commit/d123045/Other-Config.text.tar"})
f(&storage.ObjectAttrs{Name: "commit/d123045/Other-Config.profraw"})
}).Return(nil).Once()
contents := []byte(FILE_CONTENT)
mg.On("GetFileContents", mockctx, mock.Anything).Return(contents, nil)
i := New(tpath, mg, mcc)
i.IngestCommits(ctx, mockLongCommits("abcdefgh", "d123045"))
mg.AssertNumberOfCalls(t, "GetFileContents", 6)
assertFilesExist(t, path.Join(tpath, "abcdefgh"), "Some-Config.text.tar", "Some-Config.profraw")
assertFilesExist(t, path.Join(tpath, "d123045"), "Some-Config.text.tar", "Some-Config.profraw", "Other-Config.text.tar", "Other-Config.profraw")
// spot check that the bytes were written appropriately
f, err := os.Open(path.Join(tpath, "d123045", "Some-Config.profraw"))
assert.NoError(t, err)
b, err := ioutil.ReadAll(f)
assert.NoError(t, err)
assert.Equal(t, contents, b)
ir := []IngestedResults{
{
Commit: &vcsinfo.ShortCommit{
Hash: "abcdefgh",
Author: "nobody",
},
Jobs: []common.CoverageSummary{{Name: "Some-Config", TotalLines: 40, MissedLines: 7}},
TotalCoverage: common.CoverageSummary{TotalLines: 40, MissedLines: 7},
},
{
Commit: &vcsinfo.ShortCommit{
Hash: "d123045",
Author: "nobody",
},
Jobs: []common.CoverageSummary{
{Name: "Other-Config", TotalLines: 40, MissedLines: 7},
{Name: "Some-Config", TotalLines: 40, MissedLines: 7},
},
TotalCoverage: common.CoverageSummary{TotalLines: 40, MissedLines: 7},
},
}
testutils.AssertDeepEqual(t, ir, i.GetResults())
}
// Tests that we download files correctly from GCS when some files already exist.
// Additionally tests that we re-calculate coverage for anything that is a cache miss
// (which is everything)
// Doesn't test untarring, coverage calculation, or caching
func TestPartialIngestion(t *testing.T) {
// MediumTest because we write to disk
testutils.MediumTest(t)
tpath, cleanup := testutils.TempDir(t)
defer cleanup()
mcc, ctx := mockProcessingSteps()
calculateCoverage = func(ri renderInfo, folders ...string) (common.CoverageSummary, error) {
return common.CoverageSummary{TotalLines: 40, MissedLines: 7}, nil
}
// Write the files so it appears they have already been ingested
if _, err := fileutil.EnsureDirExists(path.Join(tpath, "abcdefgh")); err != nil {
assert.Fail(t, "Could not make dir abcdefgh", err)
}
if _, err := fileutil.EnsureDirExists(path.Join(tpath, "d123045")); err != nil {
assert.Fail(t, "Could not make dir abcdefgh", err)
}
testutils.WriteFile(t, path.Join(tpath, "abcdefgh", "Some-Config.text.tar"), FILE_CONTENT)
testutils.WriteFile(t, path.Join(tpath, "abcdefgh", "Some-Config.profraw"), FILE_CONTENT)
testutils.WriteFile(t, path.Join(tpath, "d123045", "Some-Config.text.tar"), FILE_CONTENT)
testutils.WriteFile(t, path.Join(tpath, "d123045", "Some-Config.html.tar"), FILE_CONTENT)
mg := mockgcsclient.New()
defer mg.AssertExpectations(t)
mg.On("AllFilesInDirectory", mockctx, "commit/abcdefgh/", callback).Run(func(args mock.Arguments) {
f := args.Get(2).(func(item *storage.ObjectAttrs))
f(&storage.ObjectAttrs{Name: "commit/abcdefgh/Some-Config.text.tar"})
f(&storage.ObjectAttrs{Name: "commit/abcdefgh/Some-Config.profraw"})
}).Return(nil).Once()
mg.On("AllFilesInDirectory", mockctx, "commit/d123045/", callback).Run(func(args mock.Arguments) {
f := args.Get(2).(func(item *storage.ObjectAttrs))
f(&storage.ObjectAttrs{Name: "commit/d123045/Some-Config.text.tar"})
f(&storage.ObjectAttrs{Name: "commit/d123045/Some-Config.html.tar"})
f(&storage.ObjectAttrs{Name: "commit/d123045/Other-Config.text.tar"})
f(&storage.ObjectAttrs{Name: "commit/d123045/Other-Config.profraw.tar.gz"})
}).Return(nil).Once()
contents := []byte(FILE_CONTENT)
mg.On("GetFileContents", mockctx, mock.Anything).Return(contents, nil)
i := New(tpath, mg, mcc)
// Old results should be purgd on new ingest
i.results = []IngestedResults{{Commit: &vcsinfo.ShortCommit{Hash: "Should not exist"}, Jobs: []common.CoverageSummary{{Name: "Go away"}}}}
i.IngestCommits(ctx, mockLongCommits("abcdefgh", "d123045"))
mg.AssertNumberOfCalls(t, "GetFileContents", 1)
mg.AssertCalled(t, "GetFileContents", mockctx, "commit/d123045/Other-Config.text.tar")
// Don't download .tar.gz files
assertFilesExist(t, path.Join(tpath, "abcdefgh"), "Some-Config.text.tar", "Some-Config.profraw")
assertFilesExist(t, path.Join(tpath, "d123045"), "Some-Config.text.tar", "Some-Config.html.tar", "Other-Config.text.tar")
ir := []IngestedResults{
{
Commit: &vcsinfo.ShortCommit{
Hash: "abcdefgh",
Author: "nobody",
},
Jobs: []common.CoverageSummary{{Name: "Some-Config", TotalLines: 40, MissedLines: 7}},
TotalCoverage: common.CoverageSummary{TotalLines: 40, MissedLines: 7},
},
{
Commit: &vcsinfo.ShortCommit{
Hash: "d123045",
Author: "nobody",
},
Jobs: []common.CoverageSummary{
{Name: "Other-Config", TotalLines: 40, MissedLines: 7},
{Name: "Some-Config", TotalLines: 40, MissedLines: 7},
},
TotalCoverage: common.CoverageSummary{TotalLines: 40, MissedLines: 7},
},
}
testutils.AssertDeepEqual(t, ir, i.GetResults())
}
// Tests that we correctly call the untar() function after downloading a tar file.
// Doesn't test downloading from GCS, coverage calculation, or caching.
func TestTarIngestion(t *testing.T) {
// MediumTest because we write to disk
testutils.MediumTest(t)
tpath, cleanup := testutils.TempDir(t)
defer cleanup()
mg := mockgcsclient.New()
defer mg.AssertExpectations(t)
mcc, _ := mockProcessingSteps()
called := 0
ctx := exec.NewContext(context.Background(), func(c *exec.Command) error {
if c.Name == "tar" {
called++
expectedArgs := []string{"xf", path.Join(tpath, "abcdefgh", "Some-Config.html.tar"), "--strip-components=6", "-C", path.Join(tpath, "abcdefgh", "Some-Config", "html")}
testutils.AssertDeepEqual(t, expectedArgs, c.Args)
}
return fmt.Errorf("Unexpected use of context: calling command %#v", c)
})
mg.On("AllFilesInDirectory", mockctx, "commit/abcdefgh/", callback).Run(func(args mock.Arguments) {
f := args.Get(2).(func(item *storage.ObjectAttrs))
f(&storage.ObjectAttrs{Name: "commit/abcdefgh/Some-Config.html.tar"})
}).Return(nil).Once()
mg.On("GetFileContents", mockctx, mock.Anything).Return([]byte(FILE_CONTENT), nil)
i := New(tpath, mg, mcc)
i.IngestCommits(ctx, mockLongCommits("abcdefgh"))
mg.AssertCalled(t, "GetFileContents", mockctx, "commit/abcdefgh/Some-Config.html.tar")
testutils.AssertDeepEqual(t, 1, called) // unTar() should be called exactly once
}
// Tests that the call to the "get combined coverage" function is well-formed.
// Doesn't test downloading from GCS, coverage calculation, untarring, or caching.
func TestCallToCombine(t *testing.T) {
testutils.MediumTest(t)
tpath, cleanup := testutils.TempDir(t)
defer cleanup()
mg := mockgcsclient.New()
defer mg.AssertExpectations(t)
mcc, ctx := mockProcessingSteps()
called := 1
calculateCoverage = func(ri renderInfo, folders ...string) (common.CoverageSummary, error) {
someFolder := path.Join(tpath, "abcdefgh", "Some-Config", "text", "coverage")
otherFolder := path.Join(tpath, "abcdefgh", "Other-Config", "text", "coverage")
if len(folders) == 1 && folders[0] == someFolder {
called *= 3
assert.Equal(t, renderInfo{}, ri, "Expecting empty render info for anything that's not the combined coverage")
} else if len(folders) == 1 && folders[0] == otherFolder {
called *= 5
assert.Equal(t, renderInfo{}, ri, "Expecting empty render info for anything that's not the combined coverage")
} else if len(folders) == 2 {
called *= 7
assert.Equal(t, "Combined", ri.jobName)
assert.Equal(t, "abcdefgh", ri.commit)
assert.Equal(t, []string{otherFolder, someFolder}, folders)
assert.Equal(t, path.Join(tpath, "abcdefgh", "Combined", "html"), ri.outputPath)
} else {
assert.Failf(t, "unexpected call", "calculateCoverage(%s)", folders)
}
return common.CoverageSummary{}, nil
}
// Assume they've been downloaded
if _, err := fileutil.EnsureDirExists(path.Join(tpath, "abcdefgh")); err != nil {
assert.Fail(t, "Could not make dir abcdefgh", err)
}
testutils.WriteFile(t, path.Join(tpath, "abcdefgh", "Some-Config.text.tar"), FILE_CONTENT)
testutils.WriteFile(t, path.Join(tpath, "abcdefgh", "Other-Config.text.tar"), FILE_CONTENT)
mg.On("AllFilesInDirectory", mockctx, "commit/abcdefgh/", callback).Run(func(args mock.Arguments) {
f := args.Get(2).(func(item *storage.ObjectAttrs))
f(&storage.ObjectAttrs{Name: "commit/abcdefgh/Some-Config.text.tar"})
f(&storage.ObjectAttrs{Name: "commit/abcdefgh/Other-Config.text.tar"})
}).Return(nil).Once()
i := New(tpath, mg, mcc)
i.IngestCommits(ctx, mockLongCommits("abcdefgh"))
testutils.AssertDeepEqual(t, 3*5*7, called) // createSummary() should be called 3 times, once with each folder, then once with an array.
}
// Tests the interface between the ingestion and the caching layer,
// i.e. that we use the cached results when appropriate.
// Doesn't test downloading from GCS, coverage calculation, or untarring.
func TestCachingCalls(t *testing.T) {
testutils.MediumTest(t)
tpath, cleanup := testutils.TempDir(t)
defer cleanup()
ctx := context.Background()
mg := mockgcsclient.New()
defer mg.AssertExpectations(t)
mcc := &db.MockCoverageCache{}
mcc.On("CheckCache", "Some-Config:abcdefgh").Return(common.CoverageSummary{MissedLines: 7, TotalLines: 77}, true)
mcc.On("CheckCache", mock.Anything).Return(common.CoverageSummary{}, false).Twice()
mcc.On("StoreToCache", mock.Anything, common.CoverageSummary{MissedLines: 12, TotalLines: 17}).Return(nil).Twice()
defer mcc.AssertExpectations(t)
mockProcessingSteps()
called := 0
calculateCoverage = func(ri renderInfo, folders ...string) (common.CoverageSummary, error) {
called++
return common.CoverageSummary{MissedLines: 12, TotalLines: 17}, nil
}
// Assume they've been downloaded
if _, err := fileutil.EnsureDirExists(path.Join(tpath, "abcdefgh")); err != nil {
assert.Fail(t, "Could not make dir abcdefgh", err)
}
testutils.WriteFile(t, path.Join(tpath, "abcdefgh", "Some-Config.text.tar"), FILE_CONTENT)
testutils.WriteFile(t, path.Join(tpath, "abcdefgh", "Other-Config.text.tar"), FILE_CONTENT)
mg.On("AllFilesInDirectory", mockctx, "commit/abcdefgh/", callback).Run(func(args mock.Arguments) {
f := args.Get(2).(func(item *storage.ObjectAttrs))
f(&storage.ObjectAttrs{Name: "commit/abcdefgh/Some-Config.text.tar"})
f(&storage.ObjectAttrs{Name: "commit/abcdefgh/Other-Config.text.tar"})
}).Return(nil).Once()
i := New(tpath, mg, mcc)
i.IngestCommits(ctx, mockLongCommits("abcdefgh"))
assert.Equal(t, 2, called)
ir := []IngestedResults{
{
Commit: &vcsinfo.ShortCommit{
Hash: "abcdefgh",
Author: "nobody",
},
Jobs: []common.CoverageSummary{
{Name: "Other-Config", TotalLines: 17, MissedLines: 12},
{Name: "Some-Config", TotalLines: 77, MissedLines: 7},
},
TotalCoverage: common.CoverageSummary{TotalLines: 17, MissedLines: 12},
},
}
testutils.AssertDeepEqual(t, ir, i.GetResults())
}
// Tests the defaultUntar function, in that it behaves properly on a tar file that
// mimics what the Coverage bots produce.
func TestUntarDefaultStructure(t *testing.T) {
// MediumTest because we write to disk
testutils.MediumTest(t)
tpath, cleanup := testutils.TempDir(t)
defer cleanup()
ctx := context.Background()
tarpath, err := testutils.TestDataDir()
assert.NoError(t, err, "Problem with getting TestDataDir")
tarpath = path.Join(tarpath, "Sample-Config.html.tar")
// The sample tar folder has the same directory structure that comes off
// the Linux coverage bots.
err = unTar(ctx, tarpath, path.Join(tpath, "SomeFolder"))
assert.NoError(t, err, "Problem untarring")
assertFilesExist(t, path.Join(tpath, "SomeFolder"), "bar.html", "foo.html")
assertFilesExist(t, path.Join(tpath, "SomeFolder", "coverage", "mnt", "pd0", "work", "skia", "dm"), "alpha.cpp")
}
// Tests the coverageData struct and its various operations.
func TestCoverageDataOperations(t *testing.T) {
testutils.SmallTest(t)
c1 := coverageData{
executableLines: map[int]bool{
1: true,
2: true,
5: false,
6: false,
7: true,
8: false,
},
sourceLines: map[int]string{
1: "alpha",
2: "beta",
3: "gamma",
},
}
assert.Equal(t, 6, c1.TotalExecutable())
assert.Equal(t, 3, c1.MissedExecutable())
c2 := coverageData{
executableLines: map[int]bool{
1: false,
2: true,
3: true,
4: false,
5: false,
6: true,
},
sourceLines: map[int]string{
1: "alpha",
2: "beta",
3: "gamma",
},
}
assert.Equal(t, 6, c2.TotalExecutable())
assert.Equal(t, 3, c2.MissedExecutable())
expected := &coverageData{
executableLines: map[int]bool{
1: true,
2: true,
3: true,
4: false,
5: false,
6: true,
7: true,
8: false,
},
sourceLines: map[int]string{
1: "alpha",
2: "beta",
3: "gamma",
},
}
assert.Equal(t, 8, expected.TotalExecutable())
assert.Equal(t, 3, expected.MissedExecutable())
testutils.AssertDeepEqual(t, expected, c1.Union(&c2))
testutils.AssertDeepEqual(t, expected, c2.Union(&c1))
}
// Tests the parsing logic in coverageData for output produced by LLVM 5
func TestCoverageDataParsingLLVM5(t *testing.T) {
testutils.SmallTest(t)
contents := testutils.MustReadFile("some-config.main.cpp")
expectedHTML := testutils.MustReadFile("someconfig.html")
expected := &coverageData{
executableLines: map[int]bool{
6: true,
7: true,
8: true,
9: true,
10: true,
11: true,
12: true,
13: true,
14: true,
15: true,
16: true,
17: true,
18: true,
19: true,
25: false,
26: false,
27: false,
29: true,
30: true,
31: true,
32: true,
33: true,
34: true,
35: true,
36: true,
39: false,
40: false,
41: false,
42: false,
},
}
parsed := parseLinesCovered(contents)
testutils.AssertDeepEqual(t, expected.executableLines, parsed.executableLines)
actualHTML, err := parsed.ToHTMLPage(CoverageFileData{
FileName: "test.cpp",
Commit: "adbde2143",
JobName: "Combined Report",
})
assert.NoError(t, err)
assert.Equal(t, 29, parsed.TotalExecutable())
assert.Equal(t, 7, parsed.MissedExecutable())
assert.Equal(t, 44, parsed.TotalSource())
assert.Equal(t, expectedHTML, actualHTML)
}
func calculateTotalCoverageSetup(t *testing.T, tpath string) {
// Set up a directory structure like what the coverage data looks like
somepath := path.Join(tpath, "Some-Config", "coverage", "mnt", "pd0", "work", "skia", "foo")
otherpath := path.Join(tpath, "Other-Config", "coverage", "mnt", "pd0", "work", "skia", "foo")
if _, err := fileutil.EnsureDirExists(somepath); err != nil {
assert.Fail(t, "Could not make dir"+tpath, err)
}
if _, err := fileutil.EnsureDirExists(otherpath); err != nil {
assert.Fail(t, "Could not make dir"+tpath, err)
}
contents := testutils.MustReadFile("some-config.main.cpp")
// Append .txt to mimic the LLVM files that we will analyze.
testutils.WriteFile(t, path.Join(somepath, "main.cpp.txt"), contents)
contents = testutils.MustReadFile("some-config.header.h")
testutils.WriteFile(t, path.Join(somepath, "one-header.h.txt"), contents)
contents = testutils.MustReadFile("other-config.main.cpp")
testutils.WriteFile(t, path.Join(otherpath, "main.cpp.txt"), contents)
contents = testutils.MustReadFile("other-config.header.h")
testutils.WriteFile(t, path.Join(otherpath, "two-header.h.txt"), contents)
}
// Tests that coverage of two folders (one per commit) is properly joined together
// and analyzed. In this example, the two folders share one file (main.cpp) and have
// two different headers that were "run".
func TestCalculateTotalCoverage(t *testing.T) {
testutils.MediumTest(t)
tpath, cleanup := testutils.TempDir(t)
defer cleanup()
calculateTotalCoverageSetup(t, tpath)
tc, err := defaultCalculateTotalCoverage(renderInfo{}, path.Join(tpath, "Some-Config", "coverage"), path.Join(tpath, "Other-Config", "coverage"))
assert.NoError(t, err)
expected := common.CoverageSummary{
TotalLines: 36, // Computed by hand
MissedLines: 7,
}
testutils.AssertDeepEqual(t, expected, tc)
}
func read(t *testing.T, path string) string {
contents, err := ioutil.ReadFile(path)
assert.NoError(t, err)
return string(contents)
}
func TestCalculateTotalCoverageOutputHTML(t *testing.T) {
testutils.MediumTest(t)
tpath, cleanup := testutils.TempDir(t)
defer cleanup()
calculateTotalCoverageSetup(t, tpath)
ri := renderInfo{
outputPath: path.Join(tpath, "Combined"),
commit: "98776ab3",
jobName: "Combined Unit Test",
}
_, err := defaultCalculateTotalCoverage(ri, path.Join(tpath, "Some-Config", "coverage"), path.Join(tpath, "Other-Config", "coverage"))
assert.NoError(t, err)
assert.Equal(t, testutils.MustReadFile("combined.main.cpp.html"),
read(t, path.Join(ri.outputPath, "coverage", "foo", "main.cpp.html")), "main.cpp.html differs")
assert.Equal(t, testutils.MustReadFile("combined.one-header.h.html"),
read(t, path.Join(ri.outputPath, "coverage", "foo", "one-header.h.html")), "one-header.h.html differs")
assert.Equal(t, testutils.MustReadFile("combined.two-header.h.html"),
read(t, path.Join(ri.outputPath, "coverage", "foo", "two-header.h.html")), "two-header.h.html differs")
assert.Equal(t, testutils.MustReadFile("combined.index.html"),
read(t, path.Join(ri.outputPath, "index.html")), "index.html differs")
}