Browse Source

Refactor parsing to return error instead of using panic/recover

Currently the parser code relies on recover function to catch errors and
return them to the end user. The recommended go approach is to return
errors from functions and check them. This commit refactors all
log.Panicf calls to actually returning errors.
Maciej Szulik 9 years ago
parent
commit
fcc395c6f4
2 changed files with 237 additions and 110 deletions
  1. 111 77
      parser.go
  2. 126 33
      parser_test.go

+ 111 - 77
parser.go

@@ -2,7 +2,6 @@ package cron
 
 
 import (
 import (
 	"fmt"
 	"fmt"
-	"log"
 	"math"
 	"math"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
@@ -17,36 +16,46 @@ import (
 // It accepts
 // It accepts
 //   - Standard crontab specs, e.g. "* * * * ?"
 //   - Standard crontab specs, e.g. "* * * * ?"
 //   - Descriptors, e.g. "@midnight", "@every 1h30m"
 //   - Descriptors, e.g. "@midnight", "@every 1h30m"
-func ParseStandard(standardSpec string) (_ Schedule, err error) {
-	// Convert panics into errors
-	defer func() {
-		if recovered := recover(); recovered != nil {
-			err = fmt.Errorf("%v", recovered)
-		}
-	}()
-
+func ParseStandard(standardSpec string) (Schedule, error) {
 	if standardSpec[0] == '@' {
 	if standardSpec[0] == '@' {
-		return parseDescriptor(standardSpec), nil
+		return parseDescriptor(standardSpec)
 	}
 	}
 
 
 	// Split on whitespace.  We require exactly 5 fields.
 	// Split on whitespace.  We require exactly 5 fields.
 	// (minute) (hour) (day of month) (month) (day of week)
 	// (minute) (hour) (day of month) (month) (day of week)
 	fields := strings.Fields(standardSpec)
 	fields := strings.Fields(standardSpec)
 	if len(fields) != 5 {
 	if len(fields) != 5 {
-		log.Panicf("Expected exactly 5, found %d: %s", len(fields), standardSpec)
+		return nil, fmt.Errorf("Expected exactly 5 fields, found %d: %s", len(fields), standardSpec)
 	}
 	}
 
 
-	schedule := &SpecSchedule{
-		Second: 1 << seconds.min,
-		Minute: getField(fields[0], minutes),
-		Hour:   getField(fields[1], hours),
-		Dom:    getField(fields[2], dom),
-		Month:  getField(fields[3], months),
-		Dow:    getField(fields[4], dow),
+	var err error
+	field := func(field string, r bounds) uint64 {
+		if err != nil {
+			return uint64(0)
+		}
+		var bits uint64
+		bits, err = getField(field, r)
+		return bits
+	}
+	var (
+		minute     = field(fields[0], minutes)
+		hour       = field(fields[1], hours)
+		dayofmonth = field(fields[2], dom)
+		month      = field(fields[3], months)
+		dayofweek  = field(fields[4], dow)
+	)
+	if err != nil {
+		return nil, err
 	}
 	}
 
 
-	return schedule, nil
-
+	return &SpecSchedule{
+		Second: 1 << seconds.min,
+		Minute: minute,
+		Hour:   hour,
+		Dom:    dayofmonth,
+		Month:  month,
+		Dow:    dayofweek,
+	}, nil
 }
 }
 
 
 // Parse returns a new crontab schedule representing the given spec.
 // Parse returns a new crontab schedule representing the given spec.
@@ -55,23 +64,16 @@ func ParseStandard(standardSpec string) (_ Schedule, err error) {
 // It accepts
 // It accepts
 //   - Full crontab specs, e.g. "* * * * * ?"
 //   - Full crontab specs, e.g. "* * * * * ?"
 //   - Descriptors, e.g. "@midnight", "@every 1h30m"
 //   - Descriptors, e.g. "@midnight", "@every 1h30m"
-func Parse(spec string) (_ Schedule, err error) {
-	// Convert panics into errors
-	defer func() {
-		if recovered := recover(); recovered != nil {
-			err = fmt.Errorf("%v", recovered)
-		}
-	}()
-
+func Parse(spec string) (Schedule, error) {
 	if spec[0] == '@' {
 	if spec[0] == '@' {
-		return parseDescriptor(spec), nil
+		return parseDescriptor(spec)
 	}
 	}
 
 
 	// Split on whitespace.  We require 5 or 6 fields.
 	// Split on whitespace.  We require 5 or 6 fields.
 	// (second) (minute) (hour) (day of month) (month) (day of week, optional)
 	// (second) (minute) (hour) (day of month) (month) (day of week, optional)
 	fields := strings.Fields(spec)
 	fields := strings.Fields(spec)
 	if len(fields) != 5 && len(fields) != 6 {
 	if len(fields) != 5 && len(fields) != 6 {
-		log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
+		return nil, fmt.Errorf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
 	}
 	}
 
 
 	// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
 	// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
@@ -79,39 +81,64 @@ func Parse(spec string) (_ Schedule, err error) {
 		fields = append(fields, "*")
 		fields = append(fields, "*")
 	}
 	}
 
 
-	schedule := &SpecSchedule{
-		Second: getField(fields[0], seconds),
-		Minute: getField(fields[1], minutes),
-		Hour:   getField(fields[2], hours),
-		Dom:    getField(fields[3], dom),
-		Month:  getField(fields[4], months),
-		Dow:    getField(fields[5], dow),
+	var err error
+	field := func(field string, r bounds) uint64 {
+		if err != nil {
+			return uint64(0)
+		}
+		var bits uint64
+		bits, err = getField(field, r)
+		return bits
+	}
+	var (
+		second     = field(fields[0], seconds)
+		minute     = field(fields[1], minutes)
+		hour       = field(fields[2], hours)
+		dayofmonth = field(fields[3], dom)
+		month      = field(fields[4], months)
+		dayofweek  = field(fields[5], dow)
+	)
+	if err != nil {
+		return nil, err
 	}
 	}
 
 
-	return schedule, nil
+	return &SpecSchedule{
+		Second: second,
+		Minute: minute,
+		Hour:   hour,
+		Dom:    dayofmonth,
+		Month:  month,
+		Dow:    dayofweek,
+	}, nil
 }
 }
 
 
 // getField returns an Int with the bits set representing all of the times that
 // getField returns an Int with the bits set representing all of the times that
-// the field represents.  A "field" is a comma-separated list of "ranges".
-func getField(field string, r bounds) uint64 {
-	// list = range {"," range}
+// the field represents or error parsing field value.  A "field" is a comma-separated
+// list of "ranges".
+func getField(field string, r bounds) (uint64, error) {
 	var bits uint64
 	var bits uint64
 	ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
 	ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
 	for _, expr := range ranges {
 	for _, expr := range ranges {
-		bits |= getRange(expr, r)
+		bit, err := getRange(expr, r)
+		if err != nil {
+			return bits, err
+		}
+		bits |= bit
 	}
 	}
-	return bits
+	return bits, nil
 }
 }
 
 
 // getRange returns the bits indicated by the given expression:
 // getRange returns the bits indicated by the given expression:
 //   number | number "-" number [ "/" number ]
 //   number | number "-" number [ "/" number ]
-func getRange(expr string, r bounds) uint64 {
-
+// or error parsing range.
+func getRange(expr string, r bounds) (uint64, error) {
 	var (
 	var (
 		start, end, step uint
 		start, end, step uint
 		rangeAndStep     = strings.Split(expr, "/")
 		rangeAndStep     = strings.Split(expr, "/")
 		lowAndHigh       = strings.Split(rangeAndStep[0], "-")
 		lowAndHigh       = strings.Split(rangeAndStep[0], "-")
 		singleDigit      = len(lowAndHigh) == 1
 		singleDigit      = len(lowAndHigh) == 1
+		err              error
+		zero             = uint64(0)
 	)
 	)
 
 
 	var extra_star uint64
 	var extra_star uint64
@@ -120,14 +147,20 @@ func getRange(expr string, r bounds) uint64 {
 		end = r.max
 		end = r.max
 		extra_star = starBit
 		extra_star = starBit
 	} else {
 	} else {
-		start = parseIntOrName(lowAndHigh[0], r.names)
+		start, err = parseIntOrName(lowAndHigh[0], r.names)
+		if err != nil {
+			return zero, err
+		}
 		switch len(lowAndHigh) {
 		switch len(lowAndHigh) {
 		case 1:
 		case 1:
 			end = start
 			end = start
 		case 2:
 		case 2:
-			end = parseIntOrName(lowAndHigh[1], r.names)
+			end, err = parseIntOrName(lowAndHigh[1], r.names)
+			if err != nil {
+				return zero, err
+			}
 		default:
 		default:
-			log.Panicf("Too many hyphens: %s", expr)
+			return zero, fmt.Errorf("Too many hyphens: %s", expr)
 		}
 		}
 	}
 	}
 
 
@@ -135,53 +168,56 @@ func getRange(expr string, r bounds) uint64 {
 	case 1:
 	case 1:
 		step = 1
 		step = 1
 	case 2:
 	case 2:
-		step = mustParseInt(rangeAndStep[1])
+		step, err = mustParseInt(rangeAndStep[1])
+		if err != nil {
+			return zero, err
+		}
 
 
 		// Special handling: "N/step" means "N-max/step".
 		// Special handling: "N/step" means "N-max/step".
 		if singleDigit {
 		if singleDigit {
 			end = r.max
 			end = r.max
 		}
 		}
 	default:
 	default:
-		log.Panicf("Too many slashes: %s", expr)
+		return zero, fmt.Errorf("Too many slashes: %s", expr)
 	}
 	}
 
 
 	if start < r.min {
 	if start < r.min {
-		log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
+		return zero, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
 	}
 	}
 	if end > r.max {
 	if end > r.max {
-		log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
+		return zero, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
 	}
 	}
 	if start > end {
 	if start > end {
-		log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
+		return zero, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
 	}
 	}
 	if step == 0 {
 	if step == 0 {
-		log.Panicf("Step of range should be a positive number: %s", expr)
+		return zero, fmt.Errorf("Step of range should be a positive number: %s", expr)
 	}
 	}
 
 
-	return getBits(start, end, step) | extra_star
+	return getBits(start, end, step) | extra_star, nil
 }
 }
 
 
 // parseIntOrName returns the (possibly-named) integer contained in expr.
 // parseIntOrName returns the (possibly-named) integer contained in expr.
-func parseIntOrName(expr string, names map[string]uint) uint {
+func parseIntOrName(expr string, names map[string]uint) (uint, error) {
 	if names != nil {
 	if names != nil {
 		if namedInt, ok := names[strings.ToLower(expr)]; ok {
 		if namedInt, ok := names[strings.ToLower(expr)]; ok {
-			return namedInt
+			return namedInt, nil
 		}
 		}
 	}
 	}
 	return mustParseInt(expr)
 	return mustParseInt(expr)
 }
 }
 
 
-// mustParseInt parses the given expression as an int or panics.
-func mustParseInt(expr string) uint {
+// mustParseInt parses the given expression as an int or returns an error.
+func mustParseInt(expr string) (uint, error) {
 	num, err := strconv.Atoi(expr)
 	num, err := strconv.Atoi(expr)
 	if err != nil {
 	if err != nil {
-		log.Panicf("Failed to parse int from %s: %s", expr, err)
+		return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
 	}
 	}
 	if num < 0 {
 	if num < 0 {
-		log.Panicf("Negative number (%d) not allowed: %s", num, expr)
+		return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
 	}
 	}
 
 
-	return uint(num)
+	return uint(num), nil
 }
 }
 
 
 // getBits sets all bits in the range [min, max], modulo the given step size.
 // getBits sets all bits in the range [min, max], modulo the given step size.
@@ -205,10 +241,9 @@ func all(r bounds) uint64 {
 	return getBits(r.min, r.max, 1) | starBit
 	return getBits(r.min, r.max, 1) | starBit
 }
 }
 
 
-// parseDescriptor returns a pre-defined schedule for the expression, or panics
-// if none matches.
-func parseDescriptor(spec string) Schedule {
-	switch spec {
+// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
+func parseDescriptor(descriptor string) (Schedule, error) {
+	switch descriptor {
 	case "@yearly", "@annually":
 	case "@yearly", "@annually":
 		return &SpecSchedule{
 		return &SpecSchedule{
 			Second: 1 << seconds.min,
 			Second: 1 << seconds.min,
@@ -217,7 +252,7 @@ func parseDescriptor(spec string) Schedule {
 			Dom:    1 << dom.min,
 			Dom:    1 << dom.min,
 			Month:  1 << months.min,
 			Month:  1 << months.min,
 			Dow:    all(dow),
 			Dow:    all(dow),
-		}
+		}, nil
 
 
 	case "@monthly":
 	case "@monthly":
 		return &SpecSchedule{
 		return &SpecSchedule{
@@ -227,7 +262,7 @@ func parseDescriptor(spec string) Schedule {
 			Dom:    1 << dom.min,
 			Dom:    1 << dom.min,
 			Month:  all(months),
 			Month:  all(months),
 			Dow:    all(dow),
 			Dow:    all(dow),
-		}
+		}, nil
 
 
 	case "@weekly":
 	case "@weekly":
 		return &SpecSchedule{
 		return &SpecSchedule{
@@ -237,7 +272,7 @@ func parseDescriptor(spec string) Schedule {
 			Dom:    all(dom),
 			Dom:    all(dom),
 			Month:  all(months),
 			Month:  all(months),
 			Dow:    1 << dow.min,
 			Dow:    1 << dow.min,
-		}
+		}, nil
 
 
 	case "@daily", "@midnight":
 	case "@daily", "@midnight":
 		return &SpecSchedule{
 		return &SpecSchedule{
@@ -247,7 +282,7 @@ func parseDescriptor(spec string) Schedule {
 			Dom:    all(dom),
 			Dom:    all(dom),
 			Month:  all(months),
 			Month:  all(months),
 			Dow:    all(dow),
 			Dow:    all(dow),
-		}
+		}, nil
 
 
 	case "@hourly":
 	case "@hourly":
 		return &SpecSchedule{
 		return &SpecSchedule{
@@ -257,18 +292,17 @@ func parseDescriptor(spec string) Schedule {
 			Dom:    all(dom),
 			Dom:    all(dom),
 			Month:  all(months),
 			Month:  all(months),
 			Dow:    all(dow),
 			Dow:    all(dow),
-		}
+		}, nil
 	}
 	}
 
 
 	const every = "@every "
 	const every = "@every "
-	if strings.HasPrefix(spec, every) {
-		duration, err := time.ParseDuration(spec[len(every):])
+	if strings.HasPrefix(descriptor, every) {
+		duration, err := time.ParseDuration(descriptor[len(every):])
 		if err != nil {
 		if err != nil {
-			log.Panicf("Failed to parse duration %s: %s", spec, err)
+			return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)
 		}
 		}
-		return Every(duration)
+		return Every(duration), nil
 	}
 	}
 
 
-	log.Panicf("Unrecognized descriptor: %s", spec)
-	return nil
+	return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
 }
 }

+ 126 - 33
parser_test.go

@@ -2,36 +2,55 @@ package cron
 
 
 import (
 import (
 	"reflect"
 	"reflect"
+	"strings"
 	"testing"
 	"testing"
 	"time"
 	"time"
 )
 )
 
 
 func TestRange(t *testing.T) {
 func TestRange(t *testing.T) {
+	zero := uint64(0)
 	ranges := []struct {
 	ranges := []struct {
 		expr     string
 		expr     string
 		min, max uint
 		min, max uint
 		expected uint64
 		expected uint64
+		err      string
 	}{
 	}{
-		{"5", 0, 7, 1 << 5},
-		{"0", 0, 7, 1 << 0},
-		{"7", 0, 7, 1 << 7},
-
-		{"5-5", 0, 7, 1 << 5},
-		{"5-6", 0, 7, 1<<5 | 1<<6},
-		{"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7},
-
-		{"5-6/2", 0, 7, 1 << 5},
-		{"5-7/2", 0, 7, 1<<5 | 1<<7},
-		{"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7},
-
-		{"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit},
-		{"*/2", 1, 3, 1<<1 | 1<<3 | starBit},
+		{"5", 0, 7, 1 << 5, ""},
+		{"0", 0, 7, 1 << 0, ""},
+		{"7", 0, 7, 1 << 7, ""},
+
+		{"5-5", 0, 7, 1 << 5, ""},
+		{"5-6", 0, 7, 1<<5 | 1<<6, ""},
+		{"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7, ""},
+
+		{"5-6/2", 0, 7, 1 << 5, ""},
+		{"5-7/2", 0, 7, 1<<5 | 1<<7, ""},
+		{"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7, ""},
+
+		{"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit, ""},
+		{"*/2", 1, 3, 1<<1 | 1<<3 | starBit, ""},
+
+		{"5--5", 0, 0, zero, "Too many hyphens"},
+		{"jan-x", 0, 0, zero, "Failed to parse int from"},
+		{"2-x", 1, 5, zero, "Failed to parse int from"},
+		{"*/-12", 0, 0, zero, "Negative number"},
+		{"*//2", 0, 0, zero, "Too many slashes"},
+		{"1", 3, 5, zero, "below minimum"},
+		{"6", 3, 5, zero, "above maximum"},
+		{"5-3", 3, 5, zero, "beyond end of range"},
+		{"*/0", 0, 0, zero, "should be a positive number"},
 	}
 	}
 
 
 	for _, c := range ranges {
 	for _, c := range ranges {
-		actual := getRange(c.expr, bounds{c.min, c.max, nil})
+		actual, err := getRange(c.expr, bounds{c.min, c.max, nil})
+		if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {
+			t.Errorf("%s => expected %v, got %v", c.expr, c.err, err)
+		}
+		if len(c.err) == 0 && err != nil {
+			t.Error("%s => unexpected error %v", c.expr, err)
+		}
 		if actual != c.expected {
 		if actual != c.expected {
-			t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual)
+			t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual)
 		}
 		}
 	}
 	}
 }
 }
@@ -49,14 +68,14 @@ func TestField(t *testing.T) {
 	}
 	}
 
 
 	for _, c := range fields {
 	for _, c := range fields {
-		actual := getField(c.expr, bounds{c.min, c.max, nil})
+		actual, _ := getField(c.expr, bounds{c.min, c.max, nil})
 		if actual != c.expected {
 		if actual != c.expected {
-			t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual)
+			t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual)
 		}
 		}
 	}
 	}
 }
 }
 
 
-func TestBits(t *testing.T) {
+func TestAll(t *testing.T) {
 	allBits := []struct {
 	allBits := []struct {
 		r        bounds
 		r        bounds
 		expected uint64
 		expected uint64
@@ -71,16 +90,17 @@ func TestBits(t *testing.T) {
 	for _, c := range allBits {
 	for _, c := range allBits {
 		actual := all(c.r) // all() adds the starBit, so compensate for that..
 		actual := all(c.r) // all() adds the starBit, so compensate for that..
 		if c.expected|starBit != actual {
 		if c.expected|starBit != actual {
-			t.Errorf("%d-%d/%d => (expected) %b != %b (actual)",
+			t.Errorf("%d-%d/%d => expected %b, got %b",
 				c.r.min, c.r.max, 1, c.expected|starBit, actual)
 				c.r.min, c.r.max, 1, c.expected|starBit, actual)
 		}
 		}
 	}
 	}
+}
 
 
+func TestBits(t *testing.T) {
 	bits := []struct {
 	bits := []struct {
 		min, max, step uint
 		min, max, step uint
 		expected       uint64
 		expected       uint64
 	}{
 	}{
-
 		{0, 0, 1, 0x1},
 		{0, 0, 1, 0x1},
 		{1, 1, 1, 0x2},
 		{1, 1, 1, 0x2},
 		{1, 5, 2, 0x2a}, // 101010
 		{1, 5, 2, 0x2a}, // 101010
@@ -90,28 +110,83 @@ func TestBits(t *testing.T) {
 	for _, c := range bits {
 	for _, c := range bits {
 		actual := getBits(c.min, c.max, c.step)
 		actual := getBits(c.min, c.max, c.step)
 		if c.expected != actual {
 		if c.expected != actual {
-			t.Errorf("%d-%d/%d => (expected) %b != %b (actual)",
+			t.Errorf("%d-%d/%d => expected %b, got %b",
 				c.min, c.max, c.step, c.expected, actual)
 				c.min, c.max, c.step, c.expected, actual)
 		}
 		}
 	}
 	}
 }
 }
 
 
-func TestSpecSchedule(t *testing.T) {
+func TestParse(t *testing.T) {
 	entries := []struct {
 	entries := []struct {
 		expr     string
 		expr     string
 		expected Schedule
 		expected Schedule
+		err      string
 	}{
 	}{
-		{"* 5 * * * *", &SpecSchedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}},
-		{"@every 5m", ConstantDelaySchedule{time.Duration(5) * time.Minute}},
+		{
+			expr: "* 5 * * * *",
+			expected: &SpecSchedule{
+				Second: all(seconds),
+				Minute: 1 << 5,
+				Hour:   all(hours),
+				Dom:    all(dom),
+				Month:  all(months),
+				Dow:    all(dow),
+			},
+		},
+		{
+			expr: "* 5 j * * *",
+			err:  "Failed to parse int from",
+		},
+		{
+			expr:     "@every 5m",
+			expected: ConstantDelaySchedule{Delay: time.Duration(5) * time.Minute},
+		},
+		{
+			expr: "@every Xm",
+			err:  "Failed to parse duration",
+		},
+		{
+			expr: "@yearly",
+			expected: &SpecSchedule{
+				Second: 1 << seconds.min,
+				Minute: 1 << minutes.min,
+				Hour:   1 << hours.min,
+				Dom:    1 << dom.min,
+				Month:  1 << months.min,
+				Dow:    all(dow),
+			},
+		},
+		{
+			expr: "@annually",
+			expected: &SpecSchedule{
+				Second: 1 << seconds.min,
+				Minute: 1 << minutes.min,
+				Hour:   1 << hours.min,
+				Dom:    1 << dom.min,
+				Month:  1 << months.min,
+				Dow:    all(dow),
+			},
+		},
+		{
+			expr: "@unrecognized",
+			err:  "Unrecognized descriptor",
+		},
+		{
+			expr: "* * * *",
+			err:  "Expected 5 or 6 fields",
+		},
 	}
 	}
 
 
 	for _, c := range entries {
 	for _, c := range entries {
 		actual, err := Parse(c.expr)
 		actual, err := Parse(c.expr)
-		if err != nil {
-			t.Error(err)
+		if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {
+			t.Error("%s => expected %v, got %v", c.expr, c.err, err)
+		}
+		if len(c.err) == 0 && err != nil {
+			t.Error("%s => unexpected error %v", c.expr, err)
 		}
 		}
 		if !reflect.DeepEqual(actual, c.expected) {
 		if !reflect.DeepEqual(actual, c.expected) {
-			t.Errorf("%s => (expected) %b != %b (actual)", c.expr, c.expected, actual)
+			t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
 		}
 		}
 	}
 	}
 }
 }
@@ -120,18 +195,36 @@ func TestStandardSpecSchedule(t *testing.T) {
 	entries := []struct {
 	entries := []struct {
 		expr     string
 		expr     string
 		expected Schedule
 		expected Schedule
+		err      string
 	}{
 	}{
-		{"5 * * * *", &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow)}},
-		{"@every 5m", ConstantDelaySchedule{time.Duration(5) * time.Minute}},
+		{
+			expr:     "5 * * * *",
+			expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow)},
+		},
+		{
+			expr:     "@every 5m",
+			expected: ConstantDelaySchedule{time.Duration(5) * time.Minute},
+		},
+		{
+			expr: "5 j * * *",
+			err:  "Failed to parse int from",
+		},
+		{
+			expr: "* * * *",
+			err:  "Expected exactly 5 fields",
+		},
 	}
 	}
 
 
 	for _, c := range entries {
 	for _, c := range entries {
 		actual, err := ParseStandard(c.expr)
 		actual, err := ParseStandard(c.expr)
-		if err != nil {
-			t.Error(err)
+		if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) {
+			t.Error("%s => expected %v, got %v", c.expr, c.err, err)
+		}
+		if len(c.err) == 0 && err != nil {
+			t.Error("%s => unexpected error %v", c.expr, err)
 		}
 		}
 		if !reflect.DeepEqual(actual, c.expected) {
 		if !reflect.DeepEqual(actual, c.expected) {
-			t.Errorf("%s => (expected) %b != %b (actual)", c.expr, c.expected, actual)
+			t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
 		}
 		}
 	}
 	}
 }
 }