1// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package cov
16
17import (
18	"bytes"
19	"encoding/binary"
20	"encoding/json"
21	"os"
22	"os/exec"
23	"path/filepath"
24	"strings"
25
26	"../cause"
27	"../llvm"
28)
29
30// File describes the coverage spans in a single source file.
31type File struct {
32	Path      string
33	Covered   SpanList // Spans with coverage
34	Uncovered SpanList // Compiled spans without coverage
35}
36
37// Coverage describes the coverage spans for all the source files for a single
38// process invocation.
39type Coverage struct {
40	Files []File
41}
42
43// Env holds the enviroment settings for performing coverage processing.
44type Env struct {
45	LLVM     llvm.Toolchain
46	RootDir  string // path to SwiftShader git root directory
47	ExePath  string // path to the executable binary
48	TurboCov string // path to turbo-cov (optional)
49}
50
51// AppendRuntimeEnv returns the environment variables env with the
52// LLVM_PROFILE_FILE environment variable appended.
53func AppendRuntimeEnv(env []string, coverageFile string) []string {
54	return append(env, "LLVM_PROFILE_FILE="+coverageFile)
55}
56
57// AllSourceFiles returns a *Coverage containing all the source files without
58// coverage data. This populates the coverage view with files even if they
59// didn't get compiled.
60func (e Env) AllSourceFiles() *Coverage {
61	var ignorePaths = map[string]bool{
62		"src/Common":   true,
63		"src/Main":     true,
64		"src/OpenGL":   true,
65		"src/Renderer": true,
66		"src/Shader":   true,
67		"src/System":   true,
68	}
69
70	// Gather all the source files to include them even if there is no coverage
71	// information produced for these files. This highlights files that aren't
72	// even compiled.
73	cov := Coverage{}
74	allFiles := map[string]struct{}{}
75	filepath.Walk(filepath.Join(e.RootDir, "src"), func(path string, info os.FileInfo, err error) error {
76		if err != nil {
77			return err
78		}
79		rel, err := filepath.Rel(e.RootDir, path)
80		if err != nil || ignorePaths[rel] {
81			return filepath.SkipDir
82		}
83		if !info.IsDir() {
84			switch filepath.Ext(path) {
85			case ".h", ".c", ".cc", ".cpp", ".hpp":
86				if _, seen := allFiles[rel]; !seen {
87					cov.Files = append(cov.Files, File{Path: rel})
88				}
89			}
90		}
91		return nil
92	})
93	return &cov
94}
95
96// Import uses the llvm-profdata and llvm-cov tools to import the coverage
97// information from a .profraw file.
98func (e Env) Import(profrawPath string) (*Coverage, error) {
99	profdata := profrawPath + ".profdata"
100
101	if err := exec.Command(e.LLVM.Profdata(), "merge", "-sparse", profrawPath, "-output", profdata).Run(); err != nil {
102		return nil, cause.Wrap(err, "llvm-profdata errored")
103	}
104	defer os.Remove(profdata)
105
106	if e.TurboCov == "" {
107		args := []string{
108			"export",
109			e.ExePath,
110			"-instr-profile=" + profdata,
111			"-format=text",
112		}
113		if e.LLVM.Version.GreaterEqual(llvm.Version{Major: 9}) {
114			// LLVM 9 has new flags that omit stuff we don't care about.
115			args = append(args,
116				"-skip-expansions",
117				"-skip-functions",
118			)
119		}
120
121		data, err := exec.Command(e.LLVM.Cov(), args...).Output()
122		if err != nil {
123			return nil, cause.Wrap(err, "llvm-cov errored: %v", string(err.(*exec.ExitError).Stderr))
124		}
125		cov, err := e.parseCov(data)
126		if err != nil {
127			return nil, cause.Wrap(err, "Couldn't parse coverage json data")
128		}
129		return cov, nil
130	}
131
132	data, err := exec.Command(e.TurboCov, e.ExePath, profdata).Output()
133	if err != nil {
134		return nil, cause.Wrap(err, "turbo-cov errored: %v", string(err.(*exec.ExitError).Stderr))
135	}
136	cov, err := e.parseTurboCov(data)
137	if err != nil {
138		return nil, cause.Wrap(err, "Couldn't process turbo-cov output")
139	}
140
141	return cov, nil
142}
143
144func appendSpan(spans []Span, span Span) []Span {
145	if c := len(spans); c > 0 && spans[c-1].End == span.Start {
146		spans[c-1].End = span.End
147	} else {
148		spans = append(spans, span)
149	}
150	return spans
151}
152
153// https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
154// https://stackoverflow.com/a/56792192
155func (e Env) parseCov(raw []byte) (*Coverage, error) {
156	// line int, col int, count int64, hasCount bool, isRegionEntry bool
157	type segment []interface{}
158
159	type file struct {
160		// expansions ignored
161		Name     string    `json:"filename"`
162		Segments []segment `json:"segments"`
163		// summary ignored
164	}
165
166	type data struct {
167		Files []file `json:"files"`
168	}
169
170	root := struct {
171		Data []data `json:"data"`
172	}{}
173	err := json.NewDecoder(bytes.NewReader(raw)).Decode(&root)
174	if err != nil {
175		return nil, err
176	}
177
178	c := &Coverage{Files: make([]File, 0, len(root.Data[0].Files))}
179	for _, f := range root.Data[0].Files {
180		relpath, err := filepath.Rel(e.RootDir, f.Name)
181		if err != nil {
182			return nil, err
183		}
184		if strings.HasPrefix(relpath, "..") {
185			continue
186		}
187		file := File{Path: relpath}
188		for sIdx := 0; sIdx+1 < len(f.Segments); sIdx++ {
189			start := Location{(int)(f.Segments[sIdx][0].(float64)), (int)(f.Segments[sIdx][1].(float64))}
190			end := Location{(int)(f.Segments[sIdx+1][0].(float64)), (int)(f.Segments[sIdx+1][1].(float64))}
191			if covered := f.Segments[sIdx][2].(float64) != 0; covered {
192				file.Covered = appendSpan(file.Covered, Span{start, end})
193			} else {
194				file.Uncovered = appendSpan(file.Uncovered, Span{start, end})
195			}
196		}
197		if len(file.Covered) > 0 {
198			c.Files = append(c.Files, file)
199		}
200	}
201
202	return c, nil
203}
204
205func (e Env) parseTurboCov(data []byte) (*Coverage, error) {
206	u32 := func() uint32 {
207		out := binary.LittleEndian.Uint32(data)
208		data = data[4:]
209		return out
210	}
211	u8 := func() uint8 {
212		out := data[0]
213		data = data[1:]
214		return out
215	}
216	str := func() string {
217		len := u32()
218		out := data[:len]
219		data = data[len:]
220		return string(out)
221	}
222
223	numFiles := u32()
224	c := &Coverage{Files: make([]File, 0, numFiles)}
225	for i := 0; i < int(numFiles); i++ {
226		path := str()
227		relpath, err := filepath.Rel(e.RootDir, path)
228		if err != nil {
229			return nil, err
230		}
231		if strings.HasPrefix(relpath, "..") {
232			continue
233		}
234
235		file := File{Path: relpath}
236
237		type segment struct {
238			location Location
239			count    int
240			covered  bool
241		}
242
243		numSegements := u32()
244		segments := make([]segment, numSegements)
245		for j := range segments {
246			segment := &segments[j]
247			segment.location.Line = int(u32())
248			segment.location.Column = int(u32())
249			segment.count = int(u32())
250			segment.covered = u8() != 0
251		}
252
253		for sIdx := 0; sIdx+1 < len(segments); sIdx++ {
254			start := segments[sIdx].location
255			end := segments[sIdx+1].location
256			if segments[sIdx].covered {
257				if segments[sIdx].count > 0 {
258					file.Covered = appendSpan(file.Covered, Span{start, end})
259				} else {
260					file.Uncovered = appendSpan(file.Uncovered, Span{start, end})
261				}
262			}
263		}
264
265		if len(file.Covered) > 0 {
266			c.Files = append(c.Files, file)
267		}
268	}
269
270	return c, nil
271}
272
273// Path is a tree node path formed from a list of strings
274type Path []string
275