diff --git a/internal/fileutil/fileutil_test.go b/internal/fileutil/fileutil_test.go index 92920d3c4..881fbb49d 100644 --- a/internal/fileutil/fileutil_test.go +++ b/internal/fileutil/fileutil_test.go @@ -20,9 +20,12 @@ import ( "bytes" "os" "path/filepath" + "strings" "testing" ) +// TestAtomicWriteFile tests the happy path of AtomicWriteFile function. +// It verifies that the function correctly writes content to a file with the specified mode. func TestAtomicWriteFile(t *testing.T) { dir := t.TempDir() @@ -55,3 +58,64 @@ func TestAtomicWriteFile(t *testing.T) { mode, gotinfo.Mode()) } } + +// TestAtomicWriteFile_CreateTempError tests the error path when os.CreateTemp fails +func TestAtomicWriteFile_CreateTempError(t *testing.T) { + invalidPath := "/invalid/path/that/does/not/exist/testfile" + + reader := bytes.NewReader([]byte("test content")) + mode := os.FileMode(0644) + + err := AtomicWriteFile(invalidPath, reader, mode) + if err == nil { + t.Error("Expected error when CreateTemp fails, but got nil") + } +} + +// TestAtomicWriteFile_EmptyContent tests with empty content +func TestAtomicWriteFile_EmptyContent(t *testing.T) { + dir := t.TempDir() + testpath := filepath.Join(dir, "empty_helm") + + reader := bytes.NewReader([]byte("")) + mode := os.FileMode(0644) + + err := AtomicWriteFile(testpath, reader, mode) + if err != nil { + t.Errorf("AtomicWriteFile error with empty content: %s", err) + } + + got, err := os.ReadFile(testpath) + if err != nil { + t.Fatal(err) + } + + if len(got) != 0 { + t.Fatalf("expected empty content, got: %s", string(got)) + } +} + +// TestAtomicWriteFile_LargeContent tests with large content +func TestAtomicWriteFile_LargeContent(t *testing.T) { + dir := t.TempDir() + testpath := filepath.Join(dir, "large_test") + + // Create a large content string + largeContent := strings.Repeat("HELM", 1024*1024) + reader := bytes.NewReader([]byte(largeContent)) + mode := os.FileMode(0644) + + err := AtomicWriteFile(testpath, reader, mode) + if err != nil { + t.Errorf("AtomicWriteFile error with large content: %s", err) + } + + got, err := os.ReadFile(testpath) + if err != nil { + t.Fatal(err) + } + + if largeContent != string(got) { + t.Fatalf("expected large content to match, got different length: %d vs %d", len(largeContent), len(got)) + } +} diff --git a/pkg/action/action.go b/pkg/action/action.go index 5088e5de3..c62717d35 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -30,6 +30,7 @@ import ( "strings" "sync" "text/template" + "time" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -50,7 +51,6 @@ import ( releaseutil "helm.sh/helm/v4/pkg/release/v1/util" "helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage/driver" - "helm.sh/helm/v4/pkg/time" ) // Timestamper is a function capable of producing a timestamp.Timestamper. diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index b65e40024..78ca01089 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -24,6 +24,7 @@ import ( "log/slog" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -38,7 +39,6 @@ import ( release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage/driver" - "helm.sh/helm/v4/pkg/time" ) var verbose = flag.Bool("test.log", false, "enable test logging (debug by default)") diff --git a/pkg/action/get_metadata_test.go b/pkg/action/get_metadata_test.go index ca612fed7..7962a2133 100644 --- a/pkg/action/get_metadata_test.go +++ b/pkg/action/get_metadata_test.go @@ -28,7 +28,6 @@ import ( chart "helm.sh/helm/v4/pkg/chart/v2" kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) func TestNewGetMetadata(t *testing.T) { @@ -45,7 +44,7 @@ func TestGetMetadata_Run_BasicMetadata(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() rel := &release.Release{ Name: releaseName, @@ -86,7 +85,7 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() dependencies := []*chart.Dependency{ { @@ -138,7 +137,7 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() dependencies := []*chart.Dependency{ { @@ -194,7 +193,7 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() dependencies := []*chart.Dependency{ { @@ -268,7 +267,7 @@ func TestGetMetadata_Run_WithAnnotations(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() annotations := map[string]string{ "helm.sh/hook": "pre-install", @@ -313,13 +312,13 @@ func TestGetMetadata_Run_SpecificVersion(t *testing.T) { client.Version = 2 releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() rel1 := &release.Release{ Name: releaseName, Info: &release.Info{ Status: release.StatusSuperseded, - LastDeployed: helmtime.Time{Time: deployedTime.Time.Add(-time.Hour)}, + LastDeployed: deployedTime.Add(-time.Hour), }, Chart: &chart.Chart{ Metadata: &chart.Metadata{ @@ -384,7 +383,7 @@ func TestGetMetadata_Run_DifferentStatuses(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { releaseName := "test-release-" + tc.name - deployedTime := helmtime.Now() + deployedTime := time.Now() rel := &release.Release{ Name: releaseName, @@ -440,7 +439,7 @@ func TestGetMetadata_Run_EmptyAppVersion(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() rel := &release.Release{ Name: releaseName, diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index 458a6342c..4808bc054 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -29,7 +29,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) // execHook executes all of the hooks for the given hook event. @@ -62,7 +61,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, // Record the time at which the hook was applied to the cluster h.LastRun = release.HookExecution{ - StartedAt: helmtime.Now(), + StartedAt: time.Now(), Phase: release.HookPhaseRunning, } cfg.recordRelease(rl) @@ -76,7 +75,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, if _, err := cfg.KubeClient.Create( resources, kube.ClientCreateOptionServerSideApply(serverSideApply, false)); err != nil { - h.LastRun.CompletedAt = helmtime.Now() + h.LastRun.CompletedAt = time.Now() h.LastRun.Phase = release.HookPhaseFailed return fmt.Errorf("warning: Hook %s %s failed: %w", hook, h.Path, err) } @@ -88,7 +87,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, // Watch hook resources until they have completed err = waiter.WatchUntilReady(resources, timeout) // Note the time of success/failure - h.LastRun.CompletedAt = helmtime.Now() + h.LastRun.CompletedAt = time.Now() // Mark hook as succeeded or failed if err != nil { h.LastRun.Phase = release.HookPhaseFailed diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index 091155bc2..fb7d1b4ec 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "reflect" + "strings" "testing" "time" @@ -112,15 +113,15 @@ spec: } func convertHooksToCommaSeparated(hookDefinitions []release.HookOutputLogPolicy) string { - var commaSeparated string + var commaSeparated strings.Builder for i, policy := range hookDefinitions { if i+1 == len(hookDefinitions) { - commaSeparated += policy.String() + commaSeparated.WriteString(policy.String()) } else { - commaSeparated += policy.String() + "," + commaSeparated.WriteString(policy.String() + ",") } } - return commaSeparated + return commaSeparated.String() } func TestInstallRelease_HookOutputLogsOnFailure(t *testing.T) { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index b2b1508be..9031372d7 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -49,7 +49,6 @@ import ( kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage/driver" - helmtime "helm.sh/helm/v4/pkg/time" ) type nameTemplateTestCase struct { @@ -856,32 +855,32 @@ func TestNameAndChartGenerateName(t *testing.T) { { "local filepath", "./chart", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, { "dot filepath", ".", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, { "empty filepath", "", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, { "packaged chart", "chart.tgz", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, { "packaged chart with .tar.gz extension", "chart.tar.gz", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, { "packaged chart with local extension", "./chart.tgz", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, } diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index adaf22615..f56052988 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -26,7 +26,6 @@ import ( chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) // Rollback is the action for rolling back to a given release. @@ -158,7 +157,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele Config: previousRelease.Config, Info: &release.Info{ FirstDeployed: currentRelease.Info.FirstDeployed, - LastDeployed: helmtime.Now(), + LastDeployed: time.Now(), Status: release.StatusPendingRollback, Notes: previousRelease.Info.Notes, // Because we lose the reference to previous version elsewhere, we set the diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index 866be5d54..057c2118f 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -30,7 +30,6 @@ import ( release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" "helm.sh/helm/v4/pkg/storage/driver" - helmtime "helm.sh/helm/v4/pkg/time" ) // Uninstall is the action for uninstalling releases. @@ -110,7 +109,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) slog.Debug("uninstall: deleting release", "name", name) rel.Info.Status = release.StatusUninstalling - rel.Info.Deleted = helmtime.Now() + rel.Info.Deleted = time.Now() rel.Info.Description = "Deletion in progress (or silently failed)" res := &release.UninstallReleaseResponse{Release: rel} diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index d31804b87..0a436534f 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -34,7 +34,6 @@ import ( kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) func upgradeAction(t *testing.T) *Upgrade { @@ -260,7 +259,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { withValues(chartDefaultValues), withMetadataDependency(dependency), ) - now := helmtime.Now() + now := time.Now() existingValues := map[string]interface{}{ "subchart": map[string]interface{}{ "enabled": false, diff --git a/pkg/cmd/flags_test.go b/pkg/cmd/flags_test.go index dce748a6b..8d79716f0 100644 --- a/pkg/cmd/flags_test.go +++ b/pkg/cmd/flags_test.go @@ -19,19 +19,19 @@ package cmd import ( "fmt" "testing" + "time" "github.com/stretchr/testify/require" "helm.sh/helm/v4/pkg/action" chart "helm.sh/helm/v4/pkg/chart/v2" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) func outputFlagCompletionTest(t *testing.T, cmdName string) { t.Helper() releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { - info.LastDeployed = helmtime.Unix(1452902400, 0).UTC() + info.LastDeployed = time.Unix(1452902400, 0).UTC() return []*release.Release{{ Name: "athos", Namespace: "default", diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 55e3a842f..96bf6434b 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -22,6 +22,7 @@ import ( "os" "strings" "testing" + "time" shellwords "github.com/mattn/go-shellwords" "github.com/spf13/cobra" @@ -34,7 +35,6 @@ import ( release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage/driver" - "helm.sh/helm/v4/pkg/time" ) func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() } diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index 9f029268c..f4dde95e4 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -31,7 +31,6 @@ import ( "helm.sh/helm/v4/pkg/cmd/require" release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" - helmtime "helm.sh/helm/v4/pkg/time" ) var historyHelp = ` @@ -84,12 +83,12 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } type releaseInfo struct { - Revision int `json:"revision"` - Updated helmtime.Time `json:"updated"` - Status string `json:"status"` - Chart string `json:"chart"` - AppVersion string `json:"app_version"` - Description string `json:"description"` + Revision int `json:"revision"` + Updated time.Time `json:"updated,omitzero"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` + Description string `json:"description"` } type releaseHistory []releaseInfo diff --git a/pkg/cmd/list_test.go b/pkg/cmd/list_test.go index 82b25a768..22a948fff 100644 --- a/pkg/cmd/list_test.go +++ b/pkg/cmd/list_test.go @@ -18,10 +18,10 @@ package cmd import ( "testing" + "time" chart "helm.sh/helm/v4/pkg/chart/v2" release "helm.sh/helm/v4/pkg/release/v1" - "helm.sh/helm/v4/pkg/time" ) func TestListCmd(t *testing.T) { diff --git a/pkg/cmd/status_test.go b/pkg/cmd/status_test.go index cb4e23c59..8c251b76b 100644 --- a/pkg/cmd/status_test.go +++ b/pkg/cmd/status_test.go @@ -22,12 +22,11 @@ import ( chart "helm.sh/helm/v4/pkg/chart/v2" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) func TestStatusCmd(t *testing.T) { releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { - info.LastDeployed = helmtime.Unix(1452902400, 0).UTC() + info.LastDeployed = time.Unix(1452902400, 0).UTC() return []*release.Release{{ Name: "flummoxed-chickadee", Namespace: "default", @@ -130,8 +129,8 @@ func TestStatusCmd(t *testing.T) { runTestCmd(t, tests) } -func mustParseTime(t string) helmtime.Time { - res, _ := helmtime.Parse(time.RFC3339, t) +func mustParseTime(t string) time.Time { + res, _ := time.Parse(time.RFC3339, t) return res } diff --git a/pkg/cmd/testdata/output/status-with-resources.json b/pkg/cmd/testdata/output/status-with-resources.json index 275e0cfc6..af512bfd1 100644 --- a/pkg/cmd/testdata/output/status-with-resources.json +++ b/pkg/cmd/testdata/output/status-with-resources.json @@ -1 +1 @@ -{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed"},"namespace":"default"} +{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00:00:00Z","status":"deployed"},"namespace":"default"} diff --git a/pkg/cmd/testdata/output/status.json b/pkg/cmd/testdata/output/status.json index 4b499c935..4727dd100 100644 --- a/pkg/cmd/testdata/output/status.json +++ b/pkg/cmd/testdata/output/status.json @@ -1 +1 @@ -{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed","notes":"release notes"},"namespace":"default"} +{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00:00:00Z","status":"deployed","notes":"release notes"},"namespace":"default"} diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 795152ff9..bacca145d 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -34,18 +34,6 @@ import ( "helm.sh/helm/v4/pkg/chart/common" ) -// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 -// > "template: %s: executing %q at <%s>: %s" -var execErrFmt = regexp.MustCompile(`^template: (?P(?U).+): executing (?P(?U).+) at (?P(?U).+): (?P(?U).+)(?P( template:.*)?)$`) - -// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138 -// > "template: %s: %s" -var execErrFmtWithoutTemplate = regexp.MustCompile(`^template: (?P(?U).+): (?P.*)(?P( template:.*)?)$`) - -// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191 -// > "template: no template %q associated with template %q" -var execErrNoTemplateAssociated = regexp.MustCompile(`^template: no template (?P.*) associated with template (?P(.*)?)$`) - // Engine is an implementation of the Helm rendering implementation for templates. type Engine struct { // If strict is enabled, template rendering will fail if a template references @@ -338,7 +326,7 @@ func cleanupParseError(filename string, err error) error { location := tokens[1] // The remaining tokens make up a stacktrace-like chain, ending with the relevant error errMsg := tokens[len(tokens)-1] - return fmt.Errorf("parse error at (%s): %s", string(location), errMsg) + return fmt.Errorf("parse error at (%s): %s", location, errMsg) } type TraceableError struct { @@ -370,14 +358,117 @@ func (t TraceableError) String() string { return errorString.String() } +// parseTemplateExecErrorString parses a template execution error string from text/template +// without using regular expressions. It returns a TraceableError and true if parsing succeeded. +func parseTemplateExecErrorString(s string) (TraceableError, bool) { + const prefix = "template: " + if !strings.HasPrefix(s, prefix) { + return TraceableError{}, false + } + remainder := s[len(prefix):] + + // Special case: "template: no template %q associated with template %q" + // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191 + traceableError, done := parseTemplateNoTemplateError(s, remainder) + if done { + return traceableError, true + } + + // Executing form: ": executing \"\" at <>: [ template:...]" + // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 + traceableError, done = parseTemplateExecutingAtErrorType(remainder) + if done { + return traceableError, true + } + + // Simple form: ": " + // Use LastIndex to avoid splitting colons within line:col info. + // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138 + traceableError, done = parseTemplateSimpleErrorString(remainder) + if done { + return traceableError, true + } + + return TraceableError{}, false +} + +// Special case: "template: no template %q associated with template %q" +// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191 +func parseTemplateNoTemplateError(s string, remainder string) (TraceableError, bool) { + if strings.HasPrefix(remainder, "no template ") { + return TraceableError{message: s}, true + } + return TraceableError{}, false +} + +// Simple form: ": " +// Use LastIndex to avoid splitting colons within line:col info. +// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138 +func parseTemplateSimpleErrorString(remainder string) (TraceableError, bool) { + if sep := strings.LastIndex(remainder, ": "); sep != -1 { + templateName := remainder[:sep] + errMsg := remainder[sep+2:] + if cut := strings.Index(errMsg, " template:"); cut != -1 { + errMsg = errMsg[:cut] + } + return TraceableError{location: templateName, message: errMsg}, true + } + return TraceableError{}, false +} + +// Executing form: ": executing \"\" at <>: [ template:...]" +// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 +func parseTemplateExecutingAtErrorType(remainder string) (TraceableError, bool) { + if idx := strings.Index(remainder, ": executing "); idx != -1 { + templateName := remainder[:idx] + after := remainder[idx+len(": executing "):] + if len(after) == 0 || after[0] != '"' { + return TraceableError{}, false + } + // find closing quote for function name + endQuote := strings.IndexByte(after[1:], '"') + if endQuote == -1 { + return TraceableError{}, false + } + endQuote++ // account for offset we started at 1 + functionName := after[1:endQuote] + afterFunc := after[endQuote+1:] + + // expect: " at <" then location then ">: " then message + const atPrefix = " at <" + if !strings.HasPrefix(afterFunc, atPrefix) { + return TraceableError{}, false + } + afterAt := afterFunc[len(atPrefix):] + endLoc := strings.Index(afterAt, ">: ") + if endLoc == -1 { + return TraceableError{}, false + } + locationName := afterAt[:endLoc] + errMsg := afterAt[endLoc+len(">: "):] + + // trim chained next error starting with space + "template:" if present + if cut := strings.Index(errMsg, " template:"); cut != -1 { + errMsg = errMsg[:cut] + } + return TraceableError{ + location: templateName, + message: errMsg, + executedFunction: "executing \"" + functionName + "\" at <" + locationName + ">:", + }, true + } + return TraceableError{}, false +} + // reformatExecErrorMsg takes an error message for template rendering and formats it into a formatted // multi-line error string func reformatExecErrorMsg(filename string, err error) error { - // This function matches the error message against regex's for the text/template package. - // If the regex's can parse out details from that error message such as the line number, template it failed on, + // This function parses the error message produced by text/template package. + // If it can parse out details from that error message such as the line number, template it failed on, // and error description, then it will construct a new error that displays these details in a structured way. // If there are issues with parsing the error message, the err passed into the function should return instead. - if _, isExecError := err.(template.ExecError); !isExecError { + var execError template.ExecError + if !errors.As(err, &execError) { return err } @@ -393,39 +484,18 @@ func reformatExecErrorMsg(filename string, err error) error { parts := warnRegex.FindStringSubmatch(tokens[2]) if len(parts) >= 2 { - return fmt.Errorf("execution error at (%s): %s", string(location), parts[1]) + return fmt.Errorf("execution error at (%s): %s", location, parts[1]) } current := err - fileLocations := []TraceableError{} + var fileLocations []TraceableError for current != nil { - var traceable TraceableError - if matches := execErrFmt.FindStringSubmatch(current.Error()); matches != nil { - templateName := matches[execErrFmt.SubexpIndex("templateName")] - functionName := matches[execErrFmt.SubexpIndex("functionName")] - locationName := matches[execErrFmt.SubexpIndex("location")] - errMsg := matches[execErrFmt.SubexpIndex("errMsg")] - traceable = TraceableError{ - location: templateName, - message: errMsg, - executedFunction: "executing " + functionName + " at " + locationName + ":", - } - } else if matches := execErrFmtWithoutTemplate.FindStringSubmatch(current.Error()); matches != nil { - templateName := matches[execErrFmt.SubexpIndex("templateName")] - errMsg := matches[execErrFmt.SubexpIndex("errMsg")] - traceable = TraceableError{ - location: templateName, - message: errMsg, - } - } else if matches := execErrNoTemplateAssociated.FindStringSubmatch(current.Error()); matches != nil { - traceable = TraceableError{ - message: current.Error(), + if tr, ok := parseTemplateExecErrorString(current.Error()); ok { + if len(fileLocations) == 0 || fileLocations[len(fileLocations)-1] != tr { + fileLocations = append(fileLocations, tr) } } else { return err } - if len(fileLocations) == 0 || fileLocations[len(fileLocations)-1] != traceable { - fileLocations = append(fileLocations, traceable) - } current = errors.Unwrap(current) } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 7ac892cec..542ac2a9c 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -1024,15 +1024,15 @@ func TestRenderRecursionLimit(t *testing.T) { times := 4000 phrase := "All work and no play makes Jack a dull boy" printFunc := `{{define "overlook"}}{{printf "` + phrase + `\n"}}{{end}}` - var repeatedIncl string + var repeatedIncl strings.Builder for i := 0; i < times; i++ { - repeatedIncl += `{{include "overlook" . }}` + repeatedIncl.WriteString(`{{include "overlook" . }}`) } d := &chart.Chart{ Metadata: &chart.Metadata{Name: "overlook"}, Templates: []*common.File{ - {Name: "templates/quote", Data: []byte(repeatedIncl)}, + {Name: "templates/quote", Data: []byte(repeatedIncl.String())}, {Name: "templates/_function", Data: []byte(printFunc)}, }, } @@ -1429,3 +1429,50 @@ func TestRenderCustomTemplateFuncs(t *testing.T) { t.Errorf("Expected %q, got %q", expected, rendered) } } + +func TestTraceableError_SimpleForm(t *testing.T) { + testStrings := []string{ + "function_not_found/templates/secret.yaml: error calling include", + } + for _, errString := range testStrings { + trace, done := parseTemplateSimpleErrorString(errString) + if !done { + t.Errorf("Expected parse to pass but did not") + } + if trace.message != "error calling include" { + t.Errorf("Expected %q, got %q", errString, trace.message) + } + } +} +func TestTraceableError_ExecutingForm(t *testing.T) { + testStrings := [][]string{ + {"function_not_found/templates/secret.yaml:6:11: executing \"function_not_found/templates/secret.yaml\" at : ", "function_not_found/templates/secret.yaml:6:11"}, + {"divide_by_zero/templates/secret.yaml:6:11: executing \"divide_by_zero/templates/secret.yaml\" at : ", "divide_by_zero/templates/secret.yaml:6:11"}, + } + for _, errTuple := range testStrings { + errString := errTuple[0] + expectedLocation := errTuple[1] + trace, done := parseTemplateExecutingAtErrorType(errString) + if !done { + t.Errorf("Expected parse to pass but did not") + } + if trace.location != expectedLocation { + t.Errorf("Expected %q, got %q", expectedLocation, trace.location) + } + } +} + +func TestTraceableError_NoTemplateForm(t *testing.T) { + testStrings := []string{ + "no template \"common.names.get_name\" associated with template \"gotpl\"", + } + for _, errString := range testStrings { + trace, done := parseTemplateNoTemplateError(errString, errString) + if !done { + t.Errorf("Expected parse to pass but did not") + } + if trace.message != errString { + t.Errorf("Expected %q, got %q", errString, trace.message) + } + } +} diff --git a/pkg/pusher/ocipusher.go b/pkg/pusher/ocipusher.go index 699d27caf..25682969b 100644 --- a/pkg/pusher/ocipusher.go +++ b/pkg/pusher/ocipusher.go @@ -29,7 +29,6 @@ import ( "helm.sh/helm/v4/internal/tlsutil" "helm.sh/helm/v4/pkg/chart/v2/loader" "helm.sh/helm/v4/pkg/registry" - "helm.sh/helm/v4/pkg/time/ctime" ) // OCIPusher is the default OCI backend handler @@ -91,7 +90,7 @@ func (pusher *OCIPusher) push(chartRef, href string) error { meta.Metadata.Version) // The time the chart was "created" is semantically the time the chart archive file was last written(modified) - chartArchiveFileCreatedTime := ctime.Modified(stat) + chartArchiveFileCreatedTime := stat.ModTime() pushOpts = append(pushOpts, registry.PushOptCreationTime(chartArchiveFileCreatedTime.Format(time.RFC3339))) _, err = client.Push(chartBytes, ref, pushOpts...) diff --git a/pkg/registry/util.go b/pkg/registry/util.go index b31ab63fe..6071c66c3 100644 --- a/pkg/registry/util.go +++ b/pkg/registry/util.go @@ -28,7 +28,6 @@ import ( "helm.sh/helm/v4/internal/tlsutil" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" - helmtime "helm.sh/helm/v4/pkg/time" "github.com/Masterminds/semver/v3" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -157,7 +156,7 @@ func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[ chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home) if len(creationTime) == 0 { - creationTime = helmtime.Now().UTC().Format(time.RFC3339) + creationTime = time.Now().UTC().Format(time.RFC3339) } chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, creationTime) diff --git a/pkg/registry/util_test.go b/pkg/registry/util_test.go index c8ce4e4a4..a67bc853a 100644 --- a/pkg/registry/util_test.go +++ b/pkg/registry/util_test.go @@ -24,12 +24,11 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" chart "helm.sh/helm/v4/pkg/chart/v2" - helmtime "helm.sh/helm/v4/pkg/time" ) func TestGenerateOCIChartAnnotations(t *testing.T) { - nowString := helmtime.Now().Format(time.RFC3339) + nowString := time.Now().Format(time.RFC3339) tests := []struct { name string @@ -160,7 +159,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { func TestGenerateOCIAnnotations(t *testing.T) { - nowString := helmtime.Now().Format(time.RFC3339) + nowString := time.Now().Format(time.RFC3339) tests := []struct { name string @@ -234,7 +233,7 @@ func TestGenerateOCIAnnotations(t *testing.T) { func TestGenerateOCICreatedAnnotations(t *testing.T) { - nowTime := helmtime.Now() + nowTime := time.Now() nowTimeString := nowTime.Format(time.RFC3339) chart := &chart.Metadata{ @@ -250,7 +249,7 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) { } // Verify value of created artifact in RFC3339 format - if _, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { + if _, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) } @@ -262,7 +261,7 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) { t.Errorf("%s annotation not created", ocispec.AnnotationCreated) } - if createdTimeAnnotation, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { + if createdTimeAnnotation, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) // Verify creation annotation after time test began diff --git a/pkg/release/v1/hook.go b/pkg/release/v1/hook.go index 1ef5c1eb8..b7d3c3992 100644 --- a/pkg/release/v1/hook.go +++ b/pkg/release/v1/hook.go @@ -17,7 +17,7 @@ limitations under the License. package v1 import ( - "helm.sh/helm/v4/pkg/time" + "time" ) // HookEvent specifies the hook event @@ -97,9 +97,9 @@ type Hook struct { // A HookExecution records the result for the last execution of a hook for a given release. type HookExecution struct { // StartedAt indicates the date/time this hook was started - StartedAt time.Time `json:"started_at,omitempty"` + StartedAt time.Time `json:"started_at,omitzero"` // CompletedAt indicates the date/time this hook was completed. - CompletedAt time.Time `json:"completed_at,omitempty"` + CompletedAt time.Time `json:"completed_at,omitzero"` // Phase indicates whether the hook completed successfully Phase HookPhase `json:"phase"` } diff --git a/pkg/release/v1/info.go b/pkg/release/v1/info.go index ff98ab63e..cef7e9960 100644 --- a/pkg/release/v1/info.go +++ b/pkg/release/v1/info.go @@ -16,19 +16,19 @@ limitations under the License. package v1 import ( - "k8s.io/apimachinery/pkg/runtime" + "time" - "helm.sh/helm/v4/pkg/time" + "k8s.io/apimachinery/pkg/runtime" ) // Info describes release information. type Info struct { // FirstDeployed is when the release was first deployed. - FirstDeployed time.Time `json:"first_deployed,omitempty"` + FirstDeployed time.Time `json:"first_deployed,omitzero"` // LastDeployed is when the release was last deployed. - LastDeployed time.Time `json:"last_deployed,omitempty"` + LastDeployed time.Time `json:"last_deployed,omitzero"` // Deleted tracks when this object was deleted. - Deleted time.Time `json:"deleted"` + Deleted time.Time `json:"deleted,omitzero"` // Description is human-friendly "log entry" about this release. Description string `json:"description,omitempty"` // Status is the current state of the release diff --git a/pkg/release/v1/mock.go b/pkg/release/v1/mock.go index c3a6594cc..818cd777e 100644 --- a/pkg/release/v1/mock.go +++ b/pkg/release/v1/mock.go @@ -19,10 +19,10 @@ package v1 import ( "fmt" "math/rand" + "time" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" - "helm.sh/helm/v4/pkg/time" ) // MockHookTemplate is the hook template used for all mock release objects. diff --git a/pkg/release/v1/util/sorter_test.go b/pkg/release/v1/util/sorter_test.go index 4628a5192..0889ddb94 100644 --- a/pkg/release/v1/util/sorter_test.go +++ b/pkg/release/v1/util/sorter_test.go @@ -21,7 +21,6 @@ import ( "time" rspb "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) // note: this test data is shared with filter_test.go. @@ -34,7 +33,7 @@ var releases = []*rspb.Release{ } func tsRelease(name string, vers int, dur time.Duration, status rspb.Status) *rspb.Release { - info := &rspb.Info{Status: status, LastDeployed: helmtime.Now().Add(dur)} + info := &rspb.Info{Status: status, LastDeployed: time.Now().Add(dur)} return &rspb.Release{ Name: name, Version: vers, diff --git a/pkg/strvals/literal_parser_test.go b/pkg/strvals/literal_parser_test.go index 4e74423d6..6a76458f5 100644 --- a/pkg/strvals/literal_parser_test.go +++ b/pkg/strvals/literal_parser_test.go @@ -17,6 +17,7 @@ package strvals import ( "fmt" + "strings" "testing" "sigs.k8s.io/yaml" @@ -416,14 +417,14 @@ func TestParseLiteralInto(t *testing.T) { } func TestParseLiteralNestedLevels(t *testing.T) { - var keyMultipleNestedLevels string + var keyMultipleNestedLevels strings.Builder for i := 1; i <= MaxNestedNameLevel+2; i++ { tmpStr := fmt.Sprintf("name%d", i) if i <= MaxNestedNameLevel+1 { tmpStr = tmpStr + "." } - keyMultipleNestedLevels += tmpStr + keyMultipleNestedLevels.WriteString(tmpStr) } tests := []struct { @@ -439,7 +440,7 @@ func TestParseLiteralNestedLevels(t *testing.T) { "", }, { - str: keyMultipleNestedLevels + "=value", + str: keyMultipleNestedLevels.String() + "=value", err: true, errStr: fmt.Sprintf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel), }, diff --git a/pkg/strvals/parser_test.go b/pkg/strvals/parser_test.go index a0c67b791..73403fc52 100644 --- a/pkg/strvals/parser_test.go +++ b/pkg/strvals/parser_test.go @@ -17,6 +17,7 @@ package strvals import ( "fmt" + "strings" "testing" "sigs.k8s.io/yaml" @@ -757,13 +758,13 @@ func TestToYAML(t *testing.T) { } func TestParseSetNestedLevels(t *testing.T) { - var keyMultipleNestedLevels string + var keyMultipleNestedLevels strings.Builder for i := 1; i <= MaxNestedNameLevel+2; i++ { tmpStr := fmt.Sprintf("name%d", i) if i <= MaxNestedNameLevel+1 { tmpStr = tmpStr + "." } - keyMultipleNestedLevels += tmpStr + keyMultipleNestedLevels.WriteString(tmpStr) } tests := []struct { str string @@ -778,7 +779,7 @@ func TestParseSetNestedLevels(t *testing.T) { "", }, { - str: keyMultipleNestedLevels + "=value", + str: keyMultipleNestedLevels.String() + "=value", err: true, errStr: fmt.Sprintf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel), diff --git a/pkg/time/ctime/ctime.go b/pkg/time/ctime/ctime.go deleted file mode 100644 index 63a41c0bf..000000000 --- a/pkg/time/ctime/ctime.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright The Helm Authors. - -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 ctime - -import ( - "os" - "time" -) - -func Created(fi os.FileInfo) time.Time { - return modified(fi) -} - -func Modified(fi os.FileInfo) time.Time { - return modified(fi) -} diff --git a/pkg/time/ctime/ctime_linux.go b/pkg/time/ctime/ctime_linux.go deleted file mode 100644 index d8a6ea1a1..000000000 --- a/pkg/time/ctime/ctime_linux.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build linux - -/* -Copyright The Helm Authors. - -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 ctime - -import ( - "os" - "syscall" - "time" -) - -func modified(fi os.FileInfo) time.Time { - st := fi.Sys().(*syscall.Stat_t) - //nolint - return time.Unix(int64(st.Mtim.Sec), int64(st.Mtim.Nsec)) -} diff --git a/pkg/time/ctime/ctime_other.go b/pkg/time/ctime/ctime_other.go deleted file mode 100644 index 12afc6df2..000000000 --- a/pkg/time/ctime/ctime_other.go +++ /dev/null @@ -1,27 +0,0 @@ -//go:build !linux - -/* -Copyright The Helm Authors. - -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 ctime - -import ( - "os" - "time" -) - -func modified(fi os.FileInfo) time.Time { - return fi.ModTime() -} diff --git a/pkg/time/time.go b/pkg/time/time.go deleted file mode 100644 index 16973b455..000000000 --- a/pkg/time/time.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright The Helm Authors. - -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 time contains a wrapper for time.Time in the standard library and -// associated methods. This package mainly exists to work around an issue in Go -// where the serializer doesn't omit an empty value for time: -// https://github.com/golang/go/issues/11939. As such, this can be removed if a -// proposal is ever accepted for Go -package time - -import ( - "bytes" - "time" -) - -// emptyString contains an empty JSON string value to be used as output -var emptyString = `""` - -// Time is a convenience wrapper around stdlib time, but with different -// marshalling and unmarshalling for zero values -type Time struct { - time.Time -} - -// Now returns the current time. It is a convenience wrapper around time.Now() -func Now() Time { - return Time{time.Now()} -} - -func (t Time) MarshalJSON() ([]byte, error) { - if t.IsZero() { - return []byte(emptyString), nil - } - - return t.Time.MarshalJSON() -} - -func (t *Time) UnmarshalJSON(b []byte) error { - if bytes.Equal(b, []byte("null")) { - return nil - } - // If it is empty, we don't have to set anything since time.Time is not a - // pointer and will be set to the zero value - if bytes.Equal([]byte(emptyString), b) { - return nil - } - - return t.Time.UnmarshalJSON(b) -} - -func Parse(layout, value string) (Time, error) { - t, err := time.Parse(layout, value) - return Time{Time: t}, err -} - -func ParseInLocation(layout, value string, loc *time.Location) (Time, error) { - t, err := time.ParseInLocation(layout, value, loc) - return Time{Time: t}, err -} - -func Date(year int, month time.Month, day, hour, minute, second, nanoSecond int, loc *time.Location) Time { - return Time{Time: time.Date(year, month, day, hour, minute, second, nanoSecond, loc)} -} - -func Unix(sec int64, nsec int64) Time { return Time{Time: time.Unix(sec, nsec)} } - -func (t Time) Add(d time.Duration) Time { return Time{Time: t.Time.Add(d)} } -func (t Time) AddDate(years int, months int, days int) Time { - return Time{Time: t.Time.AddDate(years, months, days)} -} -func (t Time) After(u Time) bool { return t.Time.After(u.Time) } -func (t Time) Before(u Time) bool { return t.Time.Before(u.Time) } -func (t Time) Equal(u Time) bool { return t.Time.Equal(u.Time) } -func (t Time) In(loc *time.Location) Time { return Time{Time: t.Time.In(loc)} } -func (t Time) Local() Time { return Time{Time: t.Time.Local()} } -func (t Time) Round(d time.Duration) Time { return Time{Time: t.Time.Round(d)} } -func (t Time) Sub(u Time) time.Duration { return t.Time.Sub(u.Time) } -func (t Time) Truncate(d time.Duration) Time { return Time{Time: t.Time.Truncate(d)} } -func (t Time) UTC() Time { return Time{Time: t.Time.UTC()} } diff --git a/pkg/time/time_test.go b/pkg/time/time_test.go deleted file mode 100644 index 342ca4a10..000000000 --- a/pkg/time/time_test.go +++ /dev/null @@ -1,153 +0,0 @@ -/* -Copyright The Helm Authors. - -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 time - -import ( - "encoding/json" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - timeParseString = `"1977-09-02T22:04:05Z"` - timeString = "1977-09-02 22:04:05 +0000 UTC" -) - -func givenTime(t *testing.T) Time { - t.Helper() - result, err := Parse(time.RFC3339, "1977-09-02T22:04:05Z") - require.NoError(t, err) - return result -} - -func TestDate(t *testing.T) { - testingTime := givenTime(t) - got := Date(1977, 9, 2, 22, 04, 05, 0, time.UTC) - assert.Equal(t, timeString, got.String()) - assert.True(t, testingTime.Equal(got)) - assert.True(t, got.Equal(testingTime)) -} - -func TestNow(t *testing.T) { - testingTime := givenTime(t) - got := Now() - assert.True(t, testingTime.Before(got)) - assert.True(t, got.After(testingTime)) -} - -func TestTime_Add(t *testing.T) { - testingTime := givenTime(t) - got := testingTime.Add(time.Hour) - assert.Equal(t, timeString, testingTime.String()) - assert.Equal(t, "1977-09-02 23:04:05 +0000 UTC", got.String()) -} - -func TestTime_AddDate(t *testing.T) { - testingTime := givenTime(t) - got := testingTime.AddDate(1, 1, 1) - assert.Equal(t, "1978-10-03 22:04:05 +0000 UTC", got.String()) -} - -func TestTime_In(t *testing.T) { - testingTime := givenTime(t) - edt, err := time.LoadLocation("America/New_York") - assert.NoError(t, err) - got := testingTime.In(edt) - assert.Equal(t, "America/New_York", got.Location().String()) -} - -func TestTime_MarshalJSONNonZero(t *testing.T) { - testingTime := givenTime(t) - res, err := json.Marshal(testingTime) - assert.NoError(t, err) - assert.Equal(t, timeParseString, string(res)) -} - -func TestTime_MarshalJSONZeroValue(t *testing.T) { - res, err := json.Marshal(Time{}) - assert.NoError(t, err) - assert.Equal(t, `""`, string(res)) -} - -func TestTime_Round(t *testing.T) { - testingTime := givenTime(t) - got := testingTime.Round(time.Hour) - assert.Equal(t, timeString, testingTime.String()) - assert.Equal(t, "1977-09-02 22:00:00 +0000 UTC", got.String()) -} - -func TestTime_Sub(t *testing.T) { - testingTime := givenTime(t) - before, err := Parse(time.RFC3339, "1977-09-01T22:04:05Z") - require.NoError(t, err) - got := testingTime.Sub(before) - assert.Equal(t, "24h0m0s", got.String()) -} - -func TestTime_Truncate(t *testing.T) { - testingTime := givenTime(t) - got := testingTime.Truncate(time.Hour) - assert.Equal(t, timeString, testingTime.String()) - assert.Equal(t, "1977-09-02 22:00:00 +0000 UTC", got.String()) -} - -func TestTime_UTC(t *testing.T) { - edtTime, err := Parse(time.RFC3339, "1977-09-03T05:04:05+07:00") - require.NoError(t, err) - got := edtTime.UTC() - assert.Equal(t, timeString, got.String()) -} - -func TestTime_UnmarshalJSONNonZeroValue(t *testing.T) { - testingTime := givenTime(t) - var myTime Time - err := json.Unmarshal([]byte(timeParseString), &myTime) - assert.NoError(t, err) - assert.True(t, testingTime.Equal(myTime)) -} - -func TestTime_UnmarshalJSONEmptyString(t *testing.T) { - var myTime Time - err := json.Unmarshal([]byte(emptyString), &myTime) - assert.NoError(t, err) - assert.True(t, myTime.IsZero()) -} - -func TestTime_UnmarshalJSONNullString(t *testing.T) { - var myTime Time - err := json.Unmarshal([]byte("null"), &myTime) - assert.NoError(t, err) - assert.True(t, myTime.IsZero()) -} - -func TestTime_UnmarshalJSONZeroValue(t *testing.T) { - // This test ensures that we can unmarshal any time value that was output - // with the current go default value of "0001-01-01T00:00:00Z" - var myTime Time - err := json.Unmarshal([]byte(`"0001-01-01T00:00:00Z"`), &myTime) - assert.NoError(t, err) - assert.True(t, myTime.IsZero()) -} - -func TestUnix(t *testing.T) { - got := Unix(242085845, 0) - assert.Equal(t, int64(242085845), got.Unix()) - assert.Equal(t, timeString, got.UTC().String()) -}