blob: 337083745c50be8180d202b1ecdd7bd37c3e5da4 [file]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 bigqueryio
import (
"context"
"flag"
"fmt"
"math/rand"
"strings"
"testing"
"time"
"cloud.google.com/go/bigquery"
"github.com/apache/beam/sdks/v2/go/pkg/beam"
"github.com/apache/beam/sdks/v2/go/pkg/beam/io/bigqueryio"
"github.com/apache/beam/sdks/v2/go/pkg/beam/options/gcpopts"
"github.com/apache/beam/sdks/v2/go/pkg/beam/register"
_ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow"
"github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest"
"github.com/apache/beam/sdks/v2/go/test/integration"
)
func init() {
register.DoFn2x0[[]byte, func(TestRow)](&CreateTestRowsFn{})
register.Emitter1[TestRow]()
}
func checkFlags(t *testing.T) {
gcpProjectIsNotSet := gcpopts.Project == nil || *gcpopts.Project == ""
if gcpProjectIsNotSet {
t.Skip("GCP project flag is not set.")
}
if *integration.BigQueryDataset == "" {
t.Skip("No BigQuery dataset provided.")
}
}
const (
// A text to shuffle to get random words.
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas eget nulla nec " +
"velit hendrerit placerat. Donec eu odio ultricies, fermentum arcu at, mollis lectus. " +
"Vestibulum porttitor pharetra sem vitae feugiat. Mauris facilisis neque in mauris " +
"feugiat rhoncus. Donec eu ipsum at nibh lobortis euismod. Nam at hendrerit felis. " +
"Vivamus et orci ex. Nam dui nisl, rutrum ac pretium eget, vehicula in tortor. Class " +
"aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. " +
"Phasellus ante lorem, pharetra blandit dapibus et, tempus nec purus. Maecenas in " +
"posuere sem, vel pharetra nisl. Pellentesque habitant morbi tristique senectus et netus " +
"et malesuada fames ac turpis egestas. Donec nec facilisis ex. Praesent euismod commodo " +
"efficitur. Fusce in nisi nunc."
// Number of random elements to create for test. Must be less than number of words in text.
inputSize = 50
)
// TestRow is a sample row to write and read from that is expected to contain enough deterministic
// and random data in different data types to provide a reasonable signal that reading and writing
// works at a basic level.
type TestRow struct {
Counter int64 `bigquery:"counter"` // A deterministic counter, increments for each row generated.
RandData RandData `bigquery:"rand_data"` // An inner struct containing randomized data.
}
func shuffleText() []string {
words := strings.Fields(text)
rand.Shuffle(len(words), func(i, j int) { words[i], words[j] = words[j], words[i] })
return words
}
// RandData is a struct of various types of random data.
type RandData struct {
Flip bool `bigquery:"flip"` // Flip is a bool with a random chance of either result (a coin flip).
Num int64 `bigquery:"num"` // Num is a random int64.
Word string `bigquery:"word"` // Word is a randomly selected word from a sample text.
}
// ddlSchema is a string for BigQuery data definition language that corresponds to TestRow.
const ddlTestRowSchema = "counter INT64 NOT NULL, " +
"rand_data STRUCT<" +
"flip BOOL NOT NULL," +
"num INT64 NOT NULL," +
"word STRING NOT NULL" +
"> NOT NULL"
// CreateTestRowsFn is a DoFn that creates randomized TestRows based on a seed.
type CreateTestRowsFn struct {
seed int64
}
// ProcessElement creates a number of TestRows, populating the randomized data.
func (fn *CreateTestRowsFn) ProcessElement(_ []byte, emit func(TestRow)) {
rand.Seed(fn.seed)
words := shuffleText()
for i := 0; i < inputSize; i++ {
emit(TestRow{
Counter: int64(i),
RandData: RandData{
Flip: rand.Int63n(2) != 0,
Num: rand.Int63(),
Word: words[i],
},
})
}
}
func TestBigQueryIO_Write(t *testing.T) {
integration.CheckFilters(t)
checkFlags(t)
ctx := context.Background()
tests := []struct {
name string
preCreate bool
createDisposition bigquery.TableCreateDisposition
wantErr bool
}{
{
name: "CreateNever table create disposition with preexisting table",
preCreate: true,
createDisposition: bigquery.CreateNever,
wantErr: false,
},
{
name: "CreateIfNeeded table create disposition with preexisting table",
preCreate: true,
createDisposition: bigquery.CreateIfNeeded,
wantErr: false,
},
{
name: "CreateNever table create disposition with no preexisting table",
preCreate: false,
createDisposition: bigquery.CreateNever,
wantErr: true,
},
{
name: "CreateIfNeeded table create disposition with no preexisting table",
preCreate: false,
createDisposition: bigquery.CreateIfNeeded,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Get the GCP project
// this assumes dataflow is running in the same project as the project in which the bigquery dataset
// is located
project := gcpopts.GetProject(ctx)
tableID := fmt.Sprintf("%s_temp_%v", "go_bqio_it", time.Now().UnixNano())
tableName := fmt.Sprintf("%s.%s", *integration.BigQueryDataset, tableID)
if tt.preCreate {
newTempTable(t, tableName, ddlTestRowSchema)
}
t.Cleanup(func() {
deleteTempTable(t, tableName)
})
createTestRows := &CreateTestRowsFn{seed: time.Now().UnixNano()}
p, s := beam.NewPipelineWithRoot()
// Generate elements and write to table.
rows := beam.ParDo(s, createTestRows, beam.Impulse(s))
bigqueryio.Write(s, project, fmt.Sprintf("%s:%s", project, tableName), rows,
bigqueryio.WithCreateDisposition(tt.createDisposition))
if err := ptest.Run(p); (err != nil) != tt.wantErr {
t.Fatalf("ptest.Run() err = %v, wantErr %v", err, tt.wantErr)
} else if err != nil {
// Pipeline failed as expected, return early
return
}
checkTableExistsAndNonEmpty(ctx, t, project, tableID)
})
}
}
func TestMain(m *testing.M) {
flag.Parse()
beam.Init()
ptest.MainRet(m)
}