blob: 11ed6b692bea33cf17b18626a8b37de03b2cf398 [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gerrit
import (
"context"
"encoding/json"
"errors"
"fmt"
"iter"
"os"
"strconv"
"strings"
"testing"
"time"
"golang.org/x/oscar/internal/storage"
"golang.org/x/tools/txtar"
)
// divertChanges reports whether changes and their
// comments are being diverted for testing purposes.
func (c *Client) divertChanges() bool {
return c.testing && c.testClient != nil
}
// Testing returns a TestingClient, which provides access to Client functionality
// intended for testing.
// Testing only returns a non-nil TestingClient in testing mode,
// which is active if the current program is a test binary (that is, [testing.Testing] returns true).
// Otherwise, Testing returns nil.
//
// Each Client has only one TestingClient associated with it. Every call to Testing returns the same TestingClient.
func (c *Client) Testing() *TestingClient {
if !testing.Testing() && !c.testing {
return nil
}
if c.testClient == nil {
c.testClient = &TestingClient{}
}
return c.testClient
}
// A TestingClient provides access to [Client] functionality intended for testing.
type TestingClient struct {
chs []*ChangeInfo // change updates, in reverse chronological order
queryLimit int // mimic Gerrit query limits
}
// LoadTxtar loads a change info history from the named txtar file,
// and adds it to tc.chs.
//
// The file should contain a txtar archive (see [golang.org/x/tools/txtar]).
// Each file in the archive may be named “change#n” (for example “change#1”).
// A line in the file must be in the format "key: value", where "key" is one
// of the fields of [ChangeInfo] type.
func (tc *TestingClient) LoadTxtar(file string) error {
data, err := os.ReadFile(file)
if err != nil {
return err
}
err = tc.LoadTxtarData(data)
if err != nil {
err = &os.PathError{Op: "load", Path: file, Err: err}
}
return err
}
// LoadTxtarData loads a change info history from the txtar file content data.
// See [LoadTxtar] for a description of the format.
func (tc *TestingClient) LoadTxtarData(data []byte) error {
ar := txtar.Parse(data)
for _, file := range ar.Files {
data := string(file.Data)
// Skip the name and proceed to read headers.
c := &ChangeInfo{}
for {
line, rest, _ := strings.Cut(data, "\n")
data = rest
if line == "" {
break
}
key, val, ok := strings.Cut(line, ":")
if !ok {
return fmt.Errorf("%s: invalid header line: %q", file.Name, line)
}
val = strings.TrimSpace(val)
if val == "" {
continue
}
switch key {
case "Number":
i, err := strconv.Atoi(val)
if err != nil {
return err
}
c.Number = i
case "Updated":
t, err := timestamp(val)
if err != nil {
return err
}
c.Updated = t
case "MetaRevID":
c.MetaRevID = val
case "interrupt":
b, err := strconv.ParseBool(val)
if err != nil {
return err
}
c.interrupt = b
}
}
tc.chs = append(tc.chs, c)
}
return nil
}
// changes returns an iterator of change updates in tc.chs that are updated
// in the interval [after, before], in reverse chronological order. First
// skip number of matching change updates are disregarded.
func (tc *TestingClient) changes(_ context.Context, _ string, after, before string, skip int) iter.Seq2[json.RawMessage, error] {
return func(yield func(json.RawMessage, error) bool) {
skipped := 0
inInterval := false
yielded := 0 // yielded in a single batch
for _, c := range tc.chs {
in, err := updatedIn(c, after, before)
if err != nil {
yield(nil, err)
return
}
if !in {
if inInterval { // reached outside of the interval
return
}
continue
}
// We are inside the matching interval.
inInterval = true
if skip > 0 && skipped < skip {
skipped++
continue
}
cj, err := json.Marshal(c)
if err != nil {
yield(nil, err)
return
}
yielded++
if !yield(cj, nil) {
return
}
if c.interrupt { // fake an interruption
yield(nil, errors.New("test interrupt error"))
return
}
if yielded >= tc.queryLimit { // reached the batch limit
return
}
}
}
}
// updatedIn reports if c was updated in the [after, before] interval.
// Both after and before must be in gerrit timestamp layout.
func updatedIn(c *ChangeInfo, after, before string) (bool, error) {
u := c.Updated.Time()
ain := true
if after != "" {
a, err := timestamp(after)
if err != nil {
return false, err
}
ain = a.Time().Equal(u) || a.Time().Before(u)
}
bin := true
if before != "" {
b, err := timestamp(before)
if err != nil {
return false, err
}
bin = b.Time().Equal(u) || b.Time().After(u)
}
return ain && bin, nil
}
func timestamp(gt string) (TimeStamp, error) {
var ts TimeStamp
if err := ts.UnmarshalJSON([]byte(quote(gt))); err != nil {
return TimeStamp(time.Time{}), err
}
return ts, nil
}
// syncComments does nothing, for testing purposes.
func (tc *TestingClient) syncComments(_ context.Context, _ storage.Batch, _ string, _ int) error {
return nil
}