blob: a6f8c00b896ee8f18540b55eaba7c911d035d732 [file] [log] [blame]
// Copyright 2019 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.
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"strconv"
"strings"
"testing"
)
// newNodeWithSize creates a node with p as name and a specified size.
func newNodeWithSize(p string, size int64) *Node {
n := newNode(p)
n.size = size
return n
}
// setChildren sets the children of the given node.
func withSetChildren(n *Node, children map[string]*Node) *Node {
n.children = children
return n
}
func Test_processBlobsJSON(t *testing.T) {
tests := []struct {
name string
file io.Reader
expected map[string]int64
}{
{
"One Line", strings.NewReader(`[{"source_path":"","merkle": "foo","bytes":0,"size":0}]`), map[string]int64{"foo": 0},
},
{
"Two Lines", strings.NewReader(`[{"source_path":"","merkle": "foo","bytes":0,"size":1},{"source_path":"","merkle": "bar","bytes":0,"size":2}]`), map[string]int64{"foo": 1, "bar": 2},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
m, err := processBlobsJSON(test.file)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(m, test.expected) {
t.Fatalf("processBlobsJSON(%s) = %+v; expect %+v", test.file, m, test.expected)
}
})
}
}
func Test_processBlobsManifest(t *testing.T) {
tests := []struct {
name string
blobMap map[string]*Blob
sizeMap map[string]int64
fileName string
file io.Reader
expectedPackage []string
expectedBlobMap map[string]*Blob
}{
{
"Adding New Blob To Empty Map",
map[string]*Blob{},
map[string]int64{"hash": 1},
"fileFoo",
strings.NewReader("hash=foo"),
[]string{},
map[string]*Blob{"hash": {dep: []string{"fileFoo"}, size: 1, name: "foo", hash: "hash"}},
},
{
"Adding New Meta Far To Empty Map",
map[string]*Blob{},
map[string]int64{"hash": 1},
"fileFoo",
strings.NewReader("hash=meta.far"),
[]string{"meta.far"},
map[string]*Blob{"hash": {dep: []string{"fileFoo"}, size: 1, name: "meta.far", hash: "hash"}},
},
{
"Adding A Blob To A Map With That Blob",
map[string]*Blob{"hash": {dep: []string{"foo"}, size: 1, name: "foo", hash: "hash"}},
map[string]int64{"hash": 1},
"fileFoo",
strings.NewReader("hash=foo"),
[]string{},
map[string]*Blob{"hash": {dep: []string{"foo", "fileFoo"}, size: 1, name: "foo", hash: "hash"}},
},
{
"Adding A Meta Far To A Map With That Meta Far",
map[string]*Blob{"hash": {dep: []string{"foo"}, size: 1, name: "meta.far", hash: "hash"}},
map[string]int64{"hash": 1},
"fileFoo",
strings.NewReader("hash=meta.far"),
[]string{},
map[string]*Blob{"hash": {dep: []string{"foo", "fileFoo"}, size: 1, name: "meta.far", hash: "hash"}},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if p := parseBlobsManifest(test.blobMap, test.sizeMap, test.fileName, test.file); !reflect.DeepEqual(p, test.expectedPackage) {
t.Fatalf("processBlobsManifest(%+v, %+v, %s, %+v) = %+v; expect %+v", test.blobMap, test.sizeMap, test.fileName, test.file, p, test.expectedPackage)
}
if !reflect.DeepEqual(test.blobMap["hash"], test.expectedBlobMap["hash"]) {
t.Fatalf("blob map: %+v; expect %+v", test.blobMap["hash"], test.expectedBlobMap["hash"])
}
})
}
}
func Test_processBlobs(t *testing.T) {
tests := []struct {
name string
blobMap map[string]*Blob
icuDataMap map[string]*Node
distributedShlibsMap map[string]*Node
blobs []BlobFromJSON
expectedBlobMap map[string]*Blob
expectedSize int64
}{
{
"Adding Asset Blob",
map[string]*Blob{"hash": {size: 1}},
map[string]*Node{"test.asset": {fullPath: "test.asset", size: 0, copies: 1, children: map[string]*Node{}}},
map[string]*Node{"lib/ld.so.1": {fullPath: "lib/ld.so.1", size: 0, copies: 1, children: map[string]*Node{}}},
[]BlobFromJSON{{Path: "test.asset", Merkle: "hash"}},
map[string]*Blob{},
1,
},
{
"Adding Non-asset Blob",
map[string]*Blob{"hash": {size: 1, dep: []string{"not used"}}},
map[string]*Node{"test.asset": {fullPath: "test.asset", size: 0, copies: 1, children: map[string]*Node{}}},
map[string]*Node{"lib/ld.so.1": {fullPath: "lib/ld.so.1", size: 0, copies: 1, children: map[string]*Node{}}},
[]BlobFromJSON{{Path: "test.notasset", Merkle: "hash"}},
map[string]*Blob{"hash": {size: 1, dep: []string{"not used"}}},
0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
st := processingState{
test.blobMap,
test.icuDataMap,
test.distributedShlibsMap,
newDummyNode(),
}
parseBlobsJSON(&st, test.blobs, "", "")
if !reflect.DeepEqual(st.blobMap, test.expectedBlobMap) {
t.Fatalf("blob map: %v; expect %v", test.blobMap, test.expectedBlobMap)
}
var totalIcuDataSize int64
for _, node := range test.icuDataMap {
totalIcuDataSize += node.size
}
if totalIcuDataSize != test.expectedSize {
t.Fatalf("ICU Data size: %d; expect %d", totalIcuDataSize, test.expectedSize)
}
})
}
}
func Test_processBlobsJSON_blobLookup(t *testing.T) {
tests := []struct {
name string
pkgPath string
blobMap map[string]*Blob
blob BlobFromJSON
expectedPathInTree string
}{
{
"Adding non config-data blob",
"path/to/pkg/non-config-data",
map[string]*Blob{"hash": {size: 1, dep: []string{"not used"}}},
BlobFromJSON{Path: "data/test/foo.txt", Merkle: "hash"},
"path/to/pkg/non-config-data",
},
{
"Adding config-data blob meta far",
"path/to/pkg/config-data",
map[string]*Blob{"hash": {size: 1, dep: []string{"not used"}}},
BlobFromJSON{Path: "meta/", Merkle: "hash"},
"path/to/pkg/config-data",
},
{
"Adding config-data blob",
"path/to/pkg/config-data",
map[string]*Blob{"hash": {size: 1, dep: []string{"not used"}}},
BlobFromJSON{Path: "data/test/foo.txt", Merkle: "hash"},
"path/to/pkg/config-data/test/foo.txt",
},
}
var dummyMap map[string]*Node
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
root := newDummyNode()
st := processingState{
test.blobMap,
dummyMap,
dummyMap,
root,
}
parseBlobsJSON(&st, []BlobFromJSON{test.blob}, test.pkgPath, "")
expectedNode := root.detachByPath(test.expectedPathInTree)
if expectedNode == nil {
t.Fatalf("tree.detachByPath(%s) returns nil; expect to find a node", test.expectedPathInTree)
}
expectedSize := test.blobMap[test.blob.Merkle].size
if expectedNode.size != expectedSize {
t.Fatalf("tree.detachByPath(%s).size returns %d; expect %d", test.expectedPathInTree, expectedNode.size, expectedSize)
}
})
}
}
func Test_nodeAdd(t *testing.T) {
root := newDummyNode()
if root.size != 0 {
t.Fatalf("the size of the root node is %d; expect 0", root.size)
}
testBlob := Blob{
dep: []string{"not used"},
size: 10,
}
// Test adding a single node
root.add("foo", &testBlob)
if root.size != 10 {
t.Fatalf("the size of the root node is %d; expect 10", root.size)
}
child := root.detachByPath("foo")
if child == nil {
t.Fatal("foo is not added as the child of the root")
}
if child.size != 10 {
t.Fatalf("the size of the foo node (root's child) is %d; expect 10", child.size)
}
// Test adding a node that shares a common path
root.add("foo/bar", &testBlob)
if root.size != 10 {
t.Fatalf("the size of the root node is %d; expect 10", root.size)
}
grandchild := root.detachByPath("foo/bar")
if grandchild == nil {
t.Fatal("bar is not added as the grandchild of the root")
}
if child.size != 10 {
t.Fatalf("the size of the foo node (root's child) is %d; expect 10", child.size)
}
if grandchild.size != 10 {
t.Fatalf("the size of the bar node (root's grandchild) is %d; expect 10", grandchild.size)
}
// Test adding a node with .meta suffix
root.add("foo/update.meta", &testBlob)
if root.size != 10 {
t.Fatalf("the size of the root node is %d; expect 10", root.size)
}
update := root.detachByPath("foo/update")
if update == nil {
t.Fatal("update.meta is not added as the child of the root with the name 'update'")
}
if child.size != 10 {
t.Fatalf("the size of the foo node (root's child) is %d; expect 10", child.size)
}
if update.size != 10 {
t.Fatalf("the size of the update node (root's grandchild, bar's sibling) is %d; expect 10", update.size)
}
}
func Test_nodeFind(t *testing.T) {
tests := []struct {
name string
path string
expected *Node
}{
{
"Find Existing Node",
"foo",
&Node{"foo", 10, 0, nil, make(map[string]*Node), nil},
},
{
"Find Nonexistent Node",
"NONEXISTENT",
nil,
},
}
root := newDummyNode()
root.add("foo", &Blob{dep: []string{"not used"}, size: 10})
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if node := root.detachByPath(test.path); !reflect.DeepEqual(node, test.expected) {
t.Fatalf("node.detachByPath(%s) = %+v; expect %+v", test.path, node, test.expected)
}
})
}
}
func Test_processInput(t *testing.T) {
const singleBlobSize = 4096
fooSrcRelPath := "foo.src"
input := SizeLimits{
ICUDataLimit: json.Number("1"),
CoreLimit: json.Number("1"),
ICUData: []string{"icudtl.dat"},
Components: []Component{
{
Component: "foo",
Limit: json.Number(strconv.Itoa(singleBlobSize)),
Src: []string{"foo-pkg"},
},
},
}
buildDir := t.TempDir()
pkgDir := path.Join("obj", "foo-pkg")
if err := os.MkdirAll(path.Join(buildDir, pkgDir), 0o700); err != nil {
t.Fatalf("Failed to create package dir: %v", err)
}
blobsJSONF, err := os.Create(path.Join(buildDir, pkgDir, BlobsJSON))
if err != nil {
t.Fatalf("Failed to create %s: %v", BlobsJSON, err)
}
metaFarRelPath := path.Join(pkgDir, MetaFar)
blobs := []BlobFromJSON{
{
Merkle: "deadbeef",
Path: "meta/",
SourcePath: metaFarRelPath,
},
{
Merkle: "abc123",
Path: fooSrcRelPath,
SourcePath: fooSrcRelPath,
},
}
blobsJSONBytes, err := json.Marshal(blobs)
if err != nil {
t.Fatalf("Failed to marshal JSON for %s: %v", BlobsJSON, err)
}
if _, err := blobsJSONF.Write(blobsJSONBytes); err != nil {
t.Fatalf("Failed to write %s: %v", BlobsJSON, err)
}
blobsJSONF.Close()
blobManifestRelPath := path.Join(pkgDir, "blobs.manifest")
blobManifestF, err := os.Create(path.Join(buildDir, blobManifestRelPath))
if err != nil {
t.Fatalf("Failed to create blob manifest file: %v", err)
}
blobManifest := fmt.Sprintf("deadbeef=%s\nabc123=%s\n", metaFarRelPath, fooSrcRelPath)
if _, err := blobManifestF.Write([]byte(blobManifest)); err != nil {
t.Fatalf("Failed to write blob manifest: %v", err)
}
blobManifestF.Close()
blobListRelPath := "blob.manifest.list"
blobListF, err := os.Create(path.Join(buildDir, blobListRelPath))
if err != nil {
t.Fatalf("Failed to create blob list file: %v", err)
}
if _, err := blobListF.Write([]byte(blobManifestRelPath)); err != nil {
t.Fatalf("Failed to write blob list: %v", err)
}
blobListF.Close()
blobSizeRelPath := "blobs.json"
blobSizeF, err := os.Create(path.Join(buildDir, blobSizeRelPath))
if err != nil {
t.Fatalf("Failed to create blob size file: %v", err)
}
if _, err := blobSizeF.Write([]byte(fmt.Sprintf(`[{"source_path":"","merkle":"deadbeef","bytes":0,"size":%d},{"source_path":"","merkle":"abc123","bytes":0,"size":%d}]\n`, singleBlobSize, singleBlobSize))); err != nil {
t.Fatalf("Failed to write blob sizes: %v", err)
}
blobSizeF.Close()
sizes := parseSizeLimits(&input, buildDir, blobListRelPath, blobSizeRelPath)
fooSize, ok := sizes["foo"]
if !ok {
t.Fatalf("Failed to find foo in sizes: %v", sizes)
}
if fooSize.Size != int64(2*singleBlobSize) {
t.Fatalf("Unexpected size for component foo: %v", fooSize)
}
// Both the budget-only and full report should report going over-budget.
budgetOnlyReportOverBudget, _ := generateReport(sizes, true, false, singleBlobSize*1024)
fullReportOverBudget, _ := generateReport(sizes, false, false, singleBlobSize*1024)
ignorePerComponentBudgetOverBudget, _ := generateReport(sizes, false, true, singleBlobSize*1024)
if !budgetOnlyReportOverBudget {
t.Fatalf("The budget-only report is expected to report going overbudget.")
}
if !fullReportOverBudget {
t.Fatalf("The full report is expected to report going overbudget.")
}
if ignorePerComponentBudgetOverBudget {
t.Fatalf("Ignoring per-component budget should not cause use to go overbudget.")
}
}
func Test_writeOutputSizes(t *testing.T) {
// Ensure that the output conforms to the schema
// documented here:
// https://chromium.googlesource.com/infra/gerrit-plugins/binary-size/+/HEAD/README.md
sizes := map[string]*ComponentSize{
"a": {
Size: 1,
Budget: 2,
nodes: []*Node{newNode("a node")},
},
"b": {
Size: 2,
Budget: 2,
nodes: []*Node{newNode("b node")},
},
}
tmpDir := t.TempDir()
outPath := filepath.Join(tmpDir, "sizes.json")
if err := writeOutputSizes(sizes, outPath); err != nil {
t.Fatalf("writeOutputSizes failed: %v", err)
}
wroteBytes, err := ioutil.ReadFile(outPath)
if err != nil {
t.Fatalf("ioutil.ReadFile() failed; %v", err)
}
var unmarshalled map[string]interface{}
if err := json.Unmarshal(wroteBytes, &unmarshalled); err != nil {
t.Errorf("json.Unmarshal() failed: %v", err)
}
if val, ok := unmarshalled["a"]; !ok || val.(float64) != 1 {
t.Fatalf("json size output missing expected key/value entry for binary")
}
if val, ok := unmarshalled["a.budget"]; !ok || val.(float64) != 2 {
t.Fatalf("json size output missing expected key/value entry for budget")
}
}
func TestCustomDisplay(t *testing.T) {
tests := []struct {
node *Node
level int
expected string
}{
{
newNodeWithSize("hello", 3*1024*1024),
3,
" hello | 3.00 MiB \n",
},
{
newNodeWithDisplay("hashAAAA", displayAsBlob),
2,
" Blob ID hashAAAA (0 reuses):\n",
},
{
newNodeWithDisplay("something_not_a_meta", displayAsMeta),
2,
" something_not_a_meta\n",
},
{
newNodeWithDisplay("/some/path/a_package.meta/something_else", displayAsMeta),
2,
" a_package\n",
},
{
withSetChildren(
newNodeWithDisplay("hashAAAA", displayAsBlob),
map[string]*Node{
"1": newNodeWithDisplay("metaBBBB", displayAsMeta),
}),
2,
" Blob ID hashAAAA (1 reuses):\n metaBBBB\n",
},
}
for _, test := range tests {
test := test
t.Run(test.expected, func(t *testing.T) {
t.Parallel()
actual := test.node.storageBreakdown(test.level)
if test.expected != actual {
t.Errorf("custom display mismatch:\nexpected: '%v'\n(bytes): %v\nactual: '%v'\n(bytes): %v",
test.expected,
[]byte(test.expected),
actual,
[]byte(actual),
)
}
})
}
}