sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/resultstore/files_test.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package resultstore
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"slices"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  	"google.golang.org/genproto/googleapis/devtools/resultstore/v2"
    29  	"google.golang.org/protobuf/testing/protocmp"
    30  	"google.golang.org/protobuf/types/known/wrapperspb"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	pio "sigs.k8s.io/prow/pkg/io"
    33  )
    34  
    35  // fakeFileFinder is a testing fake for a subset of pio.Opener.
    36  type fakeFileFinder struct {
    37  	files map[string]pio.Attributes
    38  }
    39  
    40  // Assert this matches our subset of pio.Opener.
    41  var _ fileFinder = &fakeFileFinder{}
    42  
    43  var (
    44  	// A file with this value causes Iterator() to return error.
    45  	wantIterErr = pio.Attributes{ContentEncoding: "__wantIterErr__"}
    46  	// A file with this value causes Iterator.Next() to return error.
    47  	wantNextErr = pio.Attributes{ContentEncoding: "__wantNextErr__"}
    48  )
    49  
    50  // Iterator iterates at a prefix, which includes the provider and
    51  // bucket; the output names are relative to the bucket.
    52  func (f *fakeFileFinder) Iterator(_ context.Context, prefix, delimiter string) (pio.ObjectIterator, error) {
    53  	var fs []string
    54  	seenDirs := sets.Set[string]{}
    55  	b, err := bucket(prefix)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	for n, a := range f.files {
    60  		if !strings.HasPrefix(n, prefix) {
    61  			continue
    62  		}
    63  		if delimiter == "" || !strings.Contains(n[len(prefix):], delimiter) {
    64  			if a.ContentEncoding == wantIterErr.ContentEncoding {
    65  				return nil, fmt.Errorf("iterator error at %q", n)
    66  			}
    67  			fs = append(fs, n)
    68  			continue
    69  		}
    70  		ps := strings.SplitN(n[len(prefix):], delimiter, 2)
    71  		if seenDirs.Has(ps[0]) {
    72  			continue
    73  		}
    74  		fs = append(fs, fmt.Sprintf("%s%s/", prefix, ps[0]))
    75  		seenDirs.Insert(ps[0])
    76  	}
    77  	slices.Sort(fs)
    78  	return &fakeIterator{finder: f, files: fs, bucket: b}, nil
    79  }
    80  
    81  type fakeIterator struct {
    82  	finder *fakeFileFinder
    83  	files  []string
    84  	bucket string
    85  	pos    int
    86  }
    87  
    88  // Next only populates ObjectAttributes fields used by this package:
    89  // Name and IsDir.
    90  func (i *fakeIterator) Next(_ context.Context) (pio.ObjectAttributes, error) {
    91  	oa := pio.ObjectAttributes{}
    92  	if i.pos >= len(i.files) {
    93  		return oa, io.EOF
    94  	}
    95  	n := i.files[i.pos]
    96  	i.pos++
    97  	if i.finder.files[n].ContentEncoding == wantNextErr.ContentEncoding {
    98  		return oa, fmt.Errorf("next error at %q", n)
    99  	}
   100  	oa.Name = strings.TrimPrefix(n, i.bucket)
   101  	oa.IsDir = strings.HasSuffix(n, "/")
   102  	oa.Size = i.finder.files[n].Size
   103  	return oa, nil
   104  }
   105  
   106  func TestArtifactFiles(t *testing.T) {
   107  	ctx := context.Background()
   108  	base := "gs://bucket/pr-logs/1234"
   109  	for _, tc := range []struct {
   110  		desc    string
   111  		ff      *fakeFileFinder
   112  		opts    ArtifactOpts
   113  		want    []*resultstore.File
   114  		wantErr bool
   115  	}{
   116  		{
   117  			desc: "success",
   118  			ff: &fakeFileFinder{
   119  				files: map[string]pio.Attributes{
   120  					base + "/build-log.txt": {
   121  						Size: 9000,
   122  					},
   123  					base + "/started.json": {
   124  						Size: 350,
   125  					},
   126  					base + "/artifacts/artifact.txt": {
   127  						Size: 10000,
   128  					},
   129  				},
   130  			},
   131  			opts: ArtifactOpts{
   132  				Dir: base,
   133  				DefaultFiles: []DefaultFile{
   134  					{
   135  						Name: "prowjob.json",
   136  						Size: 1984,
   137  					},
   138  					{
   139  						Name: "started.json",
   140  						Size: 3500,
   141  					},
   142  				},
   143  			},
   144  			want: []*resultstore.File{
   145  				{
   146  					Uid:         "build.log",
   147  					Uri:         "gs://bucket/pr-logs/1234/build-log.txt",
   148  					Length:      &wrapperspb.Int64Value{Value: 9000},
   149  					ContentType: "text/plain",
   150  				},
   151  				{
   152  					Uid:         "started.json",
   153  					Uri:         "gs://bucket/pr-logs/1234/started.json",
   154  					Length:      &wrapperspb.Int64Value{Value: 350},
   155  					ContentType: "application/json",
   156  				},
   157  				{
   158  					Uid:         "prowjob.json",
   159  					Uri:         "gs://bucket/pr-logs/1234/prowjob.json",
   160  					Length:      &wrapperspb.Int64Value{Value: 1984},
   161  					ContentType: "application/json",
   162  				},
   163  				{
   164  					Uid:         "artifacts/artifact.txt",
   165  					Uri:         "gs://bucket/pr-logs/1234/artifacts/artifact.txt",
   166  					Length:      &wrapperspb.Int64Value{Value: 10000},
   167  					ContentType: "text/plain",
   168  				},
   169  			},
   170  		},
   171  		{
   172  			desc: "artifacts dir",
   173  			ff: &fakeFileFinder{
   174  				files: map[string]pio.Attributes{
   175  					base + "/build-log.txt": {
   176  						Size: 9000,
   177  					},
   178  					base + "/started.json": {
   179  						Size: 350,
   180  					},
   181  					base + "/artifacts/artifact.txt": {
   182  						Size: 10000,
   183  					},
   184  				},
   185  			},
   186  			opts: ArtifactOpts{
   187  				Dir:              base,
   188  				ArtifactsDirOnly: true,
   189  			},
   190  			want: []*resultstore.File{
   191  				{
   192  					Uid:         "build.log",
   193  					Uri:         "gs://bucket/pr-logs/1234/build-log.txt",
   194  					Length:      &wrapperspb.Int64Value{Value: 9000},
   195  					ContentType: "text/plain",
   196  				},
   197  				{
   198  					Uid:         "started.json",
   199  					Uri:         "gs://bucket/pr-logs/1234/started.json",
   200  					Length:      &wrapperspb.Int64Value{Value: 350},
   201  					ContentType: "application/json",
   202  				},
   203  				{
   204  					Uid: "artifacts/",
   205  					Uri: "gs://bucket/pr-logs/1234/artifacts/",
   206  				},
   207  			},
   208  		},
   209  		{
   210  			desc: "exclude unwanted subdirs",
   211  			ff: &fakeFileFinder{
   212  				files: map[string]pio.Attributes{
   213  					base + "/not-artifacts-subdir/unwanted": {},
   214  				},
   215  			},
   216  			opts: ArtifactOpts{
   217  				Dir: base,
   218  			},
   219  			want: nil,
   220  		},
   221  		{
   222  			desc: "exclude build.log",
   223  			ff: &fakeFileFinder{
   224  				files: map[string]pio.Attributes{
   225  					base + "/build.log": {},
   226  				},
   227  			},
   228  			opts: ArtifactOpts{
   229  				Dir: base,
   230  			},
   231  			want: nil,
   232  		},
   233  		{
   234  			desc: "empty",
   235  			ff:   &fakeFileFinder{},
   236  			opts: ArtifactOpts{
   237  				Dir: base,
   238  			},
   239  			want: nil,
   240  		},
   241  		{
   242  			desc: "iterator error",
   243  			ff: &fakeFileFinder{
   244  				files: map[string]pio.Attributes{
   245  					base + "/iterator-error.txt": wantIterErr,
   246  				},
   247  			},
   248  			opts: ArtifactOpts{
   249  				Dir: base,
   250  			},
   251  			wantErr: true,
   252  		},
   253  		{
   254  			desc: "iteration error",
   255  			ff: &fakeFileFinder{
   256  				files: map[string]pio.Attributes{
   257  					base + "/expected.txt": {
   258  						Size: 100,
   259  					},
   260  					base + "/next-error.txt": wantNextErr,
   261  					base + "/artifacts/unexpected.txt": {
   262  						Size: 1000,
   263  					},
   264  				},
   265  			},
   266  			opts: ArtifactOpts{
   267  				Dir: base,
   268  			},
   269  			want: []*resultstore.File{
   270  				{
   271  					Uid:         "expected.txt",
   272  					Uri:         "gs://bucket/pr-logs/1234/expected.txt",
   273  					Length:      &wrapperspb.Int64Value{Value: 100},
   274  					ContentType: "text/plain",
   275  				},
   276  			},
   277  			wantErr: true,
   278  		},
   279  		{
   280  			desc: "artifacts iteration error",
   281  			ff: &fakeFileFinder{
   282  				files: map[string]pio.Attributes{
   283  					base + "/ok.txt": {
   284  						Size: 100,
   285  					},
   286  					base + "/artifacts/a.txt": {
   287  						Size: 1000,
   288  					},
   289  					base + "/artifacts/b.txt": wantNextErr,
   290  					base + "/artifacts/c.txt": {
   291  						Size: 10000,
   292  					},
   293  				},
   294  			},
   295  			opts: ArtifactOpts{
   296  				Dir: base,
   297  			},
   298  			want: []*resultstore.File{
   299  				{
   300  					Uid:         "ok.txt",
   301  					Uri:         "gs://bucket/pr-logs/1234/ok.txt",
   302  					Length:      &wrapperspb.Int64Value{Value: 100},
   303  					ContentType: "text/plain",
   304  				},
   305  				{
   306  					Uid:         "artifacts/a.txt",
   307  					Uri:         "gs://bucket/pr-logs/1234/artifacts/a.txt",
   308  					Length:      &wrapperspb.Int64Value{Value: 1000},
   309  					ContentType: "text/plain",
   310  				},
   311  			},
   312  			wantErr: true,
   313  		},
   314  	} {
   315  		t.Run(tc.desc, func(t *testing.T) {
   316  			got, err := ArtifactFiles(ctx, tc.ff, tc.opts)
   317  			if err != nil {
   318  				if tc.wantErr {
   319  					t.Logf("got expected error: %v", err)
   320  				} else {
   321  					t.Fatalf("got unwanted error: %v", err)
   322  				}
   323  			} else if tc.wantErr {
   324  				t.Fatal("wanted error, got nil")
   325  			}
   326  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
   327  				t.Errorf("diff (-want +got):\n%s", diff)
   328  			}
   329  		})
   330  	}
   331  }
   332  
   333  func TestEnsureTrailingSlash(t *testing.T) {
   334  	for _, tc := range []struct {
   335  		in   string
   336  		want string
   337  	}{
   338  		{"some/path", "some/path/"},
   339  		{"some/path/", "some/path/"},
   340  	} {
   341  		if got := ensureTrailingSlash(tc.in); got != tc.want {
   342  			t.Errorf("ensureTrailingSlash(%q) got %s, want %s", tc.in, got, tc.want)
   343  		}
   344  	}
   345  }