blob: 36e995171b4fdf55ca4aac279830beaa684adf34 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//go:build !windows
// +build !windows
package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"debug/elf"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/fs"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"infra/cros/internal/gs"
)
type mockClient struct {
expectedResponses map[string]string
}
func (c *mockClient) Do(req *http.Request) (*http.Response, error) {
// if the request matches a string in the expected responses then generate a
// response and return it. If it isn't apart of the map then return an error
// and a nil response.
if val, ok := c.expectedResponses[req.URL.String()]; ok {
response := http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(val))}
return &response, nil
} else {
return nil, fmt.Errorf("error: %s is not an expected url", req.URL.String())
}
}
func initCrashConnectionMock(mockURL, mockKey string, responseMap map[string]string) crashConnectionInfo {
return crashConnectionInfo{
url: mockURL,
key: mockKey,
client: &mockClient{expectedResponses: responseMap},
}
}
func buildFakeELFWithNote(buildID string) []byte {
var buf bytes.Buffer
decoded, err := hex.DecodeString(buildID)
if err != nil {
return nil
}
idLen := len(decoded)
sectionHeaderOffset := 52 + 1 + 10 + 19 + 16 + idLen
buf.Grow(sectionHeaderOffset + 2*40)
h := elf.Header32{
Ident: [16]byte{0x7F, 'E', 'L', 'F', 0x1, 0x1, 0x1},
Type: 1,
Machine: 3,
Version: 1,
Shoff: uint32(sectionHeaderOffset),
Ehsize: 0x34,
Shentsize: 0x28,
Shnum: 3,
Shstrndx: 1,
}
binary.Write(&buf, binary.LittleEndian, h)
buf.Write([]byte{0})
buf.Write([]byte(".shstrtab\x00"))
buf.Write([]byte(".note.gnu.build-id\x00"))
noteStart := buf.Len()
binary.Write(&buf, binary.LittleEndian, []uint32{0x4, uint32(idLen), 3})
buf.Write([]byte("GNU\x00"))
buf.Write(decoded)
noteEnd := buf.Len()
binary.Write(&buf, binary.LittleEndian, elf.Section32{Name: 0})
binary.Write(&buf, binary.LittleEndian, elf.Section32{
Name: 1,
Type: uint32(elf.SHT_STRTAB),
Off: 52,
Size: 30,
Addralign: 0x1,
})
binary.Write(&buf, binary.LittleEndian, elf.Section32{
Name: 0xb,
Type: uint32(elf.SHT_NOTE),
Flags: uint32(elf.SHF_ALLOC),
Addr: uint32(noteStart),
Off: uint32(noteStart),
Addralign: 0x4,
Size: uint32(noteEnd - noteStart),
})
return buf.Bytes()
}
// TestGetBuildId ensures that we can extract a build ID from a splitdebug
// file.
func TestGetBuildId(t *testing.T) {
const expected = "BFCF6FA6CCBDEF00501810DE869C8A2F40ABC321"
testDir, err := ioutil.TempDir("", "getBuildIdTest")
if err != nil {
t.Error("error: " + err.Error())
}
defer os.RemoveAll(testDir)
mockPath := filepath.Join(testDir, "fake.debug")
err = ioutil.WriteFile(mockPath, buildFakeELFWithNote(expected), 0644)
if err != nil {
t.Error("error: " + err.Error())
}
actual, err := getBuildId(mockPath)
if err != nil {
t.Error("error: " + err.Error())
}
if actual != expected {
t.Error(actual + " doesn't match expected ID " + expected)
}
}
// TestDownloadZippedSymbols ensures that we are fetching from the correct service and
// handling the response appropriately.
func TestDownloadZippedSymbols(t *testing.T) {
gsPath := "gs://some-debug-symbol/degbug.tgz"
expectedDownloads := map[string][]byte{
gsPath: []byte("hello world"),
}
fakeClient := &gs.FakeClient{
T: t,
ExpectedDownloads: expectedDownloads,
}
tarballDir, err := ioutil.TempDir("", "tarball")
if err != nil {
t.Error("error: " + err.Error())
}
defer os.RemoveAll(tarballDir)
tgzPath, err := downloadZippedSymbols(fakeClient, gsPath, tarballDir)
if err != nil {
t.Error("error: " + err.Error())
}
if _, err := os.Stat(tgzPath); os.IsNotExist(err) {
t.Error("error: " + err.Error())
}
}
// TestUnzipTgz confirms that we can properly unzip a given tgz file.
func TestUnzipSymbols(t *testing.T) {
targetString := "gzip test"
// Create temp dir to work in.
testDir, err := ioutil.TempDir("", "tarballTest")
if err != nil {
t.Error("error: " + err.Error())
}
defer os.RemoveAll(testDir)
// Generate file information.
outputFilePath := filepath.Join(testDir, "test.tar")
inputFilePath := filepath.Join(testDir, "test.tgz")
inputFile, err := os.Create(inputFilePath)
if err != nil {
t.Error("error: " + err.Error())
}
defer inputFile.Close()
// Create a mock .tgz to test unzipping.
zipWriter := gzip.NewWriter(inputFile)
zipWriter.Name = "test.tgz"
zipWriter.Comment = "hello world"
_, err = zipWriter.Write([]byte(targetString))
if err != nil {
t.Error("error: " + err.Error())
}
err = zipWriter.Close()
if err != nil {
t.Error("error: " + err.Error())
}
err = unzipSymbols(inputFilePath, outputFilePath)
if err != nil {
t.Error("error: " + err.Error())
}
// Check if the output file was created.
if _, err := os.Stat(outputFilePath); os.IsNotExist(err) {
t.Error("error: " + err.Error())
}
outputFile, err := os.Open(outputFilePath)
if err != nil {
t.Error("error: " + err.Error())
}
defer outputFile.Close()
output, err := io.ReadAll(outputFile)
if err != nil {
t.Error("error: " + err.Error())
}
// Verify contents unzipped correctly.
if string(output) != targetString {
t.Errorf("error: expected %s got %s", targetString, string(output))
}
}
// TestUnpackTarball confirms that we can properly unpack a given tarball and
// return filepaths to it's contents. Basic testing pulled from
// https://pkg.go.dev/archive/tar#pkg-overview.
func TestUnpackTarball(t *testing.T) {
// Create working directory and tarball.
testDir, err := ioutil.TempDir("", "tarballTest")
if err != nil {
t.Error("error: " + err.Error())
}
debugSymbolsDir, err := ioutil.TempDir(testDir, "symbols")
if err != nil {
t.Error("error: " + err.Error())
}
defer os.RemoveAll(testDir)
// Generate file information.
tarPath := filepath.Join(testDir, "test.tar")
inputFile, err := os.Create(tarPath)
if err != nil {
t.Error("error: " + err.Error())
}
tarWriter := tar.NewWriter(inputFile)
// Struct for file info
type file struct {
name, body string
modeType fs.FileMode
}
// Create an array holding some basic info to build headers. Contains regular
// files and directories.
files := []file{
{"/test1.so.sym", "MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F40 blkid", 0600},
{"./test2.so.sym", "MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F41 blkid", 0600},
{"b/c", "", fs.ModeDir},
// Different Debug ID as other test1.so.sym so should be included.
{"b/c/test1.so.sym", "MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F42 blkid", 0600},
{"../test3.so.sym", "MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F43 blkid", 0600},
{"a/b/c/d/", "", fs.ModeDir},
{"./test4.so.sym", "MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F44 blkid", 0600},
// Same Debug ID as previous file so should not be included.
{"a/b/c/d/test4.so.sym", "MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F44 blkid", 0600},
{"a/shouldntadd.txt", "not a symbol file", 0600},
}
// List of files we expect to see return after the test call.
expectedSymbolFiles := map[string]bool{
debugSymbolsDir + "/test1.so.sym": false,
debugSymbolsDir + "/test1.so.sym-1": false,
debugSymbolsDir + "/test2.so.sym": false,
debugSymbolsDir + "/test3.so.sym": false,
debugSymbolsDir + "/test4.so.sym": false,
}
// Write the mock files to the tarball.
for _, file := range files {
hdr := &tar.Header{
Name: file.name,
Mode: int64(file.modeType),
Size: int64(len(file.body)),
}
if err := tarWriter.WriteHeader(hdr); err != nil {
t.Error("error: " + err.Error())
}
if file.modeType == 0600 {
if _, err := tarWriter.Write([]byte(file.body)); err != nil {
t.Error("error: " + err.Error())
}
}
}
if err := tarWriter.Close(); err != nil {
t.Error("error: " + err.Error())
}
// Call the Function
symbolPaths, err := unpackTarball(tarPath, debugSymbolsDir)
if err != nil {
t.Error("error: " + err.Error())
}
if symbolPaths == nil || len(symbolPaths) <= 0 {
t.Error("error: Empty list of paths returned")
}
// Verify that we received a list pointing to all the expected files and no
// others.
for _, path := range symbolPaths {
if val, ok := expectedSymbolFiles[path]; ok {
if val {
t.Error("error: symbol file appeared multiple times in function return")
}
expectedSymbolFiles[path] = true
} else {
t.Errorf("error: unexpected symbol file returned %s", path)
}
}
}
// TestUnpackSplitdebugTarballs confirms that we can properly unpack a given tarball and
// return filepaths to it's contents. Basic testing pulled from
// https://pkg.go.dev/archive/tar#pkg-overview.
func TestUnpackSplitdebugTarballs(t *testing.T) {
// Create working directory and tarball.
testDir, err := ioutil.TempDir("", "splitdebugTarballTest")
if err != nil {
t.Error("error: " + err.Error())
}
debugSymbolsDir, err := ioutil.TempDir(testDir, "symbols")
if err != nil {
t.Error("error: " + err.Error())
}
defer os.RemoveAll(testDir)
// Struct for symbolFile info
type symbolFile struct {
name, body string
modeType fs.FileMode
}
// The debug Id is a byte swapped and truncated version of the build Id.
const buildId1 = "F4F6FA6CCBDEF455039C8DE869C8A2F4AB"
const debugId1 = "6CFAF6F4DECB55F4039C8DE869C8A2F40"
const buildId1Dup = "F4F6FA6CCBDEF455039C8DE869C8A2F6AB"
const debugId1Dup = "6CFAF6F4DECB55F4039C8DE869C8A2F60"
const buildId2 = "F4F6FA6CCBDEF455039C8DE869C8A2F5AC"
const debugId2 = "6CFAF6F4DECB55F4039C8DE869C8A2F50"
const buildId3 = "F4F6FA6CCBDEF455039C8DE869C8A2F7AD"
const debugId3 = "6CFAF6F4DECB55F4039C8DE869C8A2F70"
const buildId4 = "F4F6FA6CCBDEF455039C8DE869C8A2F8AE"
const debugId4 = "6CFAF6F4DECB55F4039C8DE869C8A2F80"
// Create an array holding some basic info to build headers. Contains regular
// files and directories.
symbolFiles := []symbolFile{
{"/test1.so.sym", "MODULE Linux arm " + debugId1 + " test1.so\nINFO CODE_ID " + buildId1, 0600},
{"./test2.so.sym", "MODULE Linux arm " + debugId2 + " test2.so\nINFO CODE_ID " + buildId2, 0600},
{"b/c", "", fs.ModeDir},
// Different Debug ID as other test1.so.sym so should be included.
{"b/c/test1.so.sym", "MODULE Linux arm " + debugId1Dup + " test1.so\nINFO CODE_ID " + buildId1Dup, 0600},
{"../test3.so.1.sym", "MODULE Linux arm " + debugId3 + " test3.so.1\nINFO CODE_ID " + buildId3, 0600},
{"a/b/c/d/", "", fs.ModeDir},
{"./test4.so.sym", "MODULE Linux arm " + debugId4 + " test4.so\nINFO CODE_ID " + buildId4, 0600},
// Same Debug ID as previous file so should not be included.
{"a/b/c/d/test4.so.sym", "MODULE Linux arm " + debugId4 + " test4.so\nINFO CODE_ID " + buildId4, 0600},
{"a/shouldntadd.txt", "not a symbol file", 0600},
}
// List of files we expect to see return after the test call.
expectedSymbolFiles := map[string]bool{
debugSymbolsDir + "/" + debugId1 + "/test1.so.sym": false,
debugSymbolsDir + "/" + debugId1Dup + "/test1.so.sym": false,
debugSymbolsDir + "/" + debugId2 + "/test2.so.sym": false,
debugSymbolsDir + "/" + debugId3 + "/test3.so.1.sym": false,
debugSymbolsDir + "/" + debugId4 + "/test4.so.sym": false,
}
// Struct for file info
type splitdebugFile struct {
name string
body []byte
modeType fs.FileMode
}
// Create an array holding some basic info to build headers. Contains regular
// files and directories.
splitdebugFiles := []splitdebugFile{
{"/test1.so.debug", buildFakeELFWithNote(buildId1), 0600},
{"./test2.so.debug", buildFakeELFWithNote(buildId2), 0600},
{"b/c", nil, fs.ModeDir},
// Different Debug ID as other test1.so.debug so should be included.
{"b/c/test1.so.debug", buildFakeELFWithNote(buildId1Dup), 0600},
// Different name as breakpad symbol (test3.so.1.sym) should be included
{"../test3.so.1.0.5.debug", buildFakeELFWithNote(buildId3), 0600},
{"a/b/c/d/", nil, fs.ModeDir},
{"./test4.so.debug", buildFakeELFWithNote(buildId4), 0600},
// Same Debug ID as previous file so should not be included.
{"a/b/c/d/test4.so.debug", buildFakeELFWithNote(buildId4), 0600},
{"a/shouldntadd.txt", []byte("not a symbol file\x00"), 0600},
}
// Generate file information.
splitdebugTarPath := filepath.Join(testDir, "test-debug.tar")
splitdebugInputFile, err := os.Create(splitdebugTarPath)
if err != nil {
t.Error("error: " + err.Error())
}
splitdebugTarWriter := tar.NewWriter(splitdebugInputFile)
// Write the mock files to the tarball.
for _, file := range splitdebugFiles {
hdr := &tar.Header{
Name: file.name,
Mode: int64(file.modeType),
Size: int64(len(file.body)),
}
if err := splitdebugTarWriter.WriteHeader(hdr); err != nil {
t.Error("error: " + err.Error())
}
if file.modeType == 0600 {
if _, err := splitdebugTarWriter.Write(file.body); err != nil {
t.Error("error: " + err.Error())
}
}
}
if err := splitdebugTarWriter.Close(); err != nil {
t.Error("error: " + err.Error())
}
symbolTarPath := filepath.Join(testDir, "test-symbols.tar")
symbolInputFile, err := os.Create(symbolTarPath)
if err != nil {
t.Error("error: " + err.Error())
}
symbolTarWriter := tar.NewWriter(symbolInputFile)
// Write the mock files to the tarball.
for _, file := range symbolFiles {
hdr := &tar.Header{
Name: file.name,
Mode: int64(file.modeType),
Size: int64(len(file.body)),
}
if err := symbolTarWriter.WriteHeader(hdr); err != nil {
t.Error("error: " + err.Error())
}
if file.modeType == 0600 {
if _, err := symbolTarWriter.Write([]byte(file.body)); err != nil {
t.Error("error: " + err.Error())
}
}
}
if err := symbolTarWriter.Close(); err != nil {
t.Error("error: " + err.Error())
}
// Call the function under test.
breakpadPaths, err := unpackSplitdebugTarballs(splitdebugTarPath, symbolTarPath, debugSymbolsDir)
if err != nil {
t.Error("error: " + err.Error())
}
if breakpadPaths == nil || len(breakpadPaths) <= 0 {
t.Error("error: Empty list of breakpad paths returned")
}
// Verify that we received a list pointing to all the expected files and no
// others.
for _, path := range breakpadPaths {
if val, ok := expectedSymbolFiles[path]; ok {
if val {
t.Errorf("error: breakpad file %q appeared multiple times in function return", path)
}
// Verify that the breakpad file has an associated splitdebug file.
base, _ := strings.CutSuffix(path, ".sym")
debugFile := base
if _, err := os.Stat(debugFile); err != nil {
t.Errorf("error: breakpad file %q missing splitdebug file %q", path, debugFile)
}
expectedSymbolFiles[path] = true
} else {
t.Errorf("error: unexpected breakpad file returned %q", path)
}
}
for path, ok := range expectedSymbolFiles {
if !ok {
t.Errorf("error: breakpad file %q not found", path)
}
}
}
// TestGenerateConfigs validates that proper task configs are generated when a
// list of filepaths are given.
func TestGenerateConfigs(t *testing.T) {
// Init the mock files and verifying structures.
expectedTasks := map[taskConfig]bool{}
type responseInfo struct {
filename string
symbol string
status string
// Local path to write the file to. Used for dupe debug symbols
// (mimicking behavior in unpackTarball)
localPath string
}
mockResponses := []*responseInfo{
{
filename: "test1.so.sym",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "FOUND",
}, {
filename: "test1.so.sym",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F41",
status: "MISSING",
localPath: "test1.so.sym-1",
}, {
filename: "test2.so.sym",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "FOUND",
}, {
filename: "test3.so.sym",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "Missing",
},
{
filename: "test4.so.sym",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "MISSING",
},
{
filename: "test5.so.sym",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "STATUS_UNSPECIFIED",
},
{
filename: "test6.so.sym",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "STATUS_UNSPECIFIED",
},
}
// Make the expected request body.
responseBody := filterResponseBody{Pairs: []filterResponseStatusPair{}}
// Test for all 3 cases found in http://google3/net/crash/symbolcollector/symbol_collector.proto?l=19
for _, response := range mockResponses {
symbol := filterSymbolFileInfo{response.filename, response.symbol}
responseBody.Pairs = append(responseBody.Pairs, filterResponseStatusPair{SymbolId: symbol, Status: response.status})
}
mockResponseBody, err := json.Marshal(responseBody)
if err != nil {
t.Error("error: " + err.Error())
}
// Mock the symbol files locally.
testDir, err := ioutil.TempDir("", "configGenTest")
if err != nil {
t.Error("error: " + err.Error())
}
defer os.RemoveAll(testDir)
mockPaths := []string{}
for _, response := range mockResponses {
localPath := response.filename
if response.localPath != "" {
localPath = response.localPath
}
mockPath := filepath.Join(testDir, localPath)
err = ioutil.WriteFile(mockPath, []byte(fmt.Sprintf("MODULE Linux arm %s %s", response.symbol, filepath.Base(response.filename))), 0644)
if err != nil {
t.Error("error: " + err.Error())
}
task := taskConfig{mockPath, "BREAKPAD", response.filename, response.symbol, false, false}
if response.status != "FOUND" {
expectedTasks[task] = false
}
mockPaths = append(mockPaths, mockPath)
}
// Init global variables and
mockCrash := initCrashConnectionMock("google.com", "1234", map[string]string{"google.com/symbols:checkStatuses?key=1234": string(mockResponseBody)})
tasks, err := generateConfigs(context.Background(), mockPaths, 0, false, mockCrash)
if err != nil {
t.Error("error: " + err.Error())
}
// Check that returns aren't nil.
if tasks == nil {
t.Error("error: recieved tasks when nil was expected")
}
// Verify that we received a list pointing to all the expected files and no
// others.
for _, task := range tasks {
if val, ok := expectedTasks[task]; ok {
if val {
t.Errorf("error: task %v appeared multiple times in function return", task)
}
expectedTasks[task] = true
} else {
t.Errorf("error: unexpected task returned %+v", task)
}
}
for task, value := range expectedTasks {
if value == false {
t.Errorf("error: task for file %s never seen", task.debugFile)
}
}
}
// TestGenerateSplitdebugConfigs validates that proper task configs are generated when a
// list of filepaths are given.
func TestGenerateSplitdebugConfigs(t *testing.T) {
// Init the mock files and verifying structures.
expectedTasks := map[taskConfig]bool{}
type responseInfo struct {
filename string
symbol string
status string
breakpadPath string
splitdebugPath string
}
mockResponses := []*responseInfo{
{
filename: "test1.so",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "FOUND",
breakpadPath: "test1.so.sym",
splitdebugPath: "test1.so",
}, {
filename: "test2.so",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "FOUND",
breakpadPath: "test2.so.sym",
splitdebugPath: "test2.so",
}, {
filename: "test3.so",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "Missing",
breakpadPath: "test3.so.sym",
splitdebugPath: "test3.so",
},
{
filename: "test4.so.1",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "MISSING",
breakpadPath: "test4.so.1.sym",
splitdebugPath: "test4.so.1",
},
{
filename: "test5.so",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "STATUS_UNSPECIFIED",
breakpadPath: "test5.so.sym",
splitdebugPath: "test5.so",
},
{
filename: "test6.so",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "STATUS_UNSPECIFIED",
breakpadPath: "test6.so.sym",
splitdebugPath: "test6.so",
},
{
filename: "test7.so",
symbol: "F4F6FA6CCBDEF455039C8DE869C8A2F40",
status: "STATUS_UNSPECIFIED",
breakpadPath: "test7.so.sym",
// No splitdebugPath, no expected splitdebug upload task
},
}
// Make the expected request body.
responseBody := filterResponseBody{Pairs: []filterResponseStatusPair{}}
// Test for all 3 cases found in http://google3/net/crash/symbolcollector/symbol_collector.proto?l=19
for _, response := range mockResponses {
symbol := filterSymbolFileInfo{response.filename, response.symbol}
responseBody.Pairs = append(responseBody.Pairs, filterResponseStatusPair{SymbolId: symbol, Status: response.status})
}
mockResponseBody, err := json.Marshal(responseBody)
if err != nil {
t.Error("error: " + err.Error())
}
// Mock the symbol files locally.
testDir, err := ioutil.TempDir("", "configGenTest")
if err != nil {
t.Error("error: " + err.Error())
}
defer os.RemoveAll(testDir)
mockPaths := []string{}
for _, response := range mockResponses {
// Make a mock directory with the debug Id.
localDir := filepath.Join(testDir, response.symbol)
err := os.Mkdir(localDir, 0750)
if err != nil && !os.IsExist(err) {
t.Error("error: " + err.Error())
}
mockSplitdebugPath := filepath.Join(localDir, response.splitdebugPath)
// Create a mock splitdebug file.
if response.splitdebugPath != "" {
err = ioutil.WriteFile(mockSplitdebugPath, buildFakeELFWithNote(response.symbol), 0644)
if err != nil {
t.Error("error: " + err.Error())
}
}
// Create a mock breakpad file.
mockPath := filepath.Join(localDir, response.breakpadPath)
err = ioutil.WriteFile(mockPath, []byte(fmt.Sprintf("MODULE Linux arm %s %s", response.symbol, response.filename)), 0644)
if err != nil {
t.Error("error: " + err.Error())
}
// Generate two tasks.
task := taskConfig{mockPath, "BREAKPAD", response.filename, response.symbol, false, false}
splitdebugTask := taskConfig{mockSplitdebugPath, "ELF", response.filename, response.symbol, false, false}
if response.status != "FOUND" {
expectedTasks[task] = false
if response.splitdebugPath != "" {
expectedTasks[splitdebugTask] = false
}
}
mockPaths = append(mockPaths, mockPath)
}
// Init global variables and
mockCrash := initCrashConnectionMock("google.com", "1234", map[string]string{"google.com/symbols:checkStatuses?key=1234": string(mockResponseBody)})
tasks, err := generateSplitdebugConfigs(context.Background(), mockPaths, 0, false, mockCrash)
if err != nil {
t.Error("error: " + err.Error())
}
// Check that returns aren't nil.
if tasks == nil {
t.Error("error: recieved tasks when nil was expected")
}
// Verify that we received a list pointing to all the expected files and no
// others.
for _, task := range tasks {
if val, ok := expectedTasks[task]; ok {
if val {
t.Errorf("error: task %v appeared multiple times in function return", task)
}
expectedTasks[task] = true
} else {
t.Errorf("error: unexpected task returned %+v", task)
}
}
for task, value := range expectedTasks {
if value == false {
t.Errorf("error: task %+v never seen", task)
}
}
}
// TestUploadSymbols affirms that the worker design and retry model are valid.
func TestUploadSymbols(t *testing.T) {
// Create tasks and expected returns.
tasks := []taskConfig{
{"", "BREAKPAD", "test1.so.sym", "", false, false},
{"", "BREAKPAD", "test2.so.sym", "", false, false},
{"", "BREAKPAD", "test3.so.sym", "", false, false},
{"", "BREAKPAD", "test4.so.sym", "", false, false},
}
// Mock the symbol files locally.
testDir, err := ioutil.TempDir("", "uploadSymbolsTest")
if err != nil {
t.Error("error: " + err.Error())
}
defer os.RemoveAll(testDir)
// Write mock files locally.
for index, task := range tasks {
mockPath := filepath.Join(testDir, task.debugFile)
err = ioutil.WriteFile(mockPath, []byte("MODULE Linux arm F4F6FA6CCBDEF455039C8DE869C8A2F40 blkid"), 0644)
if err != nil {
t.Error("error: " + err.Error())
}
tasks[index].symbolPath = mockPath
}
// unique URL and key
mockURLKeyPair := crashUploadInformation{UploadUrl: "crashupload.com", UploadKey: "abc"}
mockCompleteResponse := crashSubmitResposne{Result: "OK"}
mockURLKeyPairJSON, err := json.Marshal(mockURLKeyPair)
if err != nil {
t.Error("error: could not marshal response")
}
mockCompleteResponseJSON, err := json.Marshal(mockCompleteResponse)
if err != nil {
t.Error("error: could not marshal response")
}
expectedResponses := map[string]string{
"google.com/uploads:create?key=1234": string(mockURLKeyPairJSON),
"crashupload.com": "",
"google.com/uploads/abc:complete?key=1234": string(mockCompleteResponseJSON),
}
crashMock := initCrashConnectionMock("google.com", "1234", expectedResponses)
retcode, err := uploadSymbols(tasks, 64, 2, false, crashMock)
if err != nil {
t.Error("error: " + err.Error())
}
if retcode != 0 {
t.Errorf("error: recieved non-zero retcode %d", retcode)
}
}
// TestCleanErrorMessage tests to make sure we won't accidentally expose the api
// key.
func TestCleanErrorMessage(t *testing.T) {
mockKey := "abcde12345!@#$"
mockErr := fmt.Errorf("could not connect to www.google.com/secret?key=%s&v1=1&v2=test", mockKey)
cleanedErrMessage := cleanErrorMessage(mockErr.Error(), mockKey)
if strings.Contains(cleanedErrMessage, mockKey) {
t.Error("error: secret key found in error response")
}
if !strings.Contains(cleanedErrMessage, "-HIDDEN-KEY-") {
t.Error("error: replaced key not found")
}
mockErr = fmt.Errorf("nil http repsonse was recieved")
cleanedErrMessage = cleanErrorMessage(mockErr.Error(), mockKey)
if strings.Contains(cleanedErrMessage, "-HIDDEN-KEY-") {
t.Error("error: non-existant key was fouind in error")
}
}