Skip to content

Commit 802a153

Browse files
thomshuttstuarthicks
authored andcommitted
Add Error test cases and simplify duration parsing logic (#41)
* Add Error test cases and simplify duration parsing logic * Moved regexp building outside of parsing function * restructure regex declaration to conform with gofmt
1 parent 732362d commit 802a153

File tree

3 files changed

+81
-75
lines changed

3 files changed

+81
-75
lines changed

mpd/duration.go

+45-56
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,27 @@ package mpd
55
import (
66
"encoding/xml"
77
"errors"
8+
"fmt"
9+
"regexp"
810
"strconv"
911
"strings"
1012
"time"
1113
)
1214

1315
type Duration time.Duration
1416

17+
var (
18+
rStart = "^P" // Must start with a 'P'
19+
rDays = "(\\d+D)?" // We only allow Days for durations, not Months or Years
20+
rTime = "(?:T" // If there's any 'time' units then they must be preceded by a 'T'
21+
rHours = "(\\d+H)?" // Hours
22+
rMinutes = "(\\d+M)?" // Minutes
23+
rSeconds = "([\\d.]+S)?" // Seconds (Potentially decimal)
24+
rEnd = ")?$" // end of regex must close "T" capture group
25+
)
26+
27+
var xmlDurationRegex = regexp.MustCompile(rStart + rDays + rTime + rHours + rMinutes + rSeconds + rEnd)
28+
1529
func (d Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
1630
return xml.Attr{name, d.String()}, nil
1731
}
@@ -141,77 +155,52 @@ func fmtInt(buf []byte, v uint64) int {
141155

142156
func parseDuration(str string) (time.Duration, error) {
143157
if len(str) < 3 {
144-
return 0, errors.New("input duration too short")
158+
return 0, errors.New("At least one number and designator are required")
145159
}
146160

147-
var minus bool
148-
offset := 0
149-
if str[offset] == '-' {
150-
minus = true
151-
offset++
161+
if strings.Contains(str, "-") {
162+
return 0, errors.New("Duration cannot be negative")
152163
}
153164

154-
if str[offset] != 'P' {
155-
return 0, errors.New("input duration does not have a valid prefix")
165+
// Check that only the parts we expect exist and that everything's in the correct order
166+
if !xmlDurationRegex.Match([]byte(str)) {
167+
return 0, errors.New("Duration must be in the format: P[nD][T[nH][nM][nS]]")
156168
}
157-
offset++
158169

159-
var dateStr, timeStr string
160-
if i := strings.IndexByte(str[offset:], 'T'); i != -1 {
161-
dateStr = str[offset : offset+i]
162-
timeStr = str[offset+i+1:]
163-
} else {
164-
dateStr = str[offset:]
165-
}
170+
var parts = xmlDurationRegex.FindStringSubmatch(str)
171+
var total time.Duration
166172

167-
var sum float64
168-
if len(dateStr) > 0 {
169-
if i := strings.IndexByte(dateStr, 'Y'); i != -1 {
170-
return 0, errors.New("input duration contains Years notation")
173+
if parts[1] != "" {
174+
days, err := strconv.Atoi(strings.TrimRight(parts[1], "D"))
175+
if err != nil {
176+
return 0, fmt.Errorf("Error parsing Days: %s", err)
171177
}
178+
total += time.Duration(days) * time.Hour * 24
179+
}
172180

173-
if i := strings.IndexByte(dateStr, 'M'); i != -1 {
174-
return 0, errors.New("input duration contains Months notation")
181+
if parts[2] != "" {
182+
hours, err := strconv.Atoi(strings.TrimRight(parts[2], "H"))
183+
if err != nil {
184+
return 0, fmt.Errorf("Error parsing Hours: %s", err)
175185
}
186+
total += time.Duration(hours) * time.Hour
187+
}
176188

177-
if i := strings.IndexByte(dateStr, 'D'); i != -1 {
178-
days, err := strconv.Atoi(dateStr[0:i])
179-
if err != nil {
180-
return 0, err
181-
}
182-
sum += float64(days) * 86400
189+
if parts[3] != "" {
190+
mins, err := strconv.Atoi(strings.TrimRight(parts[3], "M"))
191+
if err != nil {
192+
return 0, fmt.Errorf("Error parsing Minutes: %s", err)
183193
}
194+
total += time.Duration(mins) * time.Minute
184195
}
185196

186-
if len(timeStr) > 0 {
187-
var pos int
188-
if i := strings.IndexByte(timeStr[pos:], 'H'); i != -1 {
189-
hours, err := strconv.ParseInt(timeStr[pos:pos+i], 10, 64)
190-
if err != nil {
191-
return 0, err
192-
}
193-
sum += float64(hours) * 3600
194-
pos += i + 1
195-
}
196-
if i := strings.IndexByte(timeStr[pos:], 'M'); i != -1 {
197-
minutes, err := strconv.ParseInt(timeStr[pos:pos+i], 10, 64)
198-
if err != nil {
199-
return 0, err
200-
}
201-
sum += float64(minutes) * 60
202-
pos += i + 1
203-
}
204-
if i := strings.IndexByte(timeStr[pos:], 'S'); i != -1 {
205-
seconds, err := strconv.ParseFloat(timeStr[pos:pos+i], 64)
206-
if err != nil {
207-
return 0, err
208-
}
209-
sum += seconds
197+
if parts[4] != "" {
198+
secs, err := strconv.ParseFloat(strings.TrimRight(parts[4], "S"), 64)
199+
if err != nil {
200+
return 0, fmt.Errorf("Error parsing Seconds: %s", err)
210201
}
202+
total += time.Duration(secs * float64(time.Second))
211203
}
212204

213-
if minus {
214-
sum = -sum
215-
}
216-
return time.Duration(sum * float64(time.Second)), nil
205+
return total, nil
217206
}

mpd/duration_test.go

+32-15
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"testing"
55
"time"
66

7-
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
88
)
99

1010
func TestDuration(t *testing.T) {
@@ -15,28 +15,45 @@ func TestDuration(t *testing.T) {
1515
}
1616
for ins, ex := range in {
1717
timeDur, err := time.ParseDuration(ins)
18-
assert.Equal(t, nil, err)
18+
require.Equal(t, nil, err)
1919
dur := Duration(timeDur)
20-
assert.Equal(t, ex, dur.String())
20+
require.Equal(t, ex, dur.String())
2121
}
2222
}
2323

2424
func TestParseDuration(t *testing.T) {
2525
in := map[string]float64{
26-
"PT0S": 0,
27-
"PT1M": 60,
28-
"PT2H": 7200,
29-
"PT6M16S": 376,
30-
"PT1.97S": 1.97,
31-
"PT1H2M3.456S": 3723.456,
32-
"P1DT2H": (26 * time.Hour).Seconds(),
33-
"PT20M": (20 * time.Minute).Seconds(),
34-
"-P60D": -(60 * 24 * time.Hour).Seconds(),
35-
"PT1M30.5S": (time.Minute + 30*time.Second + 500*time.Millisecond).Seconds(),
26+
"PT0S": 0,
27+
"PT1M": 60,
28+
"PT2H": 7200,
29+
"PT6M16S": 376,
30+
"PT1.97S": 1.97,
31+
"PT1H2M3.456S": 3723.456,
32+
"P1DT2H": (26 * time.Hour).Seconds(),
33+
"PT20M": (20 * time.Minute).Seconds(),
34+
"PT1M30.5S": (time.Minute + 30*time.Second + 500*time.Millisecond).Seconds(),
35+
"PT1004199059S": (1004199059 * time.Second).Seconds(),
3636
}
3737
for ins, ex := range in {
3838
act, err := parseDuration(ins)
39-
assert.NoError(t, err, ins)
40-
assert.Equal(t, ex, act.Seconds(), ins)
39+
require.NoError(t, err, ins)
40+
require.Equal(t, ex, act.Seconds(), ins)
41+
}
42+
}
43+
44+
func TestParseBadDurations(t *testing.T) {
45+
in := map[string]string{
46+
"P20M": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // We don't allow Months (doesn't make sense when converting to duration)
47+
"P20Y": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // We don't allow Years (doesn't make sense when converting to duration)
48+
"P15.5D": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // Only seconds can be expressed as a decimal
49+
"P2H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // "T" must be present to separate days and hours
50+
"2DT1H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // "P" must always be present
51+
"PT2M1H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // Hours must appear before Minutes
52+
"P": `At least one number and designator are required`, // At least one number and designator are required
53+
"-P20H": `Duration cannot be negative`, // Negative duration doesn't make sense
54+
}
55+
for ins, msg := range in {
56+
_, err := parseDuration(ins)
57+
require.EqualError(t, err, msg, "Expected an error for: %s", ins)
4158
}
4259
}

mpd/mpd_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func (s *MPDSuite) TestNewMPDLive() {
7575
MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION),
7676
MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME),
7777
period: &Period{},
78-
Periods: []*Period{&Period{}},
78+
Periods: []*Period{{}},
7979
}
8080
assert.Equal(s.T(), expectedMPD, m)
8181
}
@@ -115,7 +115,7 @@ func (s *MPDSuite) TestNewMPDLiveWithBaseURLInMPD() {
115115
MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION),
116116
MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME),
117117
period: &Period{},
118-
Periods: []*Period{&Period{}},
118+
Periods: []*Period{{}},
119119
BaseURL: VALID_BASE_URL_VIDEO,
120120
}
121121
assert.Equal(s.T(), expectedMPD, m)
@@ -150,7 +150,7 @@ func (s *MPDSuite) TestNewMPDHbbTV() {
150150
MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION),
151151
MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME),
152152
period: &Period{},
153-
Periods: []*Period{&Period{}},
153+
Periods: []*Period{{}},
154154
}
155155
assert.Equal(s.T(), expectedMPD, m)
156156
}
@@ -165,7 +165,7 @@ func (s *MPDSuite) TestNewMPDOnDemand() {
165165
MediaPresentationDuration: Strptr(VALID_MEDIA_PRESENTATION_DURATION),
166166
MinBufferTime: Strptr(VALID_MIN_BUFFER_TIME),
167167
period: &Period{},
168-
Periods: []*Period{&Period{}},
168+
Periods: []*Period{{}},
169169
}
170170
assert.Equal(s.T(), expectedMPD, m)
171171
}

0 commit comments

Comments
 (0)