go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/sink/artifact_channel_test.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package sink 16 17 import ( 18 "context" 19 "io" 20 "io/ioutil" 21 "net/http" 22 "os" 23 "testing" 24 25 . "github.com/smartystreets/goconvey/convey" 26 . "go.chromium.org/luci/common/testing/assertions" 27 pb "go.chromium.org/luci/resultdb/proto/v1" 28 sinkpb "go.chromium.org/luci/resultdb/sink/proto/v1" 29 ) 30 31 func TestArtifactChannel(t *testing.T) { 32 t.Parallel() 33 34 Convey("schedule", t, func() { 35 cfg := testServerConfig("127.0.0.1:123", "secret") 36 ctx := context.Background() 37 38 streamCh := make(chan *http.Request, 10) 39 cfg.ArtifactStreamClient.Transport = mockTransport( 40 func(in *http.Request) (*http.Response, error) { 41 streamCh <- in 42 return &http.Response{StatusCode: http.StatusNoContent}, nil 43 }, 44 ) 45 batchCh := make(chan *pb.BatchCreateArtifactsRequest, 10) 46 cfg.Recorder.(*mockRecorder).batchCreateArtifacts = func(ctx context.Context, in *pb.BatchCreateArtifactsRequest) (*pb.BatchCreateArtifactsResponse, error) { 47 batchCh <- in 48 return nil, nil 49 } 50 createTask := func(name, content string) *uploadTask { 51 art := testArtifactWithContents([]byte(content)) 52 task, err := newUploadTask(name, art) 53 So(err, ShouldBeNil) 54 return task 55 } 56 57 Convey("with a small artifact", func() { 58 task := createTask("invocations/inv/artifacts/art1", "content") 59 ac := newArtifactChannel(ctx, &cfg) 60 ac.schedule(task) 61 ac.closeAndDrain(ctx) 62 63 // The artifact should have been sent to the batch channel. 64 So(<-batchCh, ShouldResembleProto, &pb.BatchCreateArtifactsRequest{ 65 Requests: []*pb.CreateArtifactRequest{{ 66 Parent: "invocations/inv", 67 Artifact: &pb.Artifact{ 68 ArtifactId: "art1", 69 ContentType: task.art.ContentType, 70 SizeBytes: int64(len("content")), 71 Contents: []byte("content"), 72 }, 73 }}, 74 }) 75 }) 76 77 Convey("with multiple, small artifacts", func() { 78 cfg.MaxBatchableArtifactSize = 10 79 ac := newArtifactChannel(ctx, &cfg) 80 81 t1 := createTask("invocations/inv/artifacts/art1", "1234") 82 t2 := createTask("invocations/inv/artifacts/art2", "5678") 83 t3 := createTask("invocations/inv/artifacts/art3", "9012") 84 ac.schedule(t1) 85 ac.schedule(t2) 86 ac.schedule(t3) 87 ac.closeAndDrain(ctx) 88 89 // The 1st request should contain the first two artifacts. 90 So(<-batchCh, ShouldResembleProto, &pb.BatchCreateArtifactsRequest{ 91 Requests: []*pb.CreateArtifactRequest{ 92 // art1 93 { 94 Parent: "invocations/inv", 95 Artifact: &pb.Artifact{ 96 ArtifactId: "art1", 97 ContentType: t1.art.ContentType, 98 SizeBytes: int64(len("1234")), 99 Contents: []byte("1234"), 100 }, 101 }, 102 // art2 103 { 104 Parent: "invocations/inv", 105 Artifact: &pb.Artifact{ 106 ArtifactId: "art2", 107 ContentType: t2.art.ContentType, 108 SizeBytes: int64(len("5678")), 109 Contents: []byte("5678"), 110 }, 111 }, 112 }, 113 }) 114 115 // The 2nd request should only contain the last one. 116 So(<-batchCh, ShouldResembleProto, &pb.BatchCreateArtifactsRequest{ 117 Requests: []*pb.CreateArtifactRequest{ 118 // art3 119 { 120 Parent: "invocations/inv", 121 Artifact: &pb.Artifact{ 122 ArtifactId: "art3", 123 ContentType: t3.art.ContentType, 124 SizeBytes: int64(len("9012")), 125 Contents: []byte("9012"), 126 }, 127 }, 128 }, 129 }) 130 }) 131 132 Convey("with a large artifact", func() { 133 cfg.MaxBatchableArtifactSize = 10 134 ac := newArtifactChannel(ctx, &cfg) 135 136 t1 := createTask("invocations/inv/artifacts/art1", "content-foo-bar") 137 ac.schedule(t1) 138 ac.closeAndDrain(ctx) 139 140 // The artifact should have been sent to the stream channel. 141 req := <-streamCh 142 So(req, ShouldNotBeNil) 143 So(req.URL.String(), ShouldEqual, 144 "https://"+cfg.ArtifactStreamHost+"/invocations/inv/artifacts/art1") 145 body, err := io.ReadAll(req.Body) 146 So(err, ShouldBeNil) 147 So(body, ShouldResemble, []byte("content-foo-bar")) 148 }) 149 }) 150 151 } 152 153 func TestUploadTask(t *testing.T) { 154 t.Parallel() 155 156 Convey("newUploadTask", t, func() { 157 name := "invocations/inv/artifacts/art1" 158 fArt := testArtifactWithFile(func(f *os.File) { 159 _, err := f.Write([]byte("content")) 160 So(err, ShouldBeNil) 161 }) 162 fArt.ContentType = "plain/text" 163 defer os.Remove(fArt.GetFilePath()) 164 165 Convey("works", func() { 166 t, err := newUploadTask(name, fArt) 167 So(err, ShouldBeNil) 168 So(t, ShouldResemble, &uploadTask{art: fArt, artName: name, size: int64(len("content"))}) 169 }) 170 171 Convey("fails", func() { 172 // stat error 173 So(os.Remove(fArt.GetFilePath()), ShouldBeNil) 174 _, err := newUploadTask(name, fArt) 175 So(err, ShouldErrLike, "querying file info") 176 177 // is a directory 178 path, err := ioutil.TempDir("", "foo") 179 So(err, ShouldBeNil) 180 defer os.RemoveAll(path) 181 fArt.Body.(*sinkpb.Artifact_FilePath).FilePath = path 182 _, err = newUploadTask(name, fArt) 183 So(err, ShouldErrLike, "is a directory") 184 }) 185 }) 186 187 Convey("CreateRequest", t, func() { 188 name := "invocations/inv/tests/t1/results/r1/artifacts/a1" 189 fArt := testArtifactWithFile(func(f *os.File) { 190 _, err := f.Write([]byte("content")) 191 So(err, ShouldBeNil) 192 }) 193 fArt.ContentType = "plain/text" 194 defer os.Remove(fArt.GetFilePath()) 195 ut, err := newUploadTask(name, fArt) 196 So(err, ShouldBeNil) 197 198 Convey("works", func() { 199 req, err := ut.CreateRequest() 200 So(err, ShouldBeNil) 201 So(req, ShouldResembleProto, &pb.CreateArtifactRequest{ 202 Parent: "invocations/inv/tests/t1/results/r1", 203 Artifact: &pb.Artifact{ 204 ArtifactId: "a1", 205 ContentType: "plain/text", 206 SizeBytes: int64(len("content")), 207 Contents: []byte("content"), 208 }, 209 }) 210 }) 211 212 Convey("fails", func() { 213 // the artifact content changed. 214 So(os.WriteFile(fArt.GetFilePath(), []byte("surprise!!"), 0), ShouldBeNil) 215 _, err := ut.CreateRequest() 216 So(err, ShouldErrLike, "the size of the artifact contents changed") 217 218 // the file no longer exists. 219 So(os.Remove(fArt.GetFilePath()), ShouldBeNil) 220 _, err = ut.CreateRequest() 221 So(err, ShouldErrLike, "open "+fArt.GetFilePath()) // no such file or directory 222 }) 223 }) 224 }