You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudberry.apache.org by dj...@apache.org on 2026/09/02 01:58:36 UTC
(cloudberry-backup) 05/40: Add end-to-end tests for gpbackman functionality.
This is an automated email from the ASF dual-hosted git repository.
tuhaihe pushed a commit to branch REL_2_STABLE
in repository https://gitbox.apache.org/repos/asf/cloudberry-backup.git
commit 98848ab1236d5a9fbac8120e45c22441c3bca2ed
Author: woblerr <an...@gmail.com>
AuthorDate: Sun Mar 22 15:12:36 2026 +0300
Add end-to-end tests for gpbackman functionality.
Also add vscode to gitignore.
---
.gitignore | 6 +
end_to_end/end_to_end_suite_test.go | 36 ++
end_to_end/gpbackman_test.go | 719 ++++++++++++++++++++++++++++++++++++
3 files changed, 761 insertions(+)
diff --git a/.gitignore b/.gitignore
index 3bef3410..a6a328c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,12 @@ _testmain.go
gpbackup
gprestore
gpbackup_helper
+gpbackman
+
+!gpbackman/
# Logs
*.log
+
+# vscode
+.vscode/
diff --git a/end_to_end/end_to_end_suite_test.go b/end_to_end/end_to_end_suite_test.go
index 5738e924..2eed0150 100644
--- a/end_to_end/end_to_end_suite_test.go
+++ b/end_to_end/end_to_end_suite_test.go
@@ -1,6 +1,7 @@
package end_to_end_test
import (
+ "database/sql"
"encoding/csv"
"flag"
"fmt"
@@ -30,6 +31,7 @@ import (
"github.com/apache/cloudberry-go-libs/structmatcher"
"github.com/apache/cloudberry-go-libs/testhelper"
"github.com/blang/semver"
+ _ "github.com/mattn/go-sqlite3"
"github.com/pkg/errors"
"github.com/spf13/pflag"
@@ -69,6 +71,7 @@ var (
schema2TupleCounts map[string]int
backupDir string
segmentCount int
+ gpbackmanPath string
)
const (
@@ -463,6 +466,38 @@ func moveSegmentBackupFiles(tarBaseName string, extractDirectory string, isMulti
}
}
+// gpbackman helpers
+
+func gpbackman(args ...string) []byte {
+ command := exec.Command(gpbackmanPath, args...)
+ return mustRunCommand(command)
+}
+
+func gpbackmanWithError(args ...string) ([]byte, error) {
+ command := exec.Command(gpbackmanPath, args...)
+ return command.CombinedOutput()
+}
+
+func getHistoryDBPathForCluster() string {
+ mdd := backupCluster.GetDirForContent(-1)
+ return path.Join(mdd, "gpbackup_history.db")
+}
+
+// queryHistoryDB runs an SQL query against gpbackup_history.db using database/sql and returns the trimmed output.
+func queryHistoryDB(historyDB string, query string) string {
+ db, err := sql.Open("sqlite3", historyDB)
+ Expect(err).ToNot(HaveOccurred())
+ defer db.Close()
+
+ var result string
+ err = db.QueryRow(query).Scan(&result)
+ if err == sql.ErrNoRows {
+ return ""
+ }
+ Expect(err).ToNot(HaveOccurred())
+ return strings.TrimSpace(result)
+}
+
func TestEndToEnd(t *testing.T) {
format.MaxLength = 0
RegisterFailHandler(Fail)
@@ -544,6 +579,7 @@ options:
gprestorePath = fmt.Sprintf("%s/gprestore", binDir)
backupHelperPath = fmt.Sprintf("%s/gpbackup_helper", binDir)
restoreHelperPath = backupHelperPath
+ gpbackmanPath = fmt.Sprintf("%s/gpbackman", binDir)
}
segConfig := cluster.MustGetSegmentConfiguration(backupConn)
backupCluster = cluster.NewCluster(segConfig)
diff --git a/end_to_end/gpbackman_test.go b/end_to_end/gpbackman_test.go
new file mode 100644
index 00000000..6db59af4
--- /dev/null
+++ b/end_to_end/gpbackman_test.go
@@ -0,0 +1,719 @@
+package end_to_end_test
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/apache/cloudberry-backup/filepath"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+// countBackupInfoLines counts the number of backup entry rows in backup-info
+// output. Data rows contain '|' separators but do not contain the header
+// label "TIMESTAMP".
+func countBackupInfoLines(output []byte) int {
+ count := 0
+ for _, line := range strings.Split(string(output), "\n") {
+ trimmed := strings.TrimSpace(line)
+ if trimmed == "" {
+ continue
+ }
+ if strings.Contains(trimmed, "|") &&
+ !strings.Contains(trimmed, "TIMESTAMP") &&
+ !isSeparatorLine(trimmed) {
+ count++
+ }
+ }
+ return count
+}
+
+// isSeparatorLine returns true for lines like "---+---+---".
+func isSeparatorLine(line string) bool {
+ for _, c := range line {
+ if c != '-' && c != '+' && c != ' ' {
+ return false
+ }
+ }
+ return true
+}
+
+var _ = Describe("gpbackman end to end tests", func() {
+
+ // ------------------------------------------------------------------ //
+ // backup-info
+ // ------------------------------------------------------------------ //
+ Describe("backup-info", func() {
+ var (
+ historyDB string
+ timestampMap map[string]string
+ )
+
+ BeforeEach(func() {
+ end_to_end_setup()
+ historyDB = getHistoryDBPathForCluster()
+ timestampMap = make(map[string]string)
+
+ // 1. Full local backup (with --leaf-partition-data for incremental compatibility)
+ output := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--leaf-partition-data")
+ timestampMap["full_local"] = getBackupTimestamp(string(output))
+
+ // 2. Full local backup with --include-table
+ output = gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--include-table", "public.foo")
+ timestampMap["full_include_table"] = getBackupTimestamp(string(output))
+
+ // 3. Full local backup with --exclude-schema
+ output = gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--exclude-schema", "schema2")
+ timestampMap["full_exclude_schema"] = getBackupTimestamp(string(output))
+
+ // 4. Incremental local backup (depends on full_local)
+ output = gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--incremental",
+ "--leaf-partition-data")
+ timestampMap["incremental"] = getBackupTimestamp(string(output))
+
+ // 5. Metadata-only backup
+ output = gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--metadata-only")
+ timestampMap["metadata_only"] = getBackupTimestamp(string(output))
+ })
+
+ AfterEach(func() {
+ end_to_end_teardown()
+ })
+
+ It("lists all backups", func() {
+ output := gpbackman(
+ "backup-info",
+ "--history-db", historyDB,
+ )
+ lines := countBackupInfoLines(output)
+ Expect(lines).To(BeNumerically(">=", 5),
+ fmt.Sprintf("Expected at least 5 backup entries, got %d.\nOutput:\n%s",
+ lines, string(output)))
+ })
+
+ It("filters by type full", func() {
+ output := gpbackman(
+ "backup-info",
+ "--history-db", historyDB,
+ "--type", "full",
+ )
+ Expect(string(output)).To(ContainSubstring("full"))
+ Expect(string(output)).ToNot(ContainSubstring("incremental"))
+ })
+
+ It("filters by type incremental", func() {
+ output := gpbackman(
+ "backup-info",
+ "--history-db", historyDB,
+ "--type", "incremental",
+ )
+ Expect(string(output)).To(ContainSubstring("incremental"))
+ lines := countBackupInfoLines(output)
+ Expect(lines).To(BeNumerically(">=", 1),
+ "Expected at least 1 incremental backup")
+ })
+
+ It("filters by type metadata-only", func() {
+ output := gpbackman(
+ "backup-info",
+ "--history-db", historyDB,
+ "--type", "metadata-only",
+ )
+ Expect(string(output)).To(ContainSubstring("metadata-only"))
+ lines := countBackupInfoLines(output)
+ Expect(lines).To(BeNumerically(">=", 1),
+ "Expected at least 1 metadata-only backup")
+ })
+
+ It("filters by include-table", func() {
+ output := gpbackman(
+ "backup-info",
+ "--history-db", historyDB,
+ "--table", "public.foo",
+ )
+ Expect(string(output)).To(ContainSubstring(timestampMap["full_include_table"]))
+ })
+
+ It("filters by exclude-schema", func() {
+ output := gpbackman(
+ "backup-info",
+ "--history-db", historyDB,
+ "--schema", "schema2",
+ "--exclude",
+ )
+ Expect(string(output)).To(ContainSubstring(timestampMap["full_exclude_schema"]))
+ })
+
+ It("shows backup chain with --timestamp", func() {
+ output := gpbackman(
+ "backup-info",
+ "--history-db", historyDB,
+ "--timestamp", timestampMap["full_local"],
+ )
+ outputStr := string(output)
+ Expect(outputStr).To(ContainSubstring(timestampMap["full_local"]),
+ "Expected the specified backup timestamp in the output")
+ })
+
+ It("shows detail with --timestamp --detail", func() {
+ output := gpbackman(
+ "backup-info",
+ "--history-db", historyDB,
+ "--timestamp", timestampMap["incremental"],
+ "--detail",
+ )
+ Expect(string(output)).To(Or(
+ ContainSubstring("OBJECT FILTERING"),
+ ContainSubstring("object filtering"),
+ ))
+ })
+
+ It("shows detail for all backups with --detail", func() {
+ output := gpbackman(
+ "backup-info",
+ "--history-db", historyDB,
+ "--detail",
+ )
+ Expect(string(output)).To(Or(
+ ContainSubstring("OBJECT FILTERING"),
+ ContainSubstring("object filtering"),
+ ))
+ Expect(string(output)).To(ContainSubstring("public.foo"))
+ })
+
+ It("rejects incompatible flags --timestamp with --type", func() {
+ _, err := gpbackmanWithError(
+ "backup-info",
+ "--history-db", historyDB,
+ "--timestamp", timestampMap["full_local"],
+ "--type", "full",
+ )
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("rejects invalid timestamp format", func() {
+ _, err := gpbackmanWithError(
+ "backup-info",
+ "--history-db", historyDB,
+ "--timestamp", "invalid",
+ )
+ Expect(err).To(HaveOccurred())
+ })
+ })
+
+ // ------------------------------------------------------------------ //
+ // report-info
+ // ------------------------------------------------------------------ //
+ Describe("report-info", func() {
+ var (
+ historyDB string
+ timestampMap map[string]string
+ )
+
+ BeforeEach(func() {
+ end_to_end_setup()
+ historyDB = getHistoryDBPathForCluster()
+ timestampMap = make(map[string]string)
+
+ // 1. Full local backup
+ output := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestampMap["full_local"] = getBackupTimestamp(string(output))
+
+ // 2. Plugin backup using example_plugin
+ copyPluginToAllHosts(backupConn, examplePluginExec)
+ output = gpbackup(gpbackupPath, backupHelperPath,
+ "--plugin-config", examplePluginTestConfig)
+ timestampMap["plugin"] = getBackupTimestamp(string(output))
+ })
+
+ AfterEach(func() {
+ end_to_end_teardown()
+ })
+
+ It("displays local backup report with --backup-dir", func() {
+ output := gpbackman(
+ "report-info",
+ "--history-db", historyDB,
+ "--timestamp", timestampMap["full_local"],
+ "--backup-dir", backupDir,
+ )
+ Expect(string(output)).To(ContainSubstring("Backup Report"))
+ Expect(string(output)).To(ContainSubstring(timestampMap["full_local"]))
+ })
+
+ It("displays local backup report without --backup-dir", func() {
+ output := gpbackman(
+ "report-info",
+ "--history-db", historyDB,
+ "--timestamp", timestampMap["full_local"],
+ )
+ Expect(string(output)).To(ContainSubstring("Backup Report"))
+ })
+
+ It("displays plugin backup report", func() {
+ ts := timestampMap["plugin"]
+ reportDir := fmt.Sprintf("/tmp/plugin_dest/%s/%s", ts[:8], ts)
+ output := gpbackman(
+ "report-info",
+ "--history-db", historyDB,
+ "--timestamp", ts,
+ "--plugin-config", examplePluginTestConfig,
+ "--plugin-report-file-path", reportDir,
+ )
+ Expect(string(output)).To(ContainSubstring("Backup Report"))
+ Expect(string(output)).To(ContainSubstring(ts))
+ })
+
+ It("rejects --plugin-report-file-path without --plugin-config", func() {
+ _, err := gpbackmanWithError(
+ "report-info",
+ "--history-db", historyDB,
+ "--timestamp", timestampMap["full_local"],
+ "--plugin-report-file-path", "/tmp/fake_report",
+ )
+ Expect(err).To(HaveOccurred())
+ })
+ })
+
+ // ------------------------------------------------------------------ //
+ // backup-delete
+ // ------------------------------------------------------------------ //
+ Describe("backup-delete", func() {
+ var historyDB string
+
+ BeforeEach(func() {
+ end_to_end_setup()
+ historyDB = getHistoryDBPathForCluster()
+ })
+
+ AfterEach(func() {
+ end_to_end_teardown()
+ })
+
+ It("deletes a local backup by timestamp", func() {
+ output := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp := getBackupTimestamp(string(output))
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", timestamp,
+ "--backup-dir", backupDir,
+ )
+
+ dateDeleted := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT date_deleted FROM backups WHERE timestamp = '%s'", timestamp))
+ Expect(dateDeleted).ToNot(BeEmpty())
+ Expect(dateDeleted).ToNot(Equal("In progress"))
+
+ fpInfo := filepath.NewFilePathInfo(backupCluster, backupDir, timestamp, "", false)
+ backupDirCoordinator := fpInfo.GetDirForContent(-1)
+ Expect(backupDirCoordinator).ToNot(BeADirectory())
+ })
+
+ It("deletes a local backup without --backup-dir", func() {
+ output := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp := getBackupTimestamp(string(output))
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", timestamp,
+ )
+
+ dateDeleted := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT date_deleted FROM backups WHERE timestamp = '%s'", timestamp))
+ Expect(dateDeleted).ToNot(BeEmpty())
+ })
+
+ It("deletes with --cascade for incremental chain", func() {
+ fullOutput := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--leaf-partition-data")
+ fullTimestamp := getBackupTimestamp(string(fullOutput))
+
+ incrOutput := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--incremental",
+ "--leaf-partition-data")
+ incrTimestamp := getBackupTimestamp(string(incrOutput))
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", fullTimestamp,
+ "--backup-dir", backupDir,
+ "--cascade",
+ )
+
+ fullDeleted := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT date_deleted FROM backups WHERE timestamp = '%s'", fullTimestamp))
+ Expect(fullDeleted).ToNot(BeEmpty())
+
+ incrDeleted := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT date_deleted FROM backups WHERE timestamp = '%s'", incrTimestamp))
+ Expect(incrDeleted).ToNot(BeEmpty())
+ })
+
+ It("deletes a plugin backup", func() {
+ copyPluginToAllHosts(backupConn, examplePluginExec)
+
+ output := gpbackup(gpbackupPath, backupHelperPath,
+ "--plugin-config", examplePluginTestConfig)
+ timestamp := getBackupTimestamp(string(output))
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", timestamp,
+ "--plugin-config", examplePluginTestConfig,
+ )
+
+ dateDeleted := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT date_deleted FROM backups WHERE timestamp = '%s'", timestamp))
+ Expect(dateDeleted).ToNot(BeEmpty())
+ Expect(dateDeleted).ToNot(Equal("In progress"))
+ })
+
+ It("fails for non-existent timestamp", func() {
+ _, err := gpbackmanWithError(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", "29991231235959",
+ "--backup-dir", backupDir,
+ )
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("skips already-deleted backup without --force", func() {
+ output := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp := getBackupTimestamp(string(output))
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", timestamp,
+ "--backup-dir", backupDir,
+ )
+
+ // Second delete without --force should succeed without error.
+ // gpbackman silently skips already-deleted backups (logged at debug level).
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", timestamp,
+ "--backup-dir", backupDir,
+ )
+
+ dateDeleted := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT date_deleted FROM backups WHERE timestamp = '%s'", timestamp))
+ Expect(dateDeleted).ToNot(BeEmpty(),
+ "Backup should still be marked as deleted after second delete attempt")
+ })
+
+ It("re-deletes with --force", func() {
+ output := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp := getBackupTimestamp(string(output))
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", timestamp,
+ "--backup-dir", backupDir,
+ )
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", timestamp,
+ "--backup-dir", backupDir,
+ "--force",
+ "--ignore-errors",
+ )
+
+ dateDeleted := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT date_deleted FROM backups WHERE timestamp = '%s'", timestamp))
+ Expect(dateDeleted).ToNot(BeEmpty())
+ })
+ })
+
+ // ------------------------------------------------------------------ //
+ // backup-delete: local file cleanup on segments
+ // ------------------------------------------------------------------ //
+ Describe("backup-delete local file cleanup", func() {
+ var historyDB string
+
+ BeforeEach(func() {
+ end_to_end_setup()
+ historyDB = getHistoryDBPathForCluster()
+ })
+
+ AfterEach(func() {
+ end_to_end_teardown()
+ })
+
+ It("removes backup files after deletion", func() {
+ output := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--single-backup-dir")
+ timestamp := getBackupTimestamp(string(output))
+
+ fpInfo := filepath.NewFilePathInfo(backupCluster, backupDir, timestamp, "", true)
+ backupTimestampDir := fpInfo.GetDirForContent(-1)
+ Expect(backupTimestampDir).To(BeADirectory(),
+ "Backup directory should exist before deletion")
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", timestamp,
+ "--backup-dir", backupDir,
+ )
+
+ Expect(backupTimestampDir).ToNot(BeADirectory(),
+ "Backup directory should be removed after deletion")
+ })
+ })
+
+ // ------------------------------------------------------------------ //
+ // backup-clean
+ // ------------------------------------------------------------------ //
+ Describe("backup-clean", func() {
+ var historyDB string
+
+ BeforeEach(func() {
+ end_to_end_setup()
+ historyDB = getHistoryDBPathForCluster()
+ })
+
+ AfterEach(func() {
+ end_to_end_teardown()
+ })
+
+ It("cleans local backups with --before-timestamp", func() {
+ output1 := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp1 := getBackupTimestamp(string(output1))
+
+ output2 := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp2 := getBackupTimestamp(string(output2))
+
+ gpbackman(
+ "backup-clean",
+ "--history-db", historyDB,
+ "--before-timestamp", timestamp2,
+ "--backup-dir", backupDir,
+ )
+
+ dateDeleted1 := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT date_deleted FROM backups WHERE timestamp = '%s'", timestamp1))
+ Expect(dateDeleted1).ToNot(BeEmpty(),
+ fmt.Sprintf("Expected backup %s to be deleted", timestamp1))
+ })
+
+ It("cleans local backups with --after-timestamp", func() {
+ output1 := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp1 := getBackupTimestamp(string(output1))
+
+ output2 := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp2 := getBackupTimestamp(string(output2))
+
+ gpbackman(
+ "backup-clean",
+ "--history-db", historyDB,
+ "--after-timestamp", timestamp1,
+ "--backup-dir", backupDir,
+ )
+
+ dateDeleted2 := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT date_deleted FROM backups WHERE timestamp = '%s'", timestamp2))
+ Expect(dateDeleted2).ToNot(BeEmpty(),
+ fmt.Sprintf("Expected backup %s to be deleted", timestamp2))
+ })
+
+ It("cleans plugin backups with --before-timestamp", func() {
+ copyPluginToAllHosts(backupConn, examplePluginExec)
+
+ output := gpbackup(gpbackupPath, backupHelperPath,
+ "--plugin-config", examplePluginTestConfig)
+ timestamp := getBackupTimestamp(string(output))
+
+ gpbackman(
+ "backup-clean",
+ "--history-db", historyDB,
+ "--before-timestamp", timestamp,
+ "--plugin-config", examplePluginTestConfig,
+ )
+
+ countStr := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT count(*) FROM backups WHERE timestamp = '%s' AND date_deleted = ''",
+ timestamp))
+ Expect(countStr).ToNot(BeEmpty())
+ })
+
+ It("cleans local backups with --cascade for incremental chain", func() {
+ fullOutput := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--leaf-partition-data")
+ fullTimestamp := getBackupTimestamp(string(fullOutput))
+
+ _ = gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--incremental",
+ "--leaf-partition-data")
+
+ gpbackman(
+ "backup-clean",
+ "--history-db", historyDB,
+ "--before-timestamp", fullTimestamp,
+ "--backup-dir", backupDir,
+ "--cascade",
+ )
+ // Success if no error was thrown
+ })
+ })
+
+ // ------------------------------------------------------------------ //
+ // history-clean
+ // ------------------------------------------------------------------ //
+ Describe("history-clean", func() {
+ var historyDB string
+
+ BeforeEach(func() {
+ end_to_end_setup()
+ historyDB = getHistoryDBPathForCluster()
+ })
+
+ AfterEach(func() {
+ end_to_end_teardown()
+ })
+
+ It("cleans deleted backup entries from history DB", func() {
+ output := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp := getBackupTimestamp(string(output))
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", timestamp,
+ "--backup-dir", backupDir,
+ )
+
+ dateDeleted := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT date_deleted FROM backups WHERE timestamp = '%s'", timestamp))
+ Expect(dateDeleted).ToNot(BeEmpty())
+
+ // --before-timestamp uses strictly less than (<), so use a
+ // far-future cutoff to include the target timestamp.
+ gpbackman(
+ "history-clean",
+ "--history-db", historyDB,
+ "--before-timestamp", "99991231235959",
+ )
+
+ countStr := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT count(*) FROM backups WHERE timestamp = '%s'", timestamp))
+ Expect(countStr).To(Equal("0"),
+ fmt.Sprintf("Expected backup %s to be removed from history DB", timestamp))
+ })
+
+ It("cleans with --older-than-days 0", func() {
+ output := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp := getBackupTimestamp(string(output))
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", timestamp,
+ "--backup-dir", backupDir,
+ )
+
+ gpbackman(
+ "history-clean",
+ "--history-db", historyDB,
+ "--older-than-days", "0",
+ )
+
+ countStr := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT count(*) FROM backups WHERE timestamp = '%s'", timestamp))
+ Expect(countStr).To(Equal("0"))
+ })
+
+ It("leaves non-deleted entries intact", func() {
+ output1 := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp1 := getBackupTimestamp(string(output1))
+
+ output2 := gpbackup(gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ timestamp2 := getBackupTimestamp(string(output2))
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", timestamp1,
+ "--backup-dir", backupDir,
+ )
+
+ gpbackman(
+ "history-clean",
+ "--history-db", historyDB,
+ "--older-than-days", "0",
+ )
+
+ count1 := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT count(*) FROM backups WHERE timestamp = '%s'", timestamp1))
+ Expect(count1).To(Equal("0"),
+ "Deleted backup should be removed from history")
+
+ count2 := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT count(*) FROM backups WHERE timestamp = '%s'", timestamp2))
+ Expect(count2).To(Equal("1"),
+ "Non-deleted backup should remain in history")
+ })
+ })
+
+ // ------------------------------------------------------------------ //
+ // version & help
+ // ------------------------------------------------------------------ //
+ Describe("gpbackman --version", func() {
+ It("prints version information", func() {
+ output := gpbackman("--version")
+ Expect(string(output)).To(ContainSubstring("gpbackman"))
+ })
+ })
+
+ Describe("gpbackman --help", func() {
+ It("prints help for all subcommands", func() {
+ for _, subcmd := range []string{
+ "backup-info", "backup-delete", "backup-clean",
+ "history-clean", "report-info",
+ } {
+ output := gpbackman(subcmd, "--help")
+ Expect(string(output)).To(ContainSubstring(subcmd),
+ fmt.Sprintf("Help for %s should mention the command name", subcmd))
+ }
+ })
+ })
+})
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cloudberry.apache.org
For additional commands, e-mail: commits-help@cloudberry.apache.org