diff --git a/cmd/update-main.go b/cmd/update-main.go index 96641e67a..4ab3f8f23 100644 --- a/cmd/update-main.go +++ b/cmd/update-main.go @@ -22,7 +22,6 @@ import ( "io/ioutil" "net/http" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -68,61 +67,89 @@ const ( minioReleaseURL = "https://dl.minio.io/server/minio/release/" + runtime.GOOS + "-" + runtime.GOARCH + "/" ) -func getCurrentReleaseTime(minioVersion, minioBinaryPath string) (releaseTime time.Time, err error) { - if releaseTime, err = time.Parse(time.RFC3339, minioVersion); err == nil { - return releaseTime, err +var ( + // Newer official download info URLs appear earlier below. + minioReleaseInfoURLs = []string{ + minioReleaseURL + "minio.sha256sum", + minioReleaseURL + "minio.shasum", } +) - if !filepath.IsAbs(minioBinaryPath) { - // Make sure to look for the absolute path of the binary. - minioBinaryPath, err = exec.LookPath(minioBinaryPath) - if err != nil { - return releaseTime, err - } +// minioVersionToReleaseTime - parses a standard official release +// Minio version string. +// +// An official binary's version string is the release time formatted +// with RFC3339 (in UTC) - e.g. `2017-09-29T19:16:56Z` +func minioVersionToReleaseTime(version string) (releaseTime time.Time, err error) { + return time.Parse(time.RFC3339, version) +} + +// releaseTimeToReleaseTag - converts a time to a string formatted as +// an official Minio release tag. +// +// An official minio release tag looks like: +// `RELEASE.2017-09-29T19-16-56Z` +func releaseTimeToReleaseTag(releaseTime time.Time) string { + return "RELEASE." + releaseTime.Format(minioReleaseTagTimeLayout) +} + +// releaseTagToReleaseTime - reverse of `releaseTimeToReleaseTag()` +func releaseTagToReleaseTime(releaseTag string) (releaseTime time.Time, err error) { + tagTimePart := strings.TrimPrefix(releaseTag, "RELEASE.") + if tagTimePart == releaseTag { + return releaseTime, fmt.Errorf("%s is not a valid release tag", releaseTag) + } + return time.Parse(minioReleaseTagTimeLayout, tagTimePart) +} + +// getModTime - get the file modification time of `path` +func getModTime(path string) (t time.Time, err error) { + // Convert to absolute path + absPath, err := filepath.Abs(path) + if err != nil { + return t, fmt.Errorf("Unable to get absolute path of %s. %s", path, err) } - // Looks like version is minio non-standard, we use minio binary's ModTime as release time. - fi, err := osStat(minioBinaryPath) + // Get Stat info + fi, err := osStat(absPath) if err != nil { - err = fmt.Errorf("Unable to get ModTime of %s. %s", minioBinaryPath, err) - } else { - releaseTime = fi.ModTime().UTC() + return t, fmt.Errorf("Unable to get ModTime of %s. %s", absPath, err) } - return releaseTime, err + // Return the ModTime + return fi.ModTime().UTC(), nil } -// GetCurrentReleaseTime - returns this process's release time. If it is official minio version, -// parsed version is returned else minio binary's mod time is returned. +// GetCurrentReleaseTime - returns this process's release time. If it +// is official minio version, parsed version is returned else minio +// binary's mod time is returned. func GetCurrentReleaseTime() (releaseTime time.Time, err error) { - return getCurrentReleaseTime(Version, os.Args[0]) + if releaseTime, err = minioVersionToReleaseTime(Version); err == nil { + return releaseTime, err + } + + // Looks like version is minio non-standard, we use minio + // binary's ModTime as release time: + return getModTime(os.Args[0]) } -// Check if we are indeed inside docker. +// IsDocker - returns if the environment minio is running in docker or +// not. The check is a simple file existence check. +// // https://github.com/moby/moby/blob/master/daemon/initlayer/setup_unix.go#L25 // // "/.dockerenv": "file", // -func isDocker(dockerEnvFile string) (ok bool, err error) { - _, err = os.Stat(dockerEnvFile) - if err != nil { - if os.IsNotExist(err) { - err = nil - } - return false, err +func IsDocker() bool { + _, err := osStat("/.dockerenv") + if os.IsNotExist(err) { + return false } - return true, nil -} -// IsDocker - returns if the environment minio is running -// is docker or not. -func IsDocker() bool { - found, err := isDocker("/.dockerenv") - // We don't need to fail for this check, log - // an error and return false. + // Log error, as we will not propagate it to caller errorIf(err, "Error in docker check.") - return found + return err == nil } // IsDCOS returns true if minio is running in DCOS. @@ -147,9 +174,12 @@ func IsKubernetes() bool { func getHelmVersion(helmInfoFilePath string) string { // Read the file exists. helmInfoFile, err := os.Open(helmInfoFilePath) - // Log errors and return "" as Minio can be deployed without Helm charts as well. - if err != nil && !os.IsNotExist(err) { - errorIf(err, "Unable to read %s", helmInfoFilePath) + if err != nil { + // Log errors and return "" as Minio can be deployed + // without Helm charts as well. + if !os.IsNotExist(err) { + errorIf(err, "Unable to read %s", helmInfoFilePath) + } return "" } @@ -165,46 +195,55 @@ func getHelmVersion(helmInfoFilePath string) string { return "" } -func isSourceBuild(minioVersion string) bool { - _, err := time.Parse(time.RFC3339, minioVersion) - return err != nil -} - -// IsSourceBuild - returns if this binary is made from source or not. +// IsSourceBuild - returns if this binary is a non-official build from +// source code. func IsSourceBuild() bool { - return isSourceBuild(Version) + _, err := minioVersionToReleaseTime(Version) + return err != nil } // DO NOT CHANGE USER AGENT STYLE. // The style should be // -// Minio (; [; dcos][; kubernetes][; docker][; source]) Minio/ Minio/ Minio/ [Minio/univers-] +// Minio (; [; ][; dcos][; kubernetes][; docker][; source]) Minio/ Minio/ Minio/ [Minio/universe-] [Minio/helm-] // -// For any change here should be discussed by openning an issue at https://github.com/minio/minio/issues. +// Any change here should be discussed by opening an issue at +// https://github.com/minio/minio/issues. func getUserAgent(mode string) string { - userAgent := "Minio (" + runtime.GOOS + "; " + runtime.GOARCH + + userAgentParts := []string{} + // Helper function to concisely append a pair of strings to a + // the user-agent slice. + uaAppend := func(p, q string) { + userAgentParts = append(userAgentParts, p, q) + } + + uaAppend("Minio (", runtime.GOOS) + uaAppend("; ", runtime.GOARCH) if mode != "" { - userAgent += "; " + mode + uaAppend("; ", mode) } if IsDCOS() { - userAgent += "; dcos" + uaAppend("; ", "dcos") } if IsKubernetes() { - userAgent += "; kubernetes" + uaAppend("; ", "kubernetes") } if IsDocker() { - userAgent += "; docker" + uaAppend("; ", "docker") } if IsSourceBuild() { - userAgent += "; source" + uaAppend("; ", "source") } - userAgent += ") Minio/" + Version + " Minio/" + ReleaseTag + " Minio/" + CommitID + uaAppend(") Minio/", Version) + uaAppend(" Minio/", ReleaseTag) + uaAppend(" Minio/", CommitID) if IsDCOS() { universePkgVersion := os.Getenv("MARATHON_APP_LABEL_DCOS_PACKAGE_VERSION") // On DC/OS environment try to the get universe package version. if universePkgVersion != "" { - userAgent += " Minio/" + "universe-" + universePkgVersion + uaAppend(" Minio/universe-", universePkgVersion) } } @@ -212,17 +251,17 @@ func getUserAgent(mode string) string { // In Kubernetes environment, try to fetch the helm package version helmChartVersion := getHelmVersion("/podinfo/labels") if helmChartVersion != "" { - userAgent += " Minio/" + "helm-" + helmChartVersion + uaAppend(" Minio/helm-", helmChartVersion) } } - return userAgent + return strings.Join(userAgentParts, "") } -func downloadReleaseData(releaseChecksumURL string, timeout time.Duration, mode string) (data string, err error) { +func downloadReleaseURL(releaseChecksumURL string, timeout time.Duration, mode string) (content string, err error) { req, err := http.NewRequest("GET", releaseChecksumURL, nil) if err != nil { - return data, err + return content, err } req.Header.Set("User-Agent", getUserAgent(mode)) @@ -236,34 +275,43 @@ func downloadReleaseData(releaseChecksumURL string, timeout time.Duration, mode resp, err := client.Do(req) if err != nil { - return data, err + return content, err } if resp == nil { - return data, fmt.Errorf("No response from server to download URL %s", releaseChecksumURL) + return content, fmt.Errorf("No response from server to download URL %s", releaseChecksumURL) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return data, fmt.Errorf("Error downloading URL %s. Response: %v", releaseChecksumURL, resp.Status) + return content, fmt.Errorf("Error downloading URL %s. Response: %v", releaseChecksumURL, resp.Status) } - dataBytes, err := ioutil.ReadAll(resp.Body) + contentBytes, err := ioutil.ReadAll(resp.Body) if err != nil { - return data, fmt.Errorf("Error reading response. %s", err) + return content, fmt.Errorf("Error reading response. %s", err) } - data = string(dataBytes) - return data, err + return string(contentBytes), err } // DownloadReleaseData - downloads release data from minio official server. func DownloadReleaseData(timeout time.Duration, mode string) (data string, err error) { - data, err = downloadReleaseData(minioReleaseURL+"minio.shasum", timeout, mode) - if err == nil { - return data, nil + for _, url := range minioReleaseInfoURLs { + data, err = downloadReleaseURL(url, timeout, mode) + if err == nil { + return data, err + } } - return downloadReleaseData(minioReleaseURL+"minio.sha256sum", timeout, mode) + return data, fmt.Errorf("Failed to fetch release URL - last error: %s", err) } +// parseReleaseData - parses release info file content fetched from +// official minio download server. +// +// The expected format is a single line with two words like: +// +// fbe246edbd382902db9a4035df7dce8cb441357d minio.RELEASE.2016-10-07T01-16-39Z +// +// The second word must be `minio.` appended to a standard release tag. func parseReleaseData(data string) (releaseTime time.Time, err error) { fields := strings.Fields(data) if len(fields) != 2 { @@ -272,19 +320,20 @@ func parseReleaseData(data string) (releaseTime time.Time, err error) { } releaseInfo := fields[1] - if fields = strings.Split(releaseInfo, "."); len(fields) != 3 { + + fields = strings.SplitN(releaseInfo, ".", 2) + if len(fields) != 2 { err = fmt.Errorf("Unknown release information `%s`", releaseInfo) return releaseTime, err } - - if !(fields[0] == "minio" && fields[1] == "RELEASE") { - err = fmt.Errorf("Unknown release '%s'", releaseInfo) + if fields[0] != "minio" { + err = fmt.Errorf("Unknown release `%s`", releaseInfo) return releaseTime, err } - releaseTime, err = time.Parse(minioReleaseTagTimeLayout, fields[2]) + releaseTime, err = releaseTagToReleaseTime(fields[1]) if err != nil { - err = fmt.Errorf("Unknown release time format. %s", err) + err = fmt.Errorf("Unknown release tag format. %s", err) } return releaseTime, err @@ -307,7 +356,7 @@ const ( mesosDeploymentDoc = "https://docs.minio.io/docs/deploy-minio-on-dc-os" ) -func getDownloadURL(buildDate time.Time) (downloadURL string) { +func getDownloadURL(releaseTag string) (downloadURL string) { // Check if we are in DCOS environment, return // deployment guide for update procedures. if IsDCOS() { @@ -323,11 +372,10 @@ func getDownloadURL(buildDate time.Time) (downloadURL string) { // Check if we are docker environment, return docker update command if IsDocker() { // Construct release tag name. - rTag := "RELEASE." + buildDate.Format(minioReleaseTagTimeLayout) - return fmt.Sprintf("docker pull minio/minio:%s", rTag) + return fmt.Sprintf("docker pull minio/minio:%s", releaseTag) } - // For binary only installations, then we just show binary download link. + // For binary only installations, we return link to the latest binary. if runtime.GOOS == "windows" { return minioReleaseURL + "minio.exe" } @@ -335,9 +383,7 @@ func getDownloadURL(buildDate time.Time) (downloadURL string) { return minioReleaseURL + "minio" } -func getUpdateInfo(timeout time.Duration, mode string) (older time.Duration, - downloadURL string, err error) { - +func getUpdateInfo(timeout time.Duration, mode string) (older time.Duration, downloadURL string, err error) { var currentReleaseTime, latestReleaseTime time.Time currentReleaseTime, err = GetCurrentReleaseTime() if err != nil { @@ -351,7 +397,7 @@ func getUpdateInfo(timeout time.Duration, mode string) (older time.Duration, if latestReleaseTime.After(currentReleaseTime) { older = latestReleaseTime.Sub(currentReleaseTime) - downloadURL = getDownloadURL(latestReleaseTime) + downloadURL = getDownloadURL(releaseTimeToReleaseTag(latestReleaseTime)) } return older, downloadURL, nil diff --git a/cmd/update-main_test.go b/cmd/update-main_test.go index 7e5bc15c5..c725e959f 100644 --- a/cmd/update-main_test.go +++ b/cmd/update-main_test.go @@ -17,21 +17,70 @@ package cmd import ( - "errors" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" - "os/exec" - "path/filepath" "runtime" "testing" "time" ) +func TestMinioVersionToReleaseTime(t *testing.T) { + testCases := []struct { + version string + isOfficial bool + }{ + {"2017-09-29T19:16:56Z", true}, + {"RELEASE.2017-09-29T19-16-56Z", false}, + {"DEVELOPMENT.GOGET", false}, + } + for i, testCase := range testCases { + _, err := minioVersionToReleaseTime(testCase.version) + if (err == nil) != testCase.isOfficial { + t.Errorf("Test %d: Expected %v but got %v", + i+1, testCase.isOfficial, err == nil) + } + } +} + +func TestReleaseTagToNFromTimeConversion(t *testing.T) { + utcLoc, _ := time.LoadLocation("") + testCases := []struct { + t time.Time + tag string + errStr string + }{ + {time.Date(2017, time.September, 29, 19, 16, 56, 0, utcLoc), + "RELEASE.2017-09-29T19-16-56Z", ""}, + {time.Date(2017, time.August, 5, 0, 0, 53, 0, utcLoc), + "RELEASE.2017-08-05T00-00-53Z", ""}, + {time.Now().UTC(), "2017-09-29T19:16:56Z", + "2017-09-29T19:16:56Z is not a valid release tag"}, + {time.Now().UTC(), "DEVELOPMENT.GOGET", + "DEVELOPMENT.GOGET is not a valid release tag"}, + } + for i, testCase := range testCases { + if testCase.errStr != "" { + got := releaseTimeToReleaseTag(testCase.t) + if got != testCase.tag && testCase.errStr == "" { + t.Errorf("Test %d: Expected %v but got %v", i+1, testCase.tag, got) + } + } + tagTime, err := releaseTagToReleaseTime(testCase.tag) + if err != nil && err.Error() != testCase.errStr { + t.Errorf("Test %d: Expected %v but got %v", i+1, testCase.errStr, err.Error()) + } + if err == nil && tagTime != testCase.t { + t.Errorf("Test %d: Expected %v but got %v", i+1, testCase.t, tagTime) + } + } + +} + func TestDownloadURL(t *testing.T) { - minioVersion1 := UTCNow() + minioVersion1 := releaseTimeToReleaseTag(UTCNow()) durl := getDownloadURL(minioVersion1) if runtime.GOOS == "windows" { if durl != minioReleaseURL+"minio.exe" { @@ -58,112 +107,6 @@ func TestDownloadURL(t *testing.T) { os.Unsetenv("MESOS_CONTAINER_NAME") } -func TestGetCurrentReleaseTime(t *testing.T) { - minioVersion1 := UTCNow().Format(time.RFC3339) - releaseTime1, _ := time.Parse(time.RFC3339, minioVersion1) - - minioVersion2 := "DEVELOPMENT.GOGET" - tmpfile1, err := ioutil.TempFile("", "get-current-release-time-testcase-1") - if err != nil { - t.Fatalf("Unable to create temporary file. %s", err) - } - defer os.Remove(tmpfile1.Name()) - - minioBinaryPath2 := tmpfile1.Name() - fi, err := tmpfile1.Stat() - if err != nil { - t.Fatalf("Unable to get temporary file info. %s", err) - } - if err = tmpfile1.Close(); err != nil { - t.Fatalf("Unable to create temporary file. %s", err) - } - releaseTime2 := fi.ModTime().UTC() - - minioBinaryPath3 := "go" - if runtime.GOOS == globalWindowsOSName { - minioBinaryPath3 = "go.exe" - } - goBinAbsPath, err := exec.LookPath(minioBinaryPath3) - if err != nil { - t.Fatal(err) - } - fi, err = osStat(goBinAbsPath) - if err != nil { - t.Fatal(err) - } - releaseTime3 := fi.ModTime().UTC() - - // Get a non-absolute binary path. - minioBinaryPath4 := filepath.Base(tmpfile1.Name()) - - // Get a non-existent absolute binary path - minioBinaryPath5 := "/tmp/non-existent-file" - if runtime.GOOS == globalWindowsOSName { - minioBinaryPath5 = "C:\\tmp\\non-existent-file" - } - errorMessage1 := "exec: \"\": executable file not found in $PATH" - if runtime.GOOS == globalWindowsOSName { - errorMessage1 = "exec: \"\": executable file not found in %PATH%" - } - errorMessage2 := "exec: \"non-existent-file\": executable file not found in $PATH" - if runtime.GOOS == globalWindowsOSName { - errorMessage2 = "exec: \"non-existent-file\": executable file not found in %PATH%" - } - errorMessage3 := fmt.Sprintf("exec: \"%s\": executable file not found in $PATH", minioBinaryPath4) - if runtime.GOOS == globalWindowsOSName { - errorMessage3 = "exec: \"" + minioBinaryPath4 + "\": executable file not found in %PATH%" - } - errorMessage4 := "Unable to get ModTime of /tmp/non-existent-file. stat /tmp/non-existent-file: no such file or directory" - if runtime.GOOS == "windows" { - errorMessage4 = "Unable to get ModTime of C:\\tmp\\non-existent-file. CreateFile C:\\tmp\\non-existent-file: The system cannot find the path specified." - } - - testCases := []struct { - minioVersion string - minioBinaryPath string - expectedResult time.Time - expectedErr error - }{ - {minioVersion1, "", releaseTime1, nil}, - {minioVersion1, minioBinaryPath2, releaseTime1, nil}, - {minioVersion2, minioBinaryPath2, releaseTime2, nil}, - {minioVersion2, minioBinaryPath3, releaseTime3, nil}, - {"junk", minioBinaryPath2, releaseTime2, nil}, - {"3.2.0", minioBinaryPath2, releaseTime2, nil}, - {minioVersion2, "", time.Time{}, errors.New(errorMessage1)}, - {"junk", "non-existent-file", time.Time{}, errors.New(errorMessage2)}, - {"3.2.0", "non-existent-file", time.Time{}, errors.New(errorMessage2)}, - {minioVersion2, minioBinaryPath4, time.Time{}, errors.New(errorMessage3)}, - {minioVersion2, minioBinaryPath5, time.Time{}, errors.New(errorMessage4)}, - } - - if runtime.GOOS == "linux" { - testCases = append(testCases, struct { - minioVersion string - minioBinaryPath string - expectedResult time.Time - expectedErr error - }{"3.2a", "/proc/1/cwd", time.Time{}, errors.New("Unable to get ModTime of /proc/1/cwd. stat /proc/1/cwd: permission denied")}) - } - - for _, testCase := range testCases { - result, err := getCurrentReleaseTime(testCase.minioVersion, testCase.minioBinaryPath) - if testCase.expectedErr == nil { - if err != nil { - t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err) - } - } else if err == nil { - t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err) - } else if testCase.expectedErr.Error() != err.Error() { - t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err) - } - - if !testCase.expectedResult.Equal(result) { - t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, result) - } - } -} - // Tests user agent string. func TestUserAgent(t *testing.T) { testCases := []struct { @@ -277,80 +220,6 @@ pod-template-hash="818089471"`) } } -// Tests if the environment we are running is in docker. -func TestIsDocker(t *testing.T) { - createTempFile := func(content string) string { - tmpfile, err := ioutil.TempFile("", "isdocker-testcase") - if err != nil { - t.Fatalf("Unable to create temporary file. %s", err) - } - if _, err = tmpfile.Write([]byte(content)); err != nil { - t.Fatalf("Unable to create temporary file. %s", err) - } - if err = tmpfile.Close(); err != nil { - t.Fatalf("Unable to create temporary file. %s", err) - } - return tmpfile.Name() - } - - filename := createTempFile("") - defer os.Remove(filename) - - testCases := []struct { - filename string - expectedResult bool - expectedErr error - }{ - {"", false, nil}, - {"/tmp/non-existing-file", false, nil}, - {filename, true, nil}, - } - - if runtime.GOOS == "linux" { - testCases = append(testCases, struct { - filename string - expectedResult bool - expectedErr error - }{"/proc/1/cwd", false, errors.New("stat /proc/1/cwd: permission denied")}) - } - - for _, testCase := range testCases { - result, err := isDocker(testCase.filename) - if testCase.expectedErr == nil { - if err != nil { - t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err) - } - } else if err == nil { - t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err) - } else if testCase.expectedErr.Error() != err.Error() { - t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err) - } - - if testCase.expectedResult != result { - t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, result) - } - } -} - -func TestIsSourceBuild(t *testing.T) { - testCases := []struct { - minioVersion string - expectedResult bool - }{ - {UTCNow().Format(time.RFC3339), false}, - {"DEVELOPMENT.GOGET", true}, - {"junk", true}, - {"3.2.4", true}, - } - - for _, testCase := range testCases { - result := isSourceBuild(testCase.minioVersion) - if testCase.expectedResult != result { - t.Fatalf("expected: %v, got: %v", testCase.expectedResult, result) - } - } -} - func TestDownloadReleaseData(t *testing.T) { httpServer1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) defer httpServer1.Close() @@ -374,7 +243,7 @@ func TestDownloadReleaseData(t *testing.T) { } for _, testCase := range testCases { - result, err := downloadReleaseData(testCase.releaseChecksumURL, 1*time.Second, "") + result, err := downloadReleaseURL(testCase.releaseChecksumURL, 1*time.Second, "") if testCase.expectedErr == nil { if err != nil { t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err) @@ -392,7 +261,7 @@ func TestDownloadReleaseData(t *testing.T) { } func TestParseReleaseData(t *testing.T) { - releaseTime, _ := time.Parse(minioReleaseTagTimeLayout, "2016-10-07T01-16-39Z") + releaseTime, _ := releaseTagToReleaseTime("RELEASE.2016-10-07T01-16-39Z") testCases := []struct { data string expectedResult time.Time @@ -400,26 +269,26 @@ func TestParseReleaseData(t *testing.T) { }{ {"more than two fields", time.Time{}, fmt.Errorf("Unknown release data `more than two fields`")}, {"more than", time.Time{}, fmt.Errorf("Unknown release information `than`")}, - {"more than.two.fields", time.Time{}, fmt.Errorf("Unknown release 'than.two.fields'")}, - {"more minio.RELEASE.fields", time.Time{}, fmt.Errorf(`Unknown release time format. parsing time "fields" as "2006-01-02T15-04-05Z": cannot parse "fields" as "2006"`)}, + {"more than.two.fields", time.Time{}, fmt.Errorf("Unknown release `than.two.fields`")}, + {"more minio.RELEASE.fields", time.Time{}, fmt.Errorf(`Unknown release tag format. parsing time "fields" as "2006-01-02T15-04-05Z": cannot parse "fields" as "2006"`)}, {"more minio.RELEASE.2016-10-07T01-16-39Z", releaseTime, nil}, {"fbe246edbd382902db9a4035df7dce8cb441357d minio.RELEASE.2016-10-07T01-16-39Z\n", releaseTime, nil}, } - for _, testCase := range testCases { + for i, testCase := range testCases { result, err := parseReleaseData(testCase.data) if testCase.expectedErr == nil { if err != nil { - t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err) + t.Errorf("error case %d: expected: %v, got: %v", i+1, testCase.expectedErr, err) } } else if err == nil { - t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err) + t.Errorf("error case %d: expected: %v, got: %v", i+1, testCase.expectedErr, err) } else if testCase.expectedErr.Error() != err.Error() { - t.Fatalf("error: expected: %v, got: %v", testCase.expectedErr, err) + t.Errorf("error case %d: expected: %v, got: %v", i+1, testCase.expectedErr, err) } if !testCase.expectedResult.Equal(result) { - t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, result) + t.Errorf("case %d: result: expected: %v, got: %v", i+1, testCase.expectedResult, result) } } }