Files
2026-07-13 12:32:25 +08:00

242 lines
6.4 KiB
Go

// Copyright 2023 Dolthub, Inc.
//
// 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 main
import (
"bytes"
"encoding/csv"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"sync/atomic"
"github.com/cockroachdb/errors"
"github.com/dolthub/sqllogictest/go/logictest"
"github.com/dolthub/doltgresql/testing/logictest/harness"
)
var resultFormat = flag.String("r", "json", "format of parsed results")
var manageServer = flag.Bool("server", false, "whether the runner should launch the server process")
var doltgres = flag.String("doltgres", "", "local doltgres binary")
var timeout = flag.Int64("timeout", 0, "set a timeout (in seconds) for each test query")
var workers = flag.Int("workers", 1, "number of parallel workers")
// Runs all sqllogictest test files (or directories containing them) given as arguments.
// Usage: $command (run|parse) [version] [file1.test dir1/ dir2/]
// In run mode, runs the tests and prints results to stdout.
// In parse mode, parses test results from the file given and prints them to STDOUT in a format to be imported by dolt.
func main() {
flag.Parse()
args := flag.Args()
if len(args) < 1 {
panic("Usage: logictest (run|parse) [version] file1 file2 ...")
}
if args[0] == "run" {
if *manageServer {
if *doltgres == "" {
log.Fatal("Must supply --doltgres=<path to doltgres> with --server=true")
}
if *workers > 1 {
srv := harness.StartSharedServer(*doltgres)
defer srv.Close()
runConcurrent(*workers, *timeout, args[1:]...)
} else {
dh := harness.NewDoltgresHarness(*doltgres, *timeout)
defer dh.Close()
logictest.RunTestFiles(dh, args[1:]...)
}
} else {
if *workers > 1 {
runConcurrent(*workers, *timeout, args[1:]...)
} else {
h := harness.NewPostgresqlHarness("postgresql://postgres:password@localhost:5432/sqllogictest?sslmode=disable", *timeout)
logictest.RunTestFiles(h, args[1:]...)
}
}
} else if args[0] == "parse" {
if len(args) < 3 {
panic("Usage: logictest [-r(csv|json)] parse <version> (file | dir/)")
}
parseTestResults(args[1], args[2])
} else {
panic("Unrecognized command " + args[0])
}
}
// runConcurrent fans out |numWorkers| goroutines over the test files specified in |paths|.
// Each worker gets its own isolated database (sqllogictest_1, sqllogictest_2, …).
func runConcurrent(numWorkers int, queryTimeout int64, paths ...string) {
var workerIdx int32
logictest.RunTestFilesConcurrently(func() logictest.Harness {
n := atomic.AddInt32(&workerIdx, 1)
dbName := fmt.Sprintf("sqllogictest_%d", n)
return harness.NewDoltgresWorkerHarness(harness.DefaultPort, dbName, queryTimeout)
}, numWorkers, paths...)
}
func parseTestResults(version, f string) {
entries, err := logictest.ParseResultFile(f)
if err != nil {
panic(err)
}
records := make([]*DoltResultRecord, len(entries))
for i, e := range entries {
records[i] = NewDoltRecordResult(e, version)
}
if *resultFormat == "csv" {
err := writeResultsCsv(records)
if err != nil {
panic(err)
}
} else {
b, err := JSONMarshal(records)
if err != nil {
panic(err)
}
_, err = os.Stdout.Write(b)
if err != nil {
panic(err)
}
}
}
// Custom json marshalling function is necessary to avoid escaping <, > and & to html unicode escapes
func JSONMarshal(records []*DoltResultRecord) ([]byte, error) {
rows := &TestResultArray{Rows: records}
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
err := encoder.Encode(rows)
return buffer.Bytes(), err
}
func NewDoltRecordResult(e *logictest.ResultLogEntry, version string) *DoltResultRecord {
var result string
switch e.Result {
case logictest.Ok:
result = "ok"
case logictest.NotOk:
result = "not ok"
case logictest.Skipped:
result = "skipped"
case logictest.Timeout:
result = "timeout"
case logictest.DidNotRun:
result = "did not run"
}
return &DoltResultRecord{
Version: version,
TestFile: e.TestFile,
LineNum: e.LineNum,
Query: e.Query,
Duration: e.Duration.Milliseconds(),
Result: result,
ErrorMessage: e.ErrorMessage,
}
}
type TestResultArray struct {
Rows []*DoltResultRecord `json:"rows"`
}
type DoltResultRecord struct {
Version string `json:"version"`
TestFile string `json:"test_file"`
LineNum int `json:"line_num"`
Query string `json:"query_string"`
Duration int64 `json:"duration"`
Result string `json:"result"`
ErrorMessage string `json:"error_message,omitempty"`
}
// fromResultCsvHeaders returns supported csv headers for a Result
func fromResultCsvHeaders() []string {
return []string{
"version",
"test_file",
"line_num",
"query_string",
"duration",
"result",
"error_message",
}
}
// fromHeaderColumnValue returns the value from the DoltResultRecord for the given
// header field
func fromHeaderColumnValue(h string, r *DoltResultRecord) (string, error) {
var val string
switch h {
case "version":
val = r.Version
case "test_file":
val = r.TestFile
case "line_num":
val = fmt.Sprintf("%d", r.LineNum)
case "query_string":
val = r.Query
case "duration":
val = fmt.Sprintf("%d", r.Duration)
case "result":
val = r.Result
case "error_message":
val = r.ErrorMessage
default:
return "", errors.Errorf("unsupported header field")
}
return val, nil
}
// writeResultsCsv writes []*DoltResultRecord to stdout in csv format
func writeResultsCsv(results []*DoltResultRecord) (err error) {
csvWriter := csv.NewWriter(os.Stdout)
// write header
headers := fromResultCsvHeaders()
if err := csvWriter.Write(headers); err != nil {
return err
}
// write rows
for _, r := range results {
row := make([]string, 0)
for _, field := range headers {
val, err := fromHeaderColumnValue(field, r)
if err != nil {
return err
}
row = append(row, val)
}
err = csvWriter.Write(row)
if err != nil {
return err
}
}
csvWriter.Flush()
if err := csvWriter.Error(); err != nil {
return err
}
return
}