blob: f62ad71a2528cc58ba969a27565d8e887b56a9dc [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file
// This program converts a directory of YAML reports produced by `clang_doc` into
// a report usable by test coverage.
//
// Please refer to the file README.md in this directory for more information
// about the program and its use.
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"path"
"sort"
"go.fuchsia.dev/fuchsia/sdk/cts/plasa/model"
"go.fuchsia.dev/fuchsia/tools/fidl/lib/summarize"
)
var (
plasaManifestFile = flag.String("plasa-manifest-file", "", "The Plasa manifest file to read in")
output = flag.String("output", "", "The test coverage report output file")
fragmentPrefix = flag.String("fragment-prefix", "", "The path prefixed to each fragment file, usually only needed for tests")
)
// PlasaManifest represents the contents of a Plasa manifest file.
type PlasaManifest []PlasaFragment
// PlasaFragment is a single element of a Plasa manifest
type PlasaFragment struct {
// Dest is the (optional) destination to which a fragment should be packaged.
Dest string `json:"dest,omitempty"`
// File is the GN file label pointing at a fragment.
File string `json:"file,omitempty"`
// Kind is the fragment file content type.
Kind string `json:"kind"`
// Path is the file path on the local filesystem where the fragment file
// can be found and loaded from.
Path string `json:"path"`
}
// TestCoverageReport is the data model for test coverage reporting. It is
// written out in JSON output encoding.
type TestCoverageReport struct {
// Items contains all the test coverage report items added to this report.
Items []TestCoverageReportItem `json:"items"`
// seen contains the elements that have already been inserted. No duplication
// is allowed.
seen map[string]struct{} `json:",ignore"`
}
/// NewTestCoverageReport initializes a new test coverage report type.
func NewTestCoverageReport() TestCoverageReport {
return TestCoverageReport{
Items: nil,
seen: map[string]struct{}{},
}
}
// TestCoverageReportItem is a single item reported to the test coverage.
type TestCoverageReportItem struct {
// Name is the fully qualified name of the report item, such as "::ns::Foo".
// The exact format depends on Kind. It is assumed that the Name is unique within a Kind.
Name string `json:"name"`
// The Kind of the fully qualified name. For example, it could be "cc_api"
// or "fidl_api".
Kind string `json:"kind"`
}
const (
// kindCCAPI denotes a C++ API element.
kindCCAPI = "api_cc"
// kindFIDLAPI denotes a FIDL API element.
kindFIDLAPI = "api_fidl"
// kindFIDLProtocolMember is a kind of a FIDL API element, which is in itself a method on a protocol.
kindFIDLProtocolMember = "protocol/member"
)
func readManifest(r io.Reader) (PlasaManifest, error) {
var m PlasaManifest
d := json.NewDecoder(r)
d.DisallowUnknownFields()
if err := d.Decode(&m); err != nil {
return m, fmt.Errorf("while decoding plasa manifest: %w", err)
}
return m, nil
}
// pathKind is a pair of path and kind.
type pathKind struct {
path string
kind string
}
func pathKinds(m PlasaManifest) []pathKind {
var ret []pathKind
for _, f := range m {
s := path.Join(*fragmentPrefix, f.Path)
ret = append(ret, pathKind{path: s, kind: f.Kind})
}
return ret
}
// addCCAPIReport adds a C++ API report to the test coverage report.
func (m *TestCoverageReport) addCCAPIReport(r model.Report) {
for _, i := range r.Items {
if (i.Kind != model.ReportKindMethod) && (i.Kind != model.ReportKindFunction) {
// Only collect methods and functions, and nothing else.
continue
}
n := i.Name
if _, ok := m.seen[n]; ok {
// Skip seen elements.
continue
}
ci := TestCoverageReportItem{
Name: n,
Kind: kindCCAPI,
}
m.Items = append(m.Items, ci)
m.seen[n] = struct{}{}
}
}
// addFIDLAPIReport adds a FIDL API report to the test coverage report.
func (m *TestCoverageReport) addFIDLAPIReport(r []summarize.ElementStr) {
for _, i := range r {
n := string(i.Name)
if _, ok := m.seen[n]; ok {
// Skip seen elements.
continue
}
if i.Kind != kindFIDLProtocolMember {
// Skip non-methods
continue
}
ci := TestCoverageReportItem{
Name: n,
Kind: kindFIDLAPI,
}
m.Items = append(m.Items, ci)
m.seen[n] = struct{}{}
}
}
func (m TestCoverageReport) writeTo(w io.Writer) error {
sort.SliceStable(m.Items, func(i, j int) bool {
return m.Items[i].Name < m.Items[j].Name
})
e := json.NewEncoder(w)
e.SetEscapeHTML(false)
e.SetIndent("", " ")
if err := e.Encode(m); err != nil {
return fmt.Errorf("could not encode as JSON: %w", err)
}
return nil
}
func filter(m io.Reader, w io.Writer) error {
p, err := readManifest(m)
if err != nil {
return fmt.Errorf("could not read manifest: %v: %w", plasaManifestFile, err)
}
pks := pathKinds(p)
out := NewTestCoverageReport()
for _, pk := range pks {
pf, err := os.Open(pk.path)
if err != nil {
return fmt.Errorf("could not read fragment: %v: %w", pk.path, err)
}
switch pk.kind {
case kindCCAPI:
r, err := model.ReadReportJSON(pf)
if err != nil {
return fmt.Errorf("could not parse C++ API fragment: %+v: %w", pk.path, err)
}
out.addCCAPIReport(r)
case kindFIDLAPI:
m, err := summarize.LoadSummariesJSON(pf)
if err != nil {
return fmt.Errorf("could not parse FIDL API fragment: %+v: %w", pk.path, err)
}
out.addFIDLAPIReport(m[0])
default:
panic(fmt.Sprintf("unsupported API kind: %v", pk.kind))
}
}
if err := out.writeTo(w); err != nil {
return fmt.Errorf("while writing test coverage report: %w", err)
}
return nil
}
func run(plasaManifestFile, output string) error {
if plasaManifestFile == "" {
return fmt.Errorf("missing flag: --plasa-manifest-file=...")
}
m, err := os.Open(plasaManifestFile)
if err != nil {
return fmt.Errorf("could not open: %v: %w", plasaManifestFile, err)
}
defer m.Close()
var w io.WriteCloser
if output == "" {
w = os.Stdout
} else {
w, err = os.Create(output)
if err != nil {
return fmt.Errorf("could not open output: %v: %w", output, err)
}
defer w.Close()
}
return filter(m, w)
}
func main() {
flag.Parse()
if err := run(*plasaManifestFile, *output); err != nil {
fmt.Fprintf(os.Stderr, "plasa_test_coverage_report/main.run(): %v\n", err)
os.Exit(-1)
}
}