Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,16 +222,16 @@ tasks:
cmds:
# Using go-testreport v0.3.2
- "go install github.com/becheran/go-testreport@90efc1ce13c872f23d6bc8a069527c26288b8f9c"
- "go test -race -cover -json -tags='!integration' ./internal/... ./pkg/... ./cmd/... | go-testreport -output unit-test-report.md -vars 'Title:Unit Test Report'"
- "go test -p 1 -race -cover -json -tags='!integration' ./internal/... ./pkg/... ./cmd/... | go-testreport -output unit-test-report.md -vars 'Title:Unit Test Report'"

test:unit:verbose:
desc: "Run unit tests with verbose console output"
deps:
- "generate"
- "mocks"
cmds:
- "go test -v -race -cover -tags='!integration' ./internal/... ./pkg/... ./cmd/..."
- "go test -p 1 -v -race -cover -tags='!integration' ./internal/... ./pkg/... ./cmd/..."

test:integration:verbose:
desc: "Run integration tests with verbose console output. Use TEST_NAME to run a specific test, e.g., task test:integration:verbose TEST_NAME='^Test_StepKubeadm_Fresh_Integration$'"
cmds:
Expand Down Expand Up @@ -282,9 +282,11 @@ tasks:
cmds:
# Using go-testreport v0.3.2
- "go install github.com/becheran/go-testreport@90efc1ce13c872f23d6bc8a069527c26288b8f9c"
- "go test -p 1 -v -tags='integration_block_node_full_install' ./internal/... ./pkg/... ./cmd/... -timeout 10m"
- "go test -p 1 -v -tags='integration_require_block_node_installed' ./internal/... ./pkg/... ./cmd/... -timeout 10m"
- "go test -p 1 -race -cover -json -tags='integration' ./internal/... ./pkg/... ./cmd/... -timeout 60m > integration-test.json"
- "go-testreport -input integration-test.json -output integration-test-report.md -vars 'Title:Integration Test Report'"

run:
deps:
- "run:weaver"
Expand Down
4 changes: 2 additions & 2 deletions cmd/weaver/commands/block/node/install_upgrade_it_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0

//go:build integration
//go:build integration_block_node_full_install

package node

Expand All @@ -18,7 +18,7 @@ import (
// TestHelmLifecycle_InstallAndUpgradeWithValueReuse tests a complete installation flow with flag overrides
// and multiple upgrade scenarios with different value reuse behaviors
func TestHelmLifecycle_InstallAndUpgradeWithValueReuse(t *testing.T) {
serial(t) // Enforce sequential execution due to shared flag variables
testutil.Serial(t) // Enforce sequential execution due to shared flag variables

testutil.Reset(t)

Expand Down
24 changes: 5 additions & 19 deletions cmd/weaver/commands/block/node/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"os"
"os/exec"
"path/filepath"
"sync"
"testing"

"github.com/hashgraph/solo-weaver/cmd/weaver/commands/common"
Expand All @@ -18,19 +17,6 @@ import (
"github.com/stretchr/testify/require"
)

// serialMu enforces sequential execution of tests that manipulate shared package-level flag variables.
// Tests acquire this lock at the start and release it via t.Cleanup, ensuring mutual exclusion.
var serialMu sync.Mutex

// serial ensures a test runs sequentially, never in parallel with other tests using this function.
// It acquires a mutex that is released when the test completes (via t.Cleanup).
// This protects shared package-level flag variables from concurrent access.
func serial(t *testing.T) {
t.Helper()
serialMu.Lock()
t.Cleanup(serialMu.Unlock)
}

// resetFlags resets all flag variables to empty strings
func resetFlags(cmd *cobra.Command) {
flagProfile = "local"
Expand Down Expand Up @@ -95,7 +81,7 @@ func getPVCStorageSize(t *testing.T, pvcName, namespace string) string {

// TestNegative_InvalidValuesFilePath tests that invalid values file paths are rejected
func TestNegative_InvalidValuesFilePath(t *testing.T) {
serial(t)
testutil.Serial(t)

testCases := []struct {
name string
Expand Down Expand Up @@ -175,7 +161,7 @@ blockNode:
// TestNegative_InvalidStoragePaths tests that invalid storage paths are rejected
// when they are actually validated in GetStoragePaths() during the workflow execution
func TestNegative_InvalidStoragePaths(t *testing.T) {
serial(t)
testutil.Serial(t)

testCases := []struct {
name string
Expand Down Expand Up @@ -256,7 +242,7 @@ blockNode:

// TestNegative_DirectoryAsValuesFile tests that directories are rejected as values files
func TestNegative_DirectoryAsValuesFile(t *testing.T) {
serial(t)
testutil.Serial(t)

testutil.Reset(t)

Expand Down Expand Up @@ -294,7 +280,7 @@ blockNode:

// TestNegative_InvalidChartVersion tests various invalid chart version formats
func TestNegative_InvalidChartVersion(t *testing.T) {
serial(t)
testutil.Serial(t)

cmd := testutil.PrepareSubCmdForTest(GetCmd())
resetFlags(cmd)
Expand Down Expand Up @@ -425,7 +411,7 @@ blockNode:

// TestNegative_InvalidYAMLInConfigFile tests that invalid YAML in config file is handled
func TestNegative_InvalidYAMLInConfigFile(t *testing.T) {
serial(t)
testutil.Serial(t)

testutil.Reset(t)

Expand Down
3 changes: 0 additions & 3 deletions internal/bll/block_node_test.go

This file was deleted.

4 changes: 4 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ func Initialize(path string) error {
WithProperty(errorx.PropertyPayload(), path)
}

// Migrate old config field names to new names for backward compatibility
// Old format: release, chart -> New format: releaseName, chartRepo
migrateOldConfigKeys()

if err := viper.Unmarshal(&globalConfig); err != nil {
return errorx.IllegalFormat.Wrap(err, "failed to parse configuration").
WithProperty(errorx.PropertyPayload(), path)
Expand Down
119 changes: 119 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,122 @@ blockNode:
t.Fatalf("env override failed: expected %q, got %q", expected, got)
}
}

func TestInitialize_BackwardCompatibility_OldFieldNames(t *testing.T) {
// Test that old config field names (release, chart) are migrated to new names (releaseName, chartRepo)
yamlCfg := `
blockNode:
namespace: "test-ns"
release: "old-release-name"
chart: "oci://ghcr.io/old/chart"
version: "1.0.0"
storage:
basePath: "/mnt/storage"
`
tmpFile, err := os.CreateTemp("", "weaver-config-*.yaml")
if err != nil {
t.Fatalf("create temp file: %v", err)
}
defer os.Remove(tmpFile.Name())

if _, err := tmpFile.WriteString(yamlCfg); err != nil {
tmpFile.Close()
t.Fatalf("write temp config: %v", err)
}
tmpFile.Close()

if err := Initialize(tmpFile.Name()); err != nil {
t.Fatalf("Initialize failed: %v", err)
}

cfg := Get()

// Verify old field names are migrated to new struct fields
if cfg.BlockNode.ReleaseName != "old-release-name" {
t.Errorf("ReleaseName migration failed: expected %q, got %q", "old-release-name", cfg.BlockNode.ReleaseName)
}
if cfg.BlockNode.ChartRepo != "oci://ghcr.io/old/chart" {
t.Errorf("ChartRepo migration failed: expected %q, got %q", "oci://ghcr.io/old/chart", cfg.BlockNode.ChartRepo)
}
}

func TestInitialize_NewFieldNames(t *testing.T) {
// Test that new config field names work directly
yamlCfg := `
blockNode:
namespace: "test-ns"
releaseName: "new-release-name"
chartRepo: "oci://ghcr.io/new/chart"
chartVersion: "2.0.0"
version: "1.0.0"
storage:
basePath: "/mnt/storage"
`
tmpFile, err := os.CreateTemp("", "weaver-config-*.yaml")
if err != nil {
t.Fatalf("create temp file: %v", err)
}
defer os.Remove(tmpFile.Name())

if _, err := tmpFile.WriteString(yamlCfg); err != nil {
tmpFile.Close()
t.Fatalf("write temp config: %v", err)
}
tmpFile.Close()

if err := Initialize(tmpFile.Name()); err != nil {
t.Fatalf("Initialize failed: %v", err)
}

cfg := Get()

if cfg.BlockNode.ReleaseName != "new-release-name" {
t.Errorf("ReleaseName: expected %q, got %q", "new-release-name", cfg.BlockNode.ReleaseName)
}
if cfg.BlockNode.ChartRepo != "oci://ghcr.io/new/chart" {
t.Errorf("ChartRepo: expected %q, got %q", "oci://ghcr.io/new/chart", cfg.BlockNode.ChartRepo)
}
if cfg.BlockNode.ChartVersion != "2.0.0" {
t.Errorf("ChartVersion: expected %q, got %q", "2.0.0", cfg.BlockNode.ChartVersion)
}
}

func TestInitialize_NewFieldNamesTakePrecedence(t *testing.T) {
// Test that new field names take precedence over old ones when both are specified
yamlCfg := `
blockNode:
namespace: "test-ns"
release: "old-release"
releaseName: "new-release"
chart: "oci://old/chart"
chartRepo: "oci://new/chart"
version: "1.0.0"
storage:
basePath: "/mnt/storage"
`
tmpFile, err := os.CreateTemp("", "weaver-config-*.yaml")
if err != nil {
t.Fatalf("create temp file: %v", err)
}
defer os.Remove(tmpFile.Name())

if _, err := tmpFile.WriteString(yamlCfg); err != nil {
tmpFile.Close()
t.Fatalf("write temp config: %v", err)
}
tmpFile.Close()

if err := Initialize(tmpFile.Name()); err != nil {
t.Fatalf("Initialize failed: %v", err)
}

cfg := Get()

// New field names should take precedence
if cfg.BlockNode.ReleaseName != "new-release" {
t.Errorf("ReleaseName precedence failed: expected %q, got %q", "new-release", cfg.BlockNode.ReleaseName)
}
if cfg.BlockNode.ChartRepo != "oci://new/chart" {
t.Errorf("ChartRepo precedence failed: expected %q, got %q", "oci://new/chart", cfg.BlockNode.ChartRepo)
}
}
49 changes: 49 additions & 0 deletions internal/config/migration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: Apache-2.0

// Package config migration.go contains backward compatibility logic for deprecated config fields.
// This file can be safely deleted once users have migrated to the new config format.
//
// Deprecated fields (remove support after v1.0.0 or similar milestone):
// - blockNode.release -> blockNode.releaseName
// - blockNode.chart -> blockNode.chartRepo

package config

import (
"github.com/automa-saga/logx"
"github.com/spf13/viper"
)

// deprecatedKeyMappings defines the mapping from old (deprecated) keys to new keys.
// Add new migrations here as needed.
var deprecatedKeyMappings = []struct {
oldKey string
newKey string
}{
{"blockNode.release", "blockNode.releaseName"},
{"blockNode.chart", "blockNode.chartRepo"},
}

// migrateOldConfigKeys migrates deprecated config field names to their new names.
// This provides backward compatibility for users with old config files.
// It also logs deprecation warnings to encourage users to update their config files.
func migrateOldConfigKeys() {
for _, mapping := range deprecatedKeyMappings {
migrateKey(mapping.oldKey, mapping.newKey)
}
}

// migrateKey migrates a single deprecated key to its new name.
// It only migrates if the new key is not already set and the old key exists.
// Logs a deprecation warning when migration occurs.
func migrateKey(oldKey, newKey string) {
if !viper.IsSet(newKey) && viper.IsSet(oldKey) {
value := viper.Get(oldKey)
viper.Set(newKey, value)

logx.As().Warn().
Str("oldKey", oldKey).
Str("newKey", newKey).
Msg("DEPRECATION WARNING: Config field is deprecated and will be removed in a future release. Please update your config file.")
}
}
2 changes: 1 addition & 1 deletion internal/reality/reality_it_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0

//go:build integration
//go:build integration_require_block_node_installed

package reality

Expand Down
2 changes: 1 addition & 1 deletion internal/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func NewRuntimeBase[T any](
// It respects refreshInterval if lastSync is provided.
func (rb *Base[T]) RefreshState(ctx context.Context, force bool) error {
if rb.fetch == nil {
return errorx.IllegalState.New(rb.fetchName + " fetcher is not initialized")
return errorx.IllegalState.New("%s fetcher is not initialized", rb.fetchName)
}

now := htime.Now()
Expand Down
14 changes: 14 additions & 0 deletions internal/testutil/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,27 @@ import (
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"

"github.com/hashgraph/solo-weaver/internal/core"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
)

// serialMu enforces sequential execution of tests that manipulate shared system state.
// Tests acquire this lock at the start and release it via t.Cleanup, ensuring mutual exclusion.
var serialMu sync.Mutex

// Serial ensures a test runs sequentially, never in parallel with other tests using this function.
// It acquires a mutex that is released when the test completes (via t.Cleanup).
// This protects shared system state (e.g., /opt/solo/weaver, /usr/local/bin) from concurrent access.
func Serial(t *testing.T) {
t.Helper()
serialMu.Lock()
t.Cleanup(serialMu.Unlock)
}

// PrepareSubCmdForTest creates a root command with the given subcommand added.
// Use this from tests in other packages to avoid duplicating the helper.
func PrepareSubCmdForTest(sub *cobra.Command) *cobra.Command {
Expand Down
5 changes: 5 additions & 0 deletions internal/workflows/notify/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ func (m *mockStep) State() automa.StateBag {
return m.state
}

func (m *mockStep) WithState(s automa.StateBag) automa.Step {
m.state = s
return m
}

func (m *mockStep) Id() string { return m.id }

func TestNotificationHandler_Callbacks(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion internal/workflows/steps/step_setup_directories_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestSetupHomeDirectoryStructure_Integration(t *testing.T) {
tmpHome := t.TempDir()
pp := core.NewWeaverPaths(tmpHome)

step, err := SetupHomeDirectoryStructure(pp).Build()
step, err := SetupHomeDirectoryStructure(*pp).Build()
require.NoError(t, err)

report := step.Execute(context.Background())
Expand Down
Loading
Loading