blob: 4bd2f7f635858b4cf405db029722a18cc7318324 [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package downloader
import (
"archive/tar"
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"go.chromium.org/luci/common/data/caching/cache"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/common/isolated"
"go.chromium.org/luci/common/isolatedclient"
"go.chromium.org/luci/common/isolatedclient/isolatedfake"
. "github.com/smartystreets/goconvey/convey"
)
func TestNormalizePathSeparator(t *testing.T) {
t.Parallel()
Convey("Check path normalization", t, func() {
Convey("posix path", func() {
So(normalizePathSeparator("a/b"), ShouldEqual, filepath.Join("a", "b"))
})
Convey("windows path", func() {
So(normalizePathSeparator(`a\b`), ShouldEqual, filepath.Join("a", "b"))
})
})
}
func TestDownloaderFetchIsolated(t *testing.T) {
t.Parallel()
ctx := context.Background()
data1 := []byte("hello world!")
data2 := []byte("wat")
tardata := genTar(t)
namespace := isolatedclient.DefaultNamespace
h := isolated.GetHash(namespace)
server := isolatedfake.New()
data1hash := server.Inject(namespace, data1)
data2hash := server.Inject(namespace, data2)
tardatahash := server.Inject(namespace, tardata)
tardataname := fmt.Sprintf("%s.tar", tardatahash)
onePath := filepath.Join("foo", "one.txt")
twoPath := filepath.Join("foo", "two.txt")
posixPath := "posix/path"
winPath := `win\path`
isolated1 := isolated.New(h)
isolated1.Files = map[string]isolated.File{
onePath: isolated.BasicFile(data1hash, 0664, int64(len(data1))),
twoPath: isolated.BasicFile(data2hash, 0764, int64(len(data2))),
posixPath: isolated.BasicFile(data1hash, 0664, int64(len(data1))),
winPath: isolated.BasicFile(data2hash, 0664, int64(len(data2))),
tardataname: isolated.TarFile(tardatahash, int64(len(tardata))),
}
isolated1bytes, _ := json.Marshal(&isolated1)
isolated1hash := server.Inject(namespace, isolated1bytes)
lolPath := filepath.Join("bar", "lol.txt")
oloPath := filepath.Join("foo", "boz", "olo.txt")
isolated2 := isolated.New(h)
isolated2.Files = map[string]isolated.File{
lolPath: isolated.BasicFile(data1hash, 0664, int64(len(data1))),
oloPath: isolated.BasicFile(data2hash, 0664, int64(len(data2))),
}
isolatedFiles := stringset.NewFromSlice([]string{
tardataname,
onePath,
twoPath,
normalizePathSeparator(posixPath),
normalizePathSeparator(winPath),
lolPath,
oloPath,
// In tardata
"file1",
"file2",
filepath.Join("tar", "posix", "path"),
filepath.Join("tar", "win", "path"),
}...)
blahPath := "blah.txt"
// Symlinks not supported on Windows.
if runtime.GOOS != "windows" {
isolated2.Files[blahPath] = isolated.SymLink(oloPath)
isolatedFiles.Add(blahPath)
}
isolated2.Includes = isolated.HexDigests{isolated1hash}
isolated2bytes, _ := json.Marshal(&isolated2)
isolated2hash := server.Inject(namespace, isolated2bytes)
ts := httptest.NewServer(server)
defer ts.Close()
client := isolatedclient.NewClient(ts.URL, isolatedclient.WithNamespace(namespace))
Convey(`A downloader should be able to download the isolated.`, t, func() {
tmpDir := t.TempDir()
mu := sync.Mutex{}
var files []string
d := New(ctx, client, isolated2hash, tmpDir, &Options{
FileCallback: func(name string, _ *isolated.File) {
mu.Lock()
files = append(files, name)
mu.Unlock()
},
})
So(d.Wait(), ShouldBeNil)
So(stringset.NewFromSlice(files...), ShouldResemble, isolatedFiles)
b, err := ioutil.ReadFile(filepath.Join(tmpDir, onePath))
So(err, ShouldBeNil)
So(b, ShouldResemble, data1)
fi, err := os.Stat(filepath.Join(tmpDir, onePath))
So(err, ShouldBeNil)
// to ignore effect of umask, only check executable bit.
So(fi.Mode()&0111, ShouldEqual, 0)
b, err = ioutil.ReadFile(filepath.Join(tmpDir, twoPath))
So(err, ShouldBeNil)
So(b, ShouldResemble, data2)
fi, err = os.Stat(filepath.Join(tmpDir, twoPath))
So(err, ShouldBeNil)
// to ignore effect of umask, only check executable bit.
So(fi.Mode()&0011, ShouldEqual, 0)
b, err = ioutil.ReadFile(filepath.Join(tmpDir, lolPath))
So(err, ShouldBeNil)
So(b, ShouldResemble, data1)
b, err = ioutil.ReadFile(filepath.Join(tmpDir, oloPath))
So(err, ShouldBeNil)
So(b, ShouldResemble, data2)
// Check files in tar archive
_, err = os.Stat(filepath.Join(tmpDir, "file1"))
So(err, ShouldBeNil)
_, err = os.Stat(filepath.Join(tmpDir, "file2"))
So(err, ShouldBeNil)
_, err = os.Stat(filepath.Join(tmpDir, "tar", "posix", "path"))
So(err, ShouldBeNil)
_, err = os.Stat(filepath.Join(tmpDir, "tar", "win", "path"))
So(err, ShouldBeNil)
_, err = ioutil.ReadFile(filepath.Join(tmpDir, tardataname))
So(os.IsNotExist(err), ShouldBeTrue)
if runtime.GOOS != "windows" {
l, err := os.Readlink(filepath.Join(tmpDir, blahPath))
So(err, ShouldBeNil)
So(l, ShouldResemble, oloPath)
}
})
}
// genTar returns a valid tar file.
func genTar(t *testing.T) []byte {
b := bytes.Buffer{}
tw := tar.NewWriter(&b)
d := []byte("hello file1")
if err := tw.WriteHeader(&tar.Header{Name: "file1", Mode: 0644, Typeflag: tar.TypeReg, Size: int64(len(d))}); err != nil {
t.Fatal(err)
}
if _, err := tw.Write(d); err != nil {
t.Fatal(err)
}
d = []byte(strings.Repeat("hello file2", 100))
if err := tw.WriteHeader(&tar.Header{Name: "file2", Mode: 0644, Typeflag: tar.TypeReg, Size: int64(len(d))}); err != nil {
t.Fatal(err)
}
if _, err := tw.Write(d); err != nil {
t.Fatal(err)
}
d = []byte("posixpath")
if err := tw.WriteHeader(&tar.Header{Name: "tar/posix/path", Mode: 0644, Typeflag: tar.TypeReg, Size: int64(len(d))}); err != nil {
t.Fatal(err)
}
if _, err := tw.Write(d); err != nil {
t.Fatal(err)
}
d = []byte("winpath")
if err := tw.WriteHeader(&tar.Header{Name: `tar\win\path`, Mode: 0644, Typeflag: tar.TypeReg, Size: int64(len(d))}); err != nil {
t.Fatal(err)
}
if _, err := tw.Write(d); err != nil {
t.Fatal(err)
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
return b.Bytes()
}
func TestDownloaderWithCache(t *testing.T) {
t.Parallel()
Convey(`TestDownloaderWithCache`, t, func() {
tmpDir := t.TempDir()
ctx := context.Background()
miss := []byte("cache miss")
hit := []byte("cache hit")
namespace := isolatedclient.DefaultNamespace
h := isolated.GetHash(namespace)
server := isolatedfake.New()
misshash := server.Inject(namespace, miss)
hithash := isolated.HashBytes(isolated.GetHash(namespace), hit)
missPath := filepath.Join("foo", "miss.txt")
hitPath := filepath.Join("foo", "hit.txt")
isolated1 := isolated.New(h)
isolated1.Files = map[string]isolated.File{
missPath: isolated.BasicFile(misshash, 0664, int64(len(miss))),
hitPath: isolated.BasicFile(hithash, 0664, int64(len(hit))),
}
isolated1bytes, _ := json.Marshal(&isolated1)
isolated1hash := server.Inject(namespace, isolated1bytes)
isolatedFiles := stringset.NewFromSlice([]string{
missPath,
hitPath,
}...)
ts := httptest.NewServer(server)
defer ts.Close()
client := isolatedclient.NewClient(ts.URL, isolatedclient.WithNamespace(namespace))
mu := sync.Mutex{}
var files []string
policy := cache.Policies{
MaxSize: 1024,
MaxItems: 1024,
}
cacheDir := filepath.Join(tmpDir, "cache")
So(os.MkdirAll(cacheDir, os.ModePerm), ShouldBeNil)
cacheObj, err := cache.New(policy, cacheDir, h)
So(err, ShouldBeNil)
So(cacheObj.Add(ctx, hithash, bytes.NewReader(hit)), ShouldBeNil)
d := New(ctx, client, isolated1hash, tmpDir, &Options{
FileCallback: func(name string, _ *isolated.File) {
mu.Lock()
files = append(files, name)
mu.Unlock()
},
Cache: cacheObj,
})
So(d.Wait(), ShouldBeNil)
So(stringset.NewFromSlice(files...), ShouldResemble, isolatedFiles)
So(cacheObj.Touch(misshash), ShouldBeTrue)
})
}
func TestFetchAndMap(t *testing.T) {
t.Parallel()
Convey(`TestFetchAndMap`, t, func() {
tmpdir := t.TempDir()
ctx := context.Background()
isolatedDir := filepath.Join(tmpdir, "isolated")
So(os.Mkdir(isolatedDir, 0o700), ShouldBeNil)
data1 := []byte("hello world!")
namespace := isolatedclient.DefaultNamespace
h := isolated.GetHash(namespace)
server := isolatedfake.New()
data1hash := server.Inject(namespace, data1)
onePath := filepath.Join("foo", "one.txt")
onePathFile := isolated.BasicFile(data1hash, 0664, int64(len(data1)))
isolated1 := isolated.New(h)
tardata := genTar(t)
tarhash := server.Inject(namespace, tardata)
isolated1.Files = map[string]isolated.File{
onePath: onePathFile,
"tar.tar": isolated.TarFile(tarhash, int64(len(tardata))),
}
isolated1bytes, _ := json.Marshal(&isolated1)
isolated1hash := server.Inject(namespace, isolated1bytes)
ts := httptest.NewServer(server)
defer ts.Close()
client := isolatedclient.NewClient(ts.URL, isolatedclient.WithNamespace(namespace))
policy := cache.Policies{
MaxSize: 1024 * 1024,
MaxItems: 1024,
}
diskcache, err := cache.New(policy, filepath.Join(tmpdir, "cache"), h)
So(err, ShouldBeNil)
defer func() {
So(diskcache.Close(), ShouldBeNil)
}()
isomap, stats, err := FetchAndMap(ctx, isolated1hash, client, diskcache, isolatedDir)
So(err, ShouldBeNil)
So(isomap, ShouldResemble, &isolated.Isolated{
Algo: "sha-1",
Version: isolated.IsolatedFormatVersion,
})
So(stats.Duration, ShouldBeGreaterThan, 0)
So(stats.ItemsCold, ShouldResemble, []byte{120, 156, 226, 249, 162, 15, 8, 0, 0, 255, 255, 2, 62, 1, 48})
So(stats.ItemsHot, ShouldResemble, []byte(nil))
buf, err := ioutil.ReadFile(filepath.Join(isolatedDir, onePath))
So(err, ShouldBeNil)
So(buf, ShouldResemble, data1)
So(diskcache.Touch(tarhash), ShouldBeTrue)
})
}