blob: aa4d3843910d84c236dfafe3ffe8d51f67336b04 [file] [log] [blame]
// Copyright 2016 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 lucictx
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"testing"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
"go.chromium.org/luci/common/system/environ"
)
func rawctx(content string) func() {
tf, err := ioutil.TempFile(os.TempDir(), "lucictx_test.")
if err != nil {
panic(err)
}
_, err = tf.WriteString(content)
if err != nil {
panic(err)
}
tf.Close()
os.Setenv(EnvKey, tf.Name())
return func() { os.Remove(tf.Name()) }
}
func TestLUCIContextInitialization(t *testing.T) {
// t.Parallel() because of os.Environ manipulation
Convey("extractFromEnv", t, func() {
os.Unsetenv(EnvKey)
buf := &bytes.Buffer{}
Convey("works with missing envvar", func() {
So(extractFromEnv(buf), ShouldResemble, &lctx{})
So(buf.String(), ShouldEqual, "")
_, ok := os.LookupEnv(EnvKey)
So(ok, ShouldBeFalse)
})
Convey("works with bad envvar", func() {
os.Setenv(EnvKey, "sup")
So(extractFromEnv(buf), ShouldResemble, &lctx{})
So(buf.String(), ShouldContainSubstring, "Could not open LUCI_CONTEXT file")
})
Convey("works with bad file", func() {
defer rawctx(`"not a map"`)()
So(extractFromEnv(buf), ShouldResemble, &lctx{})
So(buf.String(), ShouldContainSubstring, "cannot unmarshal string into Go value")
})
Convey("ignores bad sections", func() {
defer rawctx(`{"hi": {"hello_there": 10}, "not": "good"}`)()
blob := json.RawMessage(`{"hello_there":10}`)
So(extractFromEnv(buf).sections, ShouldResemble, map[string]*json.RawMessage{
"hi": &blob,
})
So(buf.String(), ShouldContainSubstring, `section "not": Not a map`)
})
})
}
func TestLUCIContextMethods(t *testing.T) {
// t.Parallel() manipulates environ
defer rawctx(`{"hi": {"hello_there": 10}}`)()
externalContext = extractFromEnv(nil)
Convey("lctx", t, func() {
c := context.Background()
Convey("Get/Set", func() {
Convey("defaults to env values", func() {
h := &TestStructure{}
So(Get(c, "hi", h), ShouldBeNil)
So(h, ShouldResembleProto, &TestStructure{HelloThere: 10})
})
Convey("nop for missing sections", func() {
h := &TestStructure{}
So(Get(c, "wut", h), ShouldBeNil)
So(h, ShouldResembleProto, &TestStructure{})
})
Convey("can add section", func() {
c := Set(c, "wut", &TestStructure{HelloThere: 100})
h := &TestStructure{}
So(Get(c, "wut", h), ShouldBeNil)
So(h, ShouldResembleProto, &TestStructure{HelloThere: 100})
So(Get(c, "hi", h), ShouldBeNil)
So(h, ShouldResembleProto, &TestStructure{HelloThere: 10})
})
Convey("can override section", func() {
c := Set(c, "hi", &TestStructure{HelloThere: 100})
h := &TestStructure{}
So(Get(c, "hi", h), ShouldBeNil)
So(h, ShouldResembleProto, &TestStructure{HelloThere: 100})
})
Convey("can remove section", func() {
c := Set(c, "hi", nil)
h := &TestStructure{}
So(Get(c, "hi", h), ShouldBeNil)
So(h, ShouldResembleProto, &TestStructure{HelloThere: 0})
})
Convey("allow unknown field", func() {
blob := json.RawMessage(`{"hello_there":10, "unknown_field": "unknown"}`)
newLctx := externalContext.clone()
newLctx.sections["hi"] = &blob
c := context.WithValue(context.Background(), &lctxKey, newLctx)
h := &TestStructure{}
So(Get(c, "hi", h), ShouldBeNil)
So(h, ShouldResembleProto, &TestStructure{HelloThere: 10})
})
})
Convey("Export", func() {
Convey("empty export is a noop", func() {
c = Set(c, "hi", nil)
e, err := Export(c)
So(err, ShouldBeNil)
So(e, ShouldHaveSameTypeAs, &nullExport{})
})
Convey("exporting with content gives a live export", func() {
e, err := Export(c)
So(err, ShouldBeNil)
So(e, ShouldHaveSameTypeAs, &liveExport{})
defer e.Close()
cmd := exec.Command("something", "something")
e.SetInCmd(cmd)
path, ok := environ.New(cmd.Env).Lookup(EnvKey)
So(ok, ShouldBeTrue)
// There's a valid JSON there.
blob, err := os.ReadFile(path)
So(err, ShouldBeNil)
So(string(blob), ShouldEqual, `{"hi": {"hello_there": 10}}`)
// And it is in fact located in same file as was supplied externally.
So(path, ShouldEqual, externalContext.path)
})
Convey("Export reuses files", func() {
c := Set(c, "blah", &TestStructure{})
e1, err := Export(c)
So(err, ShouldBeNil)
So(e1, ShouldHaveSameTypeAs, &liveExport{})
defer func() {
if e1 != nil {
e1.Close()
}
}()
e2, err := Export(c)
So(err, ShouldBeNil)
So(e2, ShouldHaveSameTypeAs, &liveExport{})
defer func() {
if e2 != nil {
e2.Close()
}
}()
// Exact same backing file.
So(e1.(*liveExport).path, ShouldEqual, e2.(*liveExport).path)
// It really exists.
path := e1.(*liveExport).path
_, err = os.Stat(path)
So(err, ShouldBeNil)
// Closing one export keeps the file open.
e1.Close()
e1 = nil
_, err = os.Stat(path)
So(err, ShouldBeNil)
// Closing both exports removes the file from disk.
e2.Close()
e2 = nil
_, err = os.Stat(path)
So(os.IsNotExist(err), ShouldBeTrue)
})
Convey("ExportInto creates new files", func() {
tmp, err := ioutil.TempDir("", "luci_ctx")
So(err, ShouldBeNil)
defer func() {
os.RemoveAll(tmp)
}()
c := Set(c, "blah", &TestStructure{})
e1, err := ExportInto(c, tmp)
So(err, ShouldBeNil)
p1 := e1.(*liveExport).path
e2, err := ExportInto(c, tmp)
So(err, ShouldBeNil)
p2 := e2.(*liveExport).path
// Two different files, both under 'tmp'.
So(p1, ShouldNotEqual, p2)
So(filepath.Dir(p1), ShouldEqual, tmp)
So(filepath.Dir(p2), ShouldEqual, tmp)
// Both exist.
_, err = os.Stat(p1)
So(err, ShouldBeNil)
_, err = os.Stat(p2)
So(err, ShouldBeNil)
// Closing one still keeps the other open.
So(e1.Close(), ShouldBeNil)
_, err = os.Stat(p1)
So(os.IsNotExist(err), ShouldBeTrue)
_, err = os.Stat(p2)
So(err, ShouldBeNil)
})
})
})
}