From 32f64d2fb40973276bbca10d45a08a8be93d7ff1 Mon Sep 17 00:00:00 2001 From: Noy Date: Tue, 26 Nov 2024 19:10:53 +0800 Subject: [PATCH] feat: complete postgres client compatibility tests (#111) --- compatibility/pg/clean.sh | 7 ++ compatibility/pg/csharp/PGTest.cs | 174 +++++++++++++++++++++++++ compatibility/pg/csharp/PGTest.csproj | 12 ++ compatibility/pg/go/pg.go | 175 ++++++++++++++++++++++++++ compatibility/pg/r/PGTest.R | 113 +++++++++++++++++ compatibility/pg/rust/Cargo.toml | 9 ++ compatibility/pg/rust/pg_test.rs | 124 ++++++++++++++++++ compatibility/pg/test.bats | 96 ++++++++------ compatibility/pg/test.data | 2 +- 9 files changed, 672 insertions(+), 40 deletions(-) create mode 100644 compatibility/pg/clean.sh create mode 100644 compatibility/pg/csharp/PGTest.cs create mode 100644 compatibility/pg/csharp/PGTest.csproj create mode 100644 compatibility/pg/go/pg.go create mode 100644 compatibility/pg/r/PGTest.R create mode 100644 compatibility/pg/rust/Cargo.toml create mode 100644 compatibility/pg/rust/pg_test.rs diff --git a/compatibility/pg/clean.sh b/compatibility/pg/clean.sh new file mode 100644 index 00000000..b8fc0a67 --- /dev/null +++ b/compatibility/pg/clean.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +rm -rf ./c/pg_test \ + ./csharp/bin ./csharp/obj \ + ./go/pg \ + ./java/*.class \ + ./rust/target ./rust/Cargo.lock \ No newline at end of file diff --git a/compatibility/pg/csharp/PGTest.cs b/compatibility/pg/csharp/PGTest.cs new file mode 100644 index 00000000..78da9ecb --- /dev/null +++ b/compatibility/pg/csharp/PGTest.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Data; +using Microsoft.Data.SqlClient; +using System.IO; + +public class PGTest +{ + public class Tests + { + private SqlConnection conn; + private SqlCommand cmd; + private List tests = new List(); + + public void Connect(string ip, int port, string user, string password) + { + try + { + string connectionString = $"Server={ip},{port};User Id={user};Password={password};"; + conn = new SqlConnection(connectionString); + conn.Open(); + cmd = conn.CreateCommand(); + cmd.CommandType = CommandType.Text; + } + catch (SqlException e) + { + throw new Exception(e.Message); + } + } + + public void Disconnect() + { + try + { + cmd.Dispose(); + conn.Close(); + } + catch (SqlException e) + { + throw new Exception(e.Message); + } + } + + public void AddTest(string query, string[][] expectedResults) + { + tests.Add(new Test(query, expectedResults)); + } + + public bool RunTests() + { + foreach (var test in tests) + { + if (!test.Run(cmd)) + { + return false; + } + } + return true; + } + + public void ReadTestsFromFile(string filename) + { + try + { + using (var reader = new StreamReader(filename)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + if (string.IsNullOrWhiteSpace(line)) continue; + string query = line; + var results = new List(); + while ((line = reader.ReadLine()) != null && !string.IsNullOrWhiteSpace(line)) + { + results.Add(line.Split(',')); + } + string[][] expectedResults = results.ToArray(); + AddTest(query, expectedResults); + } + } + } + catch (IOException e) + { + Console.Error.WriteLine(e.Message); + Environment.Exit(1); + } + } + + public class Test + { + private string query; + private string[][] expectedResults; + + public Test(string query, string[][] expectedResults) + { + this.query = query; + this.expectedResults = expectedResults; + } + + public bool Run(SqlCommand cmd) + { + try + { + Console.WriteLine("Running test: " + query); + cmd.CommandText = query; + using (var reader = cmd.ExecuteReader()) + { + if (!reader.HasRows) + { + Console.WriteLine("Returns 0 rows"); + return expectedResults.Length == 0; + } + if (reader.FieldCount != expectedResults[0].Length) + { + Console.Error.WriteLine($"Expected {expectedResults[0].Length} columns, got {reader.FieldCount}"); + return false; + } + int rows = 0; + while (reader.Read()) + { + for (int col = 0; col < expectedResults[rows].Length; col++) + { + string result = reader.GetString(col); + if (expectedResults[rows][col] != result) + { + Console.Error.WriteLine($"Expected:\n'{expectedResults[rows][col]}'"); + Console.Error.WriteLine($"Result:\n'{result}'\nRest of the results:"); + while (reader.Read()) + { + Console.Error.WriteLine(reader.GetString(0)); + } + return false; + } + } + rows++; + } + Console.WriteLine("Returns " + rows + " rows"); + if (rows != expectedResults.Length) + { + Console.Error.WriteLine($"Expected {expectedResults.Length} rows"); + return false; + } + return true; + } + } + catch (SqlException e) + { + Console.Error.WriteLine(e.Message); + return false; + } + } + } + } + + public static void Main(string[] args) + { + if (args.Length < 5) + { + Console.Error.WriteLine("Usage: PGTest "); + Environment.Exit(1); + } + + var tests = new Tests(); + tests.Connect(args[0], int.Parse(args[1]), args[2], args[3]); + tests.ReadTestsFromFile(args[4]); + + if (!tests.RunTests()) + { + tests.Disconnect(); + Environment.Exit(1); + } + tests.Disconnect(); + } +} \ No newline at end of file diff --git a/compatibility/pg/csharp/PGTest.csproj b/compatibility/pg/csharp/PGTest.csproj new file mode 100644 index 00000000..ef692a92 --- /dev/null +++ b/compatibility/pg/csharp/PGTest.csproj @@ -0,0 +1,12 @@ + + + + Exe + net8.0 + + + + + + + \ No newline at end of file diff --git a/compatibility/pg/go/pg.go b/compatibility/pg/go/pg.go new file mode 100644 index 00000000..bfc96ec4 --- /dev/null +++ b/compatibility/pg/go/pg.go @@ -0,0 +1,175 @@ +package main + +import ( + "bufio" + "database/sql" + "fmt" + "os" + "strconv" + "strings" + + _ "github.com/lib/pq" +) + +type Test struct { + query string + expectedResults [][]string +} + +type Tests struct { + conn *sql.DB + tests []Test +} + +func (t *Tests) connect(ip string, port int, user, password string) { + connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=postgres sslmode=disable", ip, port, user, password) + var err error + t.conn, err = sql.Open("postgres", connStr) + if err != nil { + panic(err) + } +} + +func (t *Tests) disconnect() { + err := t.conn.Close() + if err != nil { + panic(err) + } +} + +func (t *Tests) addTest(query string, expectedResults [][]string) { + t.tests = append(t.tests, Test{query, expectedResults}) +} + +func (t *Tests) readTestsFromFile(filename string) { + file, err := os.Open(filename) + if err != nil { + panic(err) + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + panic(err) + } + }(file) + + scanner := bufio.NewScanner(file) + var query string + var results [][]string + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + if query != "" { + t.addTest(query, results) + query = "" + results = nil + } + } else if query == "" { + query = line + } else { + results = append(results, strings.Split(line, ",")) + } + } + if query != "" { + t.addTest(query, results) + } + if err := scanner.Err(); err != nil { + panic(err) + } +} + +func (t *Tests) runTests() bool { + for _, test := range t.tests { + if !t.runTest(test) { + return false + } + } + return true +} + +func (t *Tests) runTest(test Test) bool { + fmt.Println("Running test:", test.query) + rows, err := t.conn.Query(test.query) + if err != nil { + fmt.Println("Error executing query:", err) + return false + } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + panic(err) + } + }(rows) + + columns, err := rows.Columns() + if err != nil { + fmt.Println("Error getting columns:", err) + return false + } + + if len(test.expectedResults) == 0 { + fmt.Println("Returns 0 rows") + return len(columns) == 0 + } + + if len(columns) != len(test.expectedResults[0]) { + fmt.Printf("Expected %d columns, got %d\n", len(test.expectedResults[0]), len(columns)) + return false + } + + var rowCount int + for rows.Next() { + row := make([]string, len(columns)) + rowPointers := make([]interface{}, len(columns)) + for i := range row { + rowPointers[i] = &row[i] + } + if err := rows.Scan(rowPointers...); err != nil { + fmt.Println("Error scanning row:", err) + return false + } + + for i, expected := range test.expectedResults[rowCount] { + if row[i] != expected { + fmt.Printf("Expected: '%s', got: '%s'\n", expected, row[i]) + return false + } + } + rowCount++ + } + + if rowCount != len(test.expectedResults) { + fmt.Printf("Expected %d rows, got %d\n", len(test.expectedResults), rowCount) + return false + } + + fmt.Printf("Returns %d rows\n", rowCount) + return true +} + +func main() { + if len(os.Args) < 6 { + fmt.Println("Usage: pg_test ") + os.Exit(1) + } + + ip := os.Args[1] + port, ok := strconv.Atoi(os.Args[2]) + if ok != nil { + fmt.Println("Invalid port:", os.Args[2]) + os.Exit(1) + } + user := os.Args[3] + password := os.Args[4] + testFile := os.Args[5] + + tests := &Tests{} + tests.connect(ip, port, user, password) + tests.readTestsFromFile(testFile) + + if !tests.runTests() { + tests.disconnect() + os.Exit(1) + } + tests.disconnect() +} diff --git a/compatibility/pg/r/PGTest.R b/compatibility/pg/r/PGTest.R new file mode 100644 index 00000000..32e581ad --- /dev/null +++ b/compatibility/pg/r/PGTest.R @@ -0,0 +1,113 @@ +library(RPostgres) + +Tests <- setRefClass( + "Tests", + fields = list( + conn = "ANY", + tests = "list" + ), + methods = list( + connect = function(ip, port, user, password) { + conn <<- dbConnect( + Postgres(), + host = ip, + port = port, + user = user, + password = password, + dbname = "postgres" + ) + }, + disconnect = function() { + dbDisconnect(conn) + }, + addTest = function(query, expectedResults) { + tests <<- c(tests, list(Test$new(query, expectedResults))) + }, + runTests = function() { + for (test in tests) { + if (!test$run(conn)) { + return(FALSE) + } + } + return(TRUE) + }, + readTestsFromFile = function(filename) { + lines <- readLines(filename) + i <- 1 + while (i <= length(lines)) { + if (trimws(lines[i]) == "") { + i <- i + 1 + next + } + query <- lines[i] + i <- i + 1 + results <- list() + while (i <= length(lines) && trimws(lines[i]) != "") { + results <- c(results, list(strsplit(lines[i], ",")[[1]])) + i <- i + 1 + } + addTest(query, results) + } + } + ) +) + +Test <- setRefClass( + "Test", + fields = list( + query = "character", + expectedResults = "list" + ), + methods = list( + initialize = function(query, expectedResults) { + query <<- query + expectedResults <<- expectedResults + }, + run = function(conn) { + cat("Running test:", query, "\n") + res <- dbSendQuery(conn, query) + fetched <- dbFetch(res) + dbClearResult(res) + if (length(expectedResults) == 0) { + if (nrow(fetched) == 0 || ncol(fetched) == 0) { + return(TRUE) + } else { + return(FALSE) + } + } + if (ncol(fetched) != length(expectedResults[[1]])) { + cat("Expected", length(expectedResults[[1]]), "columns, got", ncol(fetched), "\n") + return(FALSE) + } + if (nrow(fetched) != length(expectedResults)) { + cat("Expected", length(expectedResults), "rows, got", nrow(fetched), "\n") + return(FALSE) + } + for (i in seq_len(nrow(fetched))) { + for (j in seq_len(ncol(fetched))) { + if (as.character(fetched[i, j]) != expectedResults[[i]][j]) { + cat("Expected:", expectedResults[[i]][j], "Got:", fetched[i, j], "\n") + return(FALSE) + } + } + } + return(TRUE) + } + ) +) + +args <- commandArgs(trailingOnly = TRUE) +if (length(args) < 5) { + cat("Usage: Rscript PGTest.R \n") + quit(status = 1) +} + +tests <- Tests$new() +tests$connect(args[1], as.integer(args[2]), args[3], args[4]) +tests$readTestsFromFile(args[5]) + +if (!tests$runTests()) { + tests$disconnect() + quit(status = 1) +} +tests$disconnect() \ No newline at end of file diff --git a/compatibility/pg/rust/Cargo.toml b/compatibility/pg/rust/Cargo.toml new file mode 100644 index 00000000..301b0027 --- /dev/null +++ b/compatibility/pg/rust/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pg_test" + +[dependencies] +postgres = "*" + +[[bin]] +name = "pg_test" +path = "./pg_test.rs" \ No newline at end of file diff --git a/compatibility/pg/rust/pg_test.rs b/compatibility/pg/rust/pg_test.rs new file mode 100644 index 00000000..fb862175 --- /dev/null +++ b/compatibility/pg/rust/pg_test.rs @@ -0,0 +1,124 @@ +use std::fs::File; +use std::io::{self, BufRead, BufReader}; +use std::process::exit; + +extern crate postgres; +use postgres::{Client, NoTls, Error}; + +struct Test { + query: String, + expected_results: Vec>, +} + +impl Test { + fn new(query: String, expected_results: Vec>) -> Self { + Test { query, expected_results } + } + + fn run(&self, client: &mut Client) -> bool { + println!("Running test: {}", self.query); + match client.query(&self.query, &[]) { + Ok(rows) => { + if rows.is_empty() { + println!("Returns 0 rows"); + return self.expected_results.is_empty(); + } + if rows[0].columns().len() != self.expected_results[0].len() { + eprintln!("Expected {} columns, got {}", self.expected_results[0].len(), rows[0].columns().len()); + return false; + } + for (i, row) in rows.iter().enumerate() { + for (j, expected) in self.expected_results[i].iter().enumerate() { + let result: String = row.get(j); + if expected != &result { + eprintln!("Expected:\n'{}'", expected); + eprintln!("Result:\n'{}'\nRest of the results:", result); + for row in rows.iter().skip(i + 1) { + eprintln!("{}", row.get::(0)); + } + return false; + } + } + } + println!("Returns {} rows", rows.len()); + if rows.len() != self.expected_results.len() { + eprintln!("Expected {} rows", self.expected_results.len()); + return false; + } + true + } + Err(err) => { + eprintln!("{}", err); + false + } + } + } +} + +struct Tests { + client: Client, + tests: Vec, +} + +impl Tests { + fn new(ip: &str, port: u16, user: &str, password: &str) -> Result { + let conn_str = format!("host={} port={} user={} password={} dbname=postgres", ip, port, user, password); + let client = Client::connect(&conn_str, NoTls)?; + Ok(Tests { client, tests: Vec::new() }) + } + + fn add_test(&mut self, query: String, expected_results: Vec>) { + self.tests.push(Test::new(query, expected_results)); + } + + fn run_tests(&mut self) -> bool { + for test in &self.tests { + if !test.run(&mut self.client) { + return false; + } + } + true + } + + fn read_tests_from_file(&mut self, filename: &str) -> io::Result<()> { + let file = File::open(filename)?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + while let Some(Ok(line)) = lines.next() { + if line.trim().is_empty() { + continue; + } + let query = line; + let mut results = Vec::new(); + while let Some(Ok(line)) = lines.next() { + if line.trim().is_empty() { + break; + } + results.push(line.split(',').map(String::from).collect()); + } + self.add_test(query, results); + } + Ok(()) + } +} + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 6 { + eprintln!("Usage: {} ", args[0]); + exit(1); + } + + let ip = &args[1]; + let port: u16 = args[2].parse().expect("Invalid port number"); + let user = &args[3]; + let password = &args[4]; + let test_file = &args[5]; + + let mut tests = Tests::new(ip, port, user, password).expect("Failed to connect to database"); + tests.read_tests_from_file(test_file).expect("Failed to read test file"); + + if !tests.run_tests() { + exit(1); + } +} \ No newline at end of file diff --git a/compatibility/pg/test.bats b/compatibility/pg/test.bats index caa88e2c..bab3858b 100644 --- a/compatibility/pg/test.bats +++ b/compatibility/pg/test.bats @@ -2,20 +2,33 @@ setup() { psql -h 127.0.0.1 -p 5432 -U postgres -c "DROP SCHEMA IF EXISTS test CASCADE;" + touch /tmp/test_pids } -@test "pg-c" { - gcc -o $BATS_TEST_DIRNAME/c/pg_test $BATS_TEST_DIRNAME/c/pg_test.c -I/usr/include/postgresql -lpq - run $BATS_TEST_DIRNAME/c/pg_test 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data - if [ "$status" -ne 0 ]; then - echo "$output" +custom_teardown="" + +set_custom_teardown() { + custom_teardown="$1" +} + +teardown() { + if [ -n "$custom_teardown" ]; then + eval "$custom_teardown" + custom_teardown="" fi - [ "$status" -eq 0 ] + + while read -r pid; do + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" + wait "$pid" 2>/dev/null + fi + done < /tmp/test_pids + rm /tmp/test_pids } -@test "pg-java" { - javac $BATS_TEST_DIRNAME/java/PGTest.java - run java -cp $BATS_TEST_DIRNAME/java:$BATS_TEST_DIRNAME/java/postgresql-42.7.4.jar PGTest 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data +start_process() { + run timeout 2m "$@" + echo $! >> /tmp/test_pids if [ "$status" -ne 0 ]; then echo "$output" echo "$stderr" @@ -23,47 +36,52 @@ setup() { [ "$status" -eq 0 ] } +@test "pg-c" { + start_process gcc -o $BATS_TEST_DIRNAME/c/pg_test $BATS_TEST_DIRNAME/c/pg_test.c -I/usr/include/postgresql -lpq + start_process $BATS_TEST_DIRNAME/c/pg_test 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data +} + +@test "pg-csharp" { + set_custom_teardown "sudo pkill -f dotnet" + start_process dotnet build $BATS_TEST_DIRNAME/csharp/PGTest.csproj -o $BATS_TEST_DIRNAME/csharp/bin + start_process dotnet $BATS_TEST_DIRNAME/csharp/bin/PGTest.dll 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data +} + +@test "pg-go" { + start_process go build -o $BATS_TEST_DIRNAME/go/pg $BATS_TEST_DIRNAME/go/pg.go + start_process $BATS_TEST_DIRNAME/go/pg 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data +} + +@test "pg-java" { + start_process javac $BATS_TEST_DIRNAME/java/PGTest.java + start_process java -cp $BATS_TEST_DIRNAME/java:$BATS_TEST_DIRNAME/java/postgresql-42.7.4.jar PGTest 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data +} + @test "pg-node" { - run node $BATS_TEST_DIRNAME/node/pg_test.js 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data - if [ "$status" -ne 0 ]; then - echo "$output" - echo "$stderr" - fi - [ "$status" -eq 0 ] + start_process node $BATS_TEST_DIRNAME/node/pg_test.js 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data } @test "pg-perl" { - run perl $BATS_TEST_DIRNAME/perl/pg_test.pl 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data - if [ "$status" -ne 0 ]; then - echo "$output" - echo "$stderr" - fi - [ "$status" -eq 0 ] + start_process perl $BATS_TEST_DIRNAME/perl/pg_test.pl 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data } @test "pg-php" { - run php $BATS_TEST_DIRNAME/php/pg_test.php 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data - if [ "$status" -ne 0 ]; then - echo "$output" - echo "$stderr" - fi - [ "$status" -eq 0 ] + start_process php $BATS_TEST_DIRNAME/php/pg_test.php 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data } @test "pg-python" { - run python3 $BATS_TEST_DIRNAME/python/pg_test.py 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data - if [ "$status" -ne 0 ]; then - echo "$output" - echo "$stderr" - fi - [ "$status" -eq 0 ] + start_process python3 $BATS_TEST_DIRNAME/python/pg_test.py 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data +} + +@test "pg-r" { + start_process Rscript $BATS_TEST_DIRNAME/r/PGTest.R 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data } @test "pg-ruby" { - run ruby $BATS_TEST_DIRNAME/ruby/pg_test.rb 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data - if [ "$status" -ne 0 ]; then - echo "$output" - echo "$stderr" - fi - [ "$status" -eq 0 ] + start_process ruby $BATS_TEST_DIRNAME/ruby/pg_test.rb 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data } + +@test "pg-rust" { + start_process cargo build --release --manifest-path $BATS_TEST_DIRNAME/rust/Cargo.toml + start_process $BATS_TEST_DIRNAME/rust/target/release/pg_test 127.0.0.1 5432 postgres "" $BATS_TEST_DIRNAME/test.data +} \ No newline at end of file diff --git a/compatibility/pg/test.data b/compatibility/pg/test.data index 122b2010..d14f2509 100644 --- a/compatibility/pg/test.data +++ b/compatibility/pg/test.data @@ -24,4 +24,4 @@ DELETE FROM test.tb1 WHERE id=1 SELECT * FROM test.tb1 2,2.2,b -DROP SCHEMA test CASCADE \ No newline at end of file +DROP SCHEMA test CASCADE