mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-07-27 00:37:14 +02:00
cc-lib changed JobData, ScopedJobStats and Job.Statistics from flat maps to structs (a .Metrics map plus array-valued Groups) to represent filesystem (and future interconnect) metric groups. Migrate all construction, indexing and iteration to the new API across the archive backends, metricstore query path, metric dispatcher, archiver, importer, tagger, taskmanager, repository and API layers. Semantics: DecodeJobStats now projects JobData.Groups into ScopedJobStats.Groups, and the archiver derives per-filesystem node-scope statistics into Job.Statistics.Groups. deepCopy, resampling and the metric/scope filter are group-aware. The metricstore internal storage (buffers, selector tree, checkpoint/parquet) is unchanged; all conversion stays at the LoadData/archive-codec seam. Note: requires the corresponding cc-lib release; bump the cc-lib dependency version once tagged (a local go.mod replace was used during development and is intentionally not committed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
400 lines
12 KiB
Go
400 lines
12 KiB
Go
// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
|
|
// All rights reserved. This file is part of cc-backend.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/ClusterCockpit/cc-backend/pkg/archive"
|
|
"github.com/ClusterCockpit/cc-lib/v2/schema"
|
|
"github.com/ClusterCockpit/cc-lib/v2/util"
|
|
)
|
|
|
|
// TestImportFileToSqlite tests importing jobs from file backend to SQLite backend
|
|
func TestImportFileToSqlite(t *testing.T) {
|
|
// Create temporary directories
|
|
tmpdir := t.TempDir()
|
|
srcArchive := filepath.Join(tmpdir, "src-archive")
|
|
dstDb := filepath.Join(tmpdir, "dst-archive.db")
|
|
|
|
// Copy test data to source archive
|
|
testDataPath := "../../pkg/archive/testdata/archive"
|
|
if _, err := os.Stat(testDataPath); os.IsNotExist(err) {
|
|
t.Skip("Test data not found, skipping integration test")
|
|
}
|
|
|
|
if err := util.CopyDir(testDataPath, srcArchive); err != nil {
|
|
t.Fatalf("Failed to copy test data: %s", err.Error())
|
|
}
|
|
|
|
// Initialize source backend (file)
|
|
srcConfig := fmt.Sprintf(`{"kind":"file","path":"%s"}`, srcArchive)
|
|
srcBackend, err := archive.InitBackend(json.RawMessage(srcConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize source backend: %s", err.Error())
|
|
}
|
|
|
|
// Initialize destination backend (sqlite)
|
|
dstConfig := fmt.Sprintf(`{"kind":"sqlite","db-path":"%s"}`, dstDb)
|
|
dstBackend, err := archive.InitBackend(json.RawMessage(dstConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize destination backend: %s", err.Error())
|
|
}
|
|
|
|
// Perform import
|
|
imported, failed, err := importArchive(srcBackend, dstBackend, srcConfig)
|
|
if err != nil {
|
|
t.Errorf("Import failed: %s", err.Error())
|
|
}
|
|
|
|
if imported == 0 {
|
|
t.Error("No jobs were imported")
|
|
}
|
|
|
|
if failed > 0 {
|
|
t.Errorf("%d jobs failed to import", failed)
|
|
}
|
|
|
|
t.Logf("Successfully imported %d jobs", imported)
|
|
|
|
// Verify jobs exist in destination
|
|
// Count jobs in source
|
|
srcCount := 0
|
|
for range srcBackend.Iter(false) {
|
|
srcCount++
|
|
}
|
|
|
|
// Count jobs in destination
|
|
dstCount := 0
|
|
for range dstBackend.Iter(false) {
|
|
dstCount++
|
|
}
|
|
|
|
if srcCount != dstCount {
|
|
t.Errorf("Job count mismatch: source has %d jobs, destination has %d jobs", srcCount, dstCount)
|
|
}
|
|
|
|
// Verify cluster config
|
|
clusters := srcBackend.GetClusters()
|
|
for _, cluster := range clusters {
|
|
cfg, err := dstBackend.LoadClusterCfg(cluster)
|
|
if err != nil {
|
|
t.Errorf("Failed to load cluster config for %s from destination: %v", cluster, err)
|
|
}
|
|
if cfg.Name != cluster {
|
|
t.Errorf("Cluster name mismatch: expected %s, got %s", cluster, cfg.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestImportFileToFile tests importing jobs from one file backend to another
|
|
func TestImportFileToFile(t *testing.T) {
|
|
// Create temporary directories
|
|
tmpdir := t.TempDir()
|
|
srcArchive := filepath.Join(tmpdir, "src-archive")
|
|
dstArchive := filepath.Join(tmpdir, "dst-archive")
|
|
|
|
// Copy test data to source archive
|
|
testDataPath := "../../pkg/archive/testdata/archive"
|
|
if _, err := os.Stat(testDataPath); os.IsNotExist(err) {
|
|
t.Skip("Test data not found, skipping integration test")
|
|
}
|
|
|
|
if err := util.CopyDir(testDataPath, srcArchive); err != nil {
|
|
t.Fatalf("Failed to copy test data: %s", err.Error())
|
|
}
|
|
|
|
// Create destination archive directory
|
|
if err := os.MkdirAll(dstArchive, 0o755); err != nil {
|
|
t.Fatalf("Failed to create destination directory: %s", err.Error())
|
|
}
|
|
|
|
// Write version file
|
|
versionFile := filepath.Join(dstArchive, "version.txt")
|
|
if err := os.WriteFile(versionFile, []byte("3"), 0o644); err != nil {
|
|
t.Fatalf("Failed to write version file: %s", err.Error())
|
|
}
|
|
|
|
// Initialize source backend
|
|
srcConfig := fmt.Sprintf(`{"kind":"file","path":"%s"}`, srcArchive)
|
|
srcBackend, err := archive.InitBackend(json.RawMessage(srcConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize source backend: %s", err.Error())
|
|
}
|
|
|
|
// Initialize destination backend
|
|
dstConfig := fmt.Sprintf(`{"kind":"file","path":"%s"}`, dstArchive)
|
|
dstBackend, err := archive.InitBackend(json.RawMessage(dstConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize destination backend: %s", err.Error())
|
|
}
|
|
|
|
// Perform import
|
|
imported, failed, err := importArchive(srcBackend, dstBackend, srcConfig)
|
|
if err != nil {
|
|
t.Errorf("Import failed: %s", err.Error())
|
|
}
|
|
|
|
if imported == 0 {
|
|
t.Error("No jobs were imported")
|
|
}
|
|
|
|
if failed > 0 {
|
|
t.Errorf("%d jobs failed to import", failed)
|
|
}
|
|
|
|
t.Logf("Successfully imported %d jobs", imported)
|
|
}
|
|
|
|
// TestImportDataIntegrity verifies that job metadata and data are correctly imported
|
|
func TestImportDataIntegrity(t *testing.T) {
|
|
// Create temporary directories
|
|
tmpdir := t.TempDir()
|
|
srcArchive := filepath.Join(tmpdir, "src-archive")
|
|
dstDb := filepath.Join(tmpdir, "dst-archive.db")
|
|
|
|
// Copy test data to source archive
|
|
testDataPath := "../../pkg/archive/testdata/archive"
|
|
if _, err := os.Stat(testDataPath); os.IsNotExist(err) {
|
|
t.Skip("Test data not found, skipping integration test")
|
|
}
|
|
|
|
if err := util.CopyDir(testDataPath, srcArchive); err != nil {
|
|
t.Fatalf("Failed to copy test data: %s", err.Error())
|
|
}
|
|
|
|
// Initialize backends
|
|
srcConfig := fmt.Sprintf(`{"kind":"file","path":"%s"}`, srcArchive)
|
|
srcBackend, err := archive.InitBackend(json.RawMessage(srcConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize source backend: %s", err.Error())
|
|
}
|
|
|
|
dstConfig := fmt.Sprintf(`{"kind":"sqlite","db-path":"%s"}`, dstDb)
|
|
dstBackend, err := archive.InitBackend(json.RawMessage(dstConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize destination backend: %s", err.Error())
|
|
}
|
|
|
|
// Perform import
|
|
_, _, err = importArchive(srcBackend, dstBackend, srcConfig)
|
|
if err != nil {
|
|
t.Errorf("Import failed: %s", err.Error())
|
|
}
|
|
|
|
// Verify data integrity for each job
|
|
verifiedJobs := 0
|
|
for srcJob := range srcBackend.Iter(false) {
|
|
if srcJob.Meta == nil {
|
|
continue
|
|
}
|
|
|
|
// Load job from destination
|
|
dstJobMeta, err := dstBackend.LoadJobMeta(srcJob.Meta)
|
|
if err != nil {
|
|
t.Errorf("Failed to load job %d from destination: %s", srcJob.Meta.JobID, err.Error())
|
|
continue
|
|
}
|
|
|
|
// Verify basic metadata
|
|
if dstJobMeta.JobID != srcJob.Meta.JobID {
|
|
t.Errorf("JobID mismatch: expected %d, got %d", srcJob.Meta.JobID, dstJobMeta.JobID)
|
|
}
|
|
|
|
if dstJobMeta.Cluster != srcJob.Meta.Cluster {
|
|
t.Errorf("Cluster mismatch for job %d: expected %s, got %s",
|
|
srcJob.Meta.JobID, srcJob.Meta.Cluster, dstJobMeta.Cluster)
|
|
}
|
|
|
|
if dstJobMeta.StartTime != srcJob.Meta.StartTime {
|
|
t.Errorf("StartTime mismatch for job %d: expected %d, got %d",
|
|
srcJob.Meta.JobID, srcJob.Meta.StartTime, dstJobMeta.StartTime)
|
|
}
|
|
|
|
// Load and verify job data
|
|
srcData, err := srcBackend.LoadJobData(srcJob.Meta)
|
|
if err != nil {
|
|
t.Errorf("Failed to load job data from source: %s", err.Error())
|
|
continue
|
|
}
|
|
|
|
dstData, err := dstBackend.LoadJobData(srcJob.Meta)
|
|
if err != nil {
|
|
t.Errorf("Failed to load job data from destination: %s", err.Error())
|
|
continue
|
|
}
|
|
|
|
// Verify metric data exists
|
|
if len(srcData.Metrics) != len(dstData.Metrics) {
|
|
t.Errorf("Metric count mismatch for job %d: expected %d, got %d",
|
|
srcJob.Meta.JobID, len(srcData.Metrics), len(dstData.Metrics))
|
|
}
|
|
|
|
verifiedJobs++
|
|
}
|
|
|
|
if verifiedJobs == 0 {
|
|
t.Error("No jobs were verified")
|
|
}
|
|
|
|
t.Logf("Successfully verified %d jobs", verifiedJobs)
|
|
}
|
|
|
|
// TestImportEmptyArchive tests importing from an empty archive
|
|
func TestImportEmptyArchive(t *testing.T) {
|
|
tmpdir := t.TempDir()
|
|
srcArchive := filepath.Join(tmpdir, "empty-archive")
|
|
dstDb := filepath.Join(tmpdir, "dst-archive.db")
|
|
|
|
// Create empty source archive
|
|
if err := os.MkdirAll(srcArchive, 0o755); err != nil {
|
|
t.Fatalf("Failed to create source directory: %s", err.Error())
|
|
}
|
|
|
|
// Write version file
|
|
versionFile := filepath.Join(srcArchive, "version.txt")
|
|
if err := os.WriteFile(versionFile, []byte("3"), 0o644); err != nil {
|
|
t.Fatalf("Failed to write version file: %s", err.Error())
|
|
}
|
|
|
|
// Initialize backends
|
|
srcConfig := fmt.Sprintf(`{"kind":"file","path":"%s"}`, srcArchive)
|
|
srcBackend, err := archive.InitBackend(json.RawMessage(srcConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize source backend: %s", err.Error())
|
|
}
|
|
|
|
dstConfig := fmt.Sprintf(`{"kind":"sqlite","db-path":"%s"}`, dstDb)
|
|
dstBackend, err := archive.InitBackend(json.RawMessage(dstConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize destination backend: %s", err.Error())
|
|
}
|
|
|
|
// Perform import
|
|
imported, failed, err := importArchive(srcBackend, dstBackend, srcConfig)
|
|
if err != nil {
|
|
t.Errorf("Import from empty archive should not fail: %s", err.Error())
|
|
}
|
|
|
|
if imported != 0 {
|
|
t.Errorf("Expected 0 imported jobs, got %d", imported)
|
|
}
|
|
|
|
if failed != 0 {
|
|
t.Errorf("Expected 0 failed jobs, got %d", failed)
|
|
}
|
|
}
|
|
|
|
// TestImportDuplicateJobs tests that duplicate jobs are skipped
|
|
func TestImportDuplicateJobs(t *testing.T) {
|
|
tmpdir := t.TempDir()
|
|
srcArchive := filepath.Join(tmpdir, "src-archive")
|
|
dstDb := filepath.Join(tmpdir, "dst-archive.db")
|
|
|
|
// Copy test data
|
|
testDataPath := "../../pkg/archive/testdata/archive"
|
|
if _, err := os.Stat(testDataPath); os.IsNotExist(err) {
|
|
t.Skip("Test data not found, skipping integration test")
|
|
}
|
|
|
|
if err := util.CopyDir(testDataPath, srcArchive); err != nil {
|
|
t.Fatalf("Failed to copy test data: %s", err.Error())
|
|
}
|
|
|
|
// Initialize backends
|
|
srcConfig := fmt.Sprintf(`{"kind":"file","path":"%s"}`, srcArchive)
|
|
srcBackend, err := archive.InitBackend(json.RawMessage(srcConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize source backend: %s", err.Error())
|
|
}
|
|
|
|
dstConfig := fmt.Sprintf(`{"kind":"sqlite","db-path":"%s"}`, dstDb)
|
|
dstBackend, err := archive.InitBackend(json.RawMessage(dstConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to initialize destination backend: %s", err.Error())
|
|
}
|
|
|
|
// First import
|
|
imported1, _, err := importArchive(srcBackend, dstBackend, srcConfig)
|
|
if err != nil {
|
|
t.Fatalf("First import failed: %s", err.Error())
|
|
}
|
|
|
|
// Second import (should skip all jobs)
|
|
imported2, _, err := importArchive(srcBackend, dstBackend, srcConfig)
|
|
if err != nil {
|
|
t.Errorf("Second import failed: %s", err.Error())
|
|
}
|
|
|
|
if imported2 != 0 {
|
|
t.Errorf("Second import should skip all jobs, but imported %d", imported2)
|
|
}
|
|
|
|
t.Logf("First import: %d jobs, Second import: %d jobs (all skipped as expected)", imported1, imported2)
|
|
}
|
|
|
|
// TestJobStub is a helper test to verify that the job stub used in tests matches the schema
|
|
func TestJobStub(t *testing.T) {
|
|
job := &schema.Job{
|
|
JobID: 123,
|
|
Cluster: "test-cluster",
|
|
StartTime: 1234567890,
|
|
}
|
|
|
|
if job.JobID != 123 {
|
|
t.Errorf("Expected JobID 123, got %d", job.JobID)
|
|
}
|
|
}
|
|
|
|
// TestImportToEmptyFileDestination tests importing to an empty file backend (bootstrapping version)
|
|
func TestImportToEmptyFileDestination(t *testing.T) {
|
|
tmpdir := t.TempDir()
|
|
srcArchive := filepath.Join(tmpdir, "src-archive")
|
|
dstArchive := filepath.Join(tmpdir, "dst-archive-empty")
|
|
|
|
// Setup valid source
|
|
testDataPath := "../../pkg/archive/testdata/archive"
|
|
if _, err := os.Stat(testDataPath); os.IsNotExist(err) {
|
|
t.Skip("Test data not found")
|
|
}
|
|
util.CopyDir(testDataPath, srcArchive)
|
|
|
|
// Setup empty destination directory
|
|
os.MkdirAll(dstArchive, 0o755)
|
|
// NOTE: NOT writing version.txt here!
|
|
|
|
// Initialize source
|
|
srcConfig := fmt.Sprintf(`{"kind":"file","path":"%s"}`, srcArchive)
|
|
srcBackend, err := archive.InitBackend(json.RawMessage(srcConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to init source: %v", err)
|
|
}
|
|
|
|
// Initialize destination (should succeed with changes, currently fails)
|
|
dstConfig := fmt.Sprintf(`{"kind":"file","path":"%s"}`, dstArchive)
|
|
dstBackend, err := archive.InitBackend(json.RawMessage(dstConfig))
|
|
if err != nil {
|
|
t.Fatalf("Failed to init destination (should bootstrap): %v", err)
|
|
}
|
|
|
|
// Perform import
|
|
imported, _, err := importArchive(srcBackend, dstBackend, srcConfig)
|
|
if err != nil {
|
|
t.Errorf("Import failed: %v", err)
|
|
}
|
|
if imported == 0 {
|
|
t.Error("No jobs imported")
|
|
}
|
|
|
|
// Check if version.txt was created
|
|
if _, err := os.Stat(filepath.Join(dstArchive, "version.txt")); os.IsNotExist(err) {
|
|
t.Error("version.txt was not created in destination")
|
|
}
|
|
}
|