blob: d73688c81e2528807bc19d1fc2ea22d82efb16d0 [file] [log] [blame]
// Copyright 2014 The LUCI Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package local
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
"golang.org/x/net/context"
. "github.com/luci/luci-go/cipd/client/cipd/common"
. "github.com/smartystreets/goconvey/convey"
)
func normalizeJSON(s string) (string, error) {
// Round trip through default json marshaller to normalize indentation.
var x map[string]interface{}
if err := json.Unmarshal([]byte(s), &x); err != nil {
return "", err
}
blob, err := json.Marshal(x)
if err != nil {
return "", err
}
return string(blob), nil
}
func shouldBeSameJSONDict(actual interface{}, expected ...interface{}) string {
if len(expected) != 1 {
return "Too many argument for shouldBeSameJSONDict"
}
actualNorm, err := normalizeJSON(actual.(string))
if err != nil {
return err.Error()
}
expectedNorm, err := normalizeJSON(expected[0].(string))
if err != nil {
return err.Error()
}
return ShouldEqual(actualNorm, expectedNorm)
}
func TestPackageReading(t *testing.T) {
ctx := context.Background()
Convey("Open empty package works", t, func() {
// Build an empty package.
out := bytes.Buffer{}
err := BuildInstance(ctx, BuildInstanceOptions{
Output: &out,
PackageName: "testing",
CompressionLevel: 5,
})
So(err, ShouldBeNil)
// Open it.
inst, err := OpenInstance(ctx, bytes.NewReader(out.Bytes()), "", VerifyHash)
So(inst, ShouldNotBeNil)
So(err, ShouldBeNil)
So(inst.Pin(), ShouldResemble, Pin{"testing", "23f2c4900785ac8faa2f38e473925b840e574ccc"})
So(len(inst.Files()), ShouldEqual, 1)
// Contains single manifest file.
f := inst.Files()[0]
So(f.Name(), ShouldEqual, ".cipdpkg/manifest.json")
So(f.Executable(), ShouldBeFalse)
r, err := f.Open()
if r != nil {
defer r.Close()
}
So(err, ShouldBeNil)
manifest, err := ioutil.ReadAll(r)
So(err, ShouldBeNil)
goodManifest := `{
"format_version": "1",
"package_name": "testing"
}`
So(string(manifest), shouldBeSameJSONDict, goodManifest)
})
Convey("Open empty package with unexpected instance ID", t, func() {
// Build an empty package.
out := bytes.Buffer{}
err := BuildInstance(ctx, BuildInstanceOptions{
Output: &out,
PackageName: "testing",
CompressionLevel: 5,
})
So(err, ShouldBeNil)
// Attempt to open it, providing correct instance ID, should work.
source := bytes.NewReader(out.Bytes())
inst, err := OpenInstance(ctx, source, "23f2c4900785ac8faa2f38e473925b840e574ccc", VerifyHash)
So(err, ShouldBeNil)
So(inst, ShouldNotBeNil)
So(inst.Pin(), ShouldResemble, Pin{"testing", "23f2c4900785ac8faa2f38e473925b840e574ccc"})
// Attempt to open it, providing incorrect instance ID.
inst, err = OpenInstance(ctx, source, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", VerifyHash)
So(err, ShouldNotBeNil)
So(inst, ShouldBeNil)
// Open with incorrect instance ID, but skipping the verification..
inst, err = OpenInstance(ctx, source, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", SkipHashVerification)
So(err, ShouldBeNil)
So(inst, ShouldNotBeNil)
So(inst.Pin(), ShouldResemble, Pin{"testing", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})
})
Convey("OpenInstanceFile works", t, func() {
// Open temp file.
tempFile, err := ioutil.TempFile("", "cipdtest")
So(err, ShouldBeNil)
tempFilePath := tempFile.Name()
defer os.Remove(tempFilePath)
// Write empty package to it.
err = BuildInstance(ctx, BuildInstanceOptions{
Output: tempFile,
PackageName: "testing",
CompressionLevel: 5,
})
So(err, ShouldBeNil)
tempFile.Close()
// Read the package.
inst, closer, err := OpenInstanceFile(ctx, tempFilePath, "", VerifyHash)
So(err, ShouldBeNil)
defer closer()
So(inst, ShouldNotBeNil)
})
Convey("ExtractInstance works", t, func() {
// Add a bunch of files to a package.
out := bytes.Buffer{}
inFiles := []File{
NewTestFile("testing/qwerty", "12345", false),
NewTestFile("abc", "duh", true),
NewTestFile("bad_dir/pkg/0/description.json", "{}", false),
NewTestSymlink("rel_symlink", "abc"),
NewTestSymlink("abs_symlink", "/abc/def"),
}
if runtime.GOOS == "windows" {
inFiles = append(inFiles,
NewWinTestFile("secret", "ninja", WinAttrHidden),
NewWinTestFile("system", "machine", WinAttrSystem),
)
}
err := BuildInstance(ctx, BuildInstanceOptions{
Input: inFiles,
Output: &out,
PackageName: "testing",
VersionFile: "subpath/version.json",
CompressionLevel: 5,
})
So(err, ShouldBeNil)
// Extract files.
inst, err := OpenInstance(ctx, bytes.NewReader(out.Bytes()), "", VerifyHash)
So(err, ShouldBeNil)
dest := &testDestination{}
err = ExtractInstance(ctx, inst, dest, func(f File) bool {
return strings.HasPrefix(f.Name(), "bad_dir/")
})
So(err, ShouldBeNil)
So(dest.beginCalls, ShouldEqual, 1)
So(dest.endCalls, ShouldEqual, 1)
// Verify file list, file data and flags are correct.
names := []string{}
for _, f := range dest.files {
names = append(names, f.name)
}
if runtime.GOOS != "windows" {
So(names, ShouldResemble, []string{
"testing/qwerty",
"abc",
"rel_symlink",
"abs_symlink",
"subpath/version.json",
".cipdpkg/manifest.json",
})
} else {
So(names, ShouldResemble, []string{
"testing/qwerty",
"abc",
"rel_symlink",
"abs_symlink",
"secret",
"system",
"subpath/version.json",
".cipdpkg/manifest.json",
})
}
So(string(dest.files[0].Bytes()), ShouldEqual, "12345")
So(dest.files[1].executable, ShouldBeTrue)
So(dest.files[2].symlinkTarget, ShouldEqual, "abc")
So(dest.files[3].symlinkTarget, ShouldEqual, "/abc/def")
// Verify version file is correct.
verFileIdx := 4
goodVersionFile := `{
"instance_id": "45542f54335688804cfba83782140d2624d265a2",
"package_name": "testing"
}`
if runtime.GOOS == "windows" {
verFileIdx = 6
goodVersionFile = `{
"instance_id": "2208cc0f800b40895c5c4d5bf0e31235fa5e246f",
"package_name": "testing"
}`
}
So(dest.files[verFileIdx].name, ShouldEqual, "subpath/version.json")
So(string(dest.files[verFileIdx].Bytes()), shouldBeSameJSONDict, goodVersionFile)
// Verify manifest file is correct.
goodManifest := `{
"format_version": "1",
"package_name": "testing",
"version_file": "subpath/version.json",
"files": [
{
"name": "testing/qwerty",
"size": 5
},
{
"name": "abc",
"size": 3,
"executable": true
},
{
"name": "rel_symlink",
"size": 0,
"symlink": "abc"
},
{
"name": "abs_symlink",
"size": 0,
"symlink": "/abc/def"
}%s,
{
"name": "subpath/version.json",
"size": 92
}
]
}`
manifestIdx := 0
if runtime.GOOS == "windows" {
manifestIdx = 7
goodManifest = fmt.Sprintf(goodManifest, `,{
"name": "secret",
"size": 5,
"win_attrs": "H"
},
{
"name": "system",
"size": 7,
"win_attrs": "S"
}`)
} else {
manifestIdx = 5
goodManifest = fmt.Sprintf(goodManifest, "")
}
So(dest.files[manifestIdx].name, ShouldEqual, ".cipdpkg/manifest.json")
So(string(dest.files[manifestIdx].Bytes()), shouldBeSameJSONDict, goodManifest)
})
}
////////////////////////////////////////////////////////////////////////////////
type testDestination struct {
beginCalls int
endCalls int
files []*testDestinationFile
}
type testDestinationFile struct {
bytes.Buffer
name string
executable bool
symlinkTarget string
winAttrs WinAttrs
}
func (d *testDestinationFile) Close() error { return nil }
func (d *testDestination) Begin(context.Context) error {
d.beginCalls++
return nil
}
func (d *testDestination) CreateFile(ctx context.Context, name string, executable bool, winAttrs WinAttrs) (io.WriteCloser, error) {
f := &testDestinationFile{
name: name,
executable: executable,
winAttrs: winAttrs,
}
d.files = append(d.files, f)
return f, nil
}
func (d *testDestination) CreateSymlink(ctx context.Context, name string, target string) error {
f := &testDestinationFile{
name: name,
symlinkTarget: target,
}
d.files = append(d.files, f)
return nil
}
func (d *testDestination) End(ctx context.Context, success bool) error {
d.endCalls++
return nil
}