diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4c8aceb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: go +go: + - "1.10" + - "1.11" + +before_install: + - go get -t -v ./... + +script: + - go test -v -race -coverprofile=coverage.txt -covermode=atomic ./test/... + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..5186d33 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,44 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + digest = "1:ee907e9ead8d8b090f1e2c07967ff1ec7ed20687c2ffd270d1c6c1b71dff5496" + name = "github.com/AlekSi/pointer" + packages = ["."] + pruneopts = "UT" + revision = "08a25bac605b3fcb6cc27f3917b2c2c87451963d" + version = "v1.0.0" + +[[projects]] + digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" + name = "github.com/davecgh/go-spew" + packages = ["spew"] + pruneopts = "UT" + revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" + version = "v1.1.1" + +[[projects]] + digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + pruneopts = "UT" + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83" + name = "github.com/stretchr/testify" + packages = ["assert"] + pruneopts = "UT" + revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" + version = "v1.2.2" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + input-imports = [ + "github.com/AlekSi/pointer", + "github.com/stretchr/testify/assert", + ] + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..7e20d9a --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,38 @@ +# Gopkg.toml example +# +# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + name = "github.com/AlekSi/pointer" + version = "1.0.0" + +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.2.2" + +[prune] + go-tests = true + unused-packages = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4cb7d3b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Tan Quang Ngo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..88e62fe --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +[![Build Status](https://travis-ci.org/quangngotan95/go-m3u8.svg?branch=master)](https://travis-ci.org/quangngotan95/go-m3u8) +[![codecov](https://codecov.io/gh/quangngotan95/go-m3u8/branch/master/graph/badge.svg)](https://codecov.io/gh/quangngotan95/go-m3u8) +# go-m3u8 +Golang package for m3u8 (ported m3u8 gem https://github.com/sethdeckard/m3u8) + +`go-m3u8` provides easy generation and parsing of m3u8 playlists defined in the HTTP Live Streaming (HLS) Internet Draft published by Apple. +* The library completely implements version 20 of the HLS Internet Draft. +* Provides parsing of an m3u8 playlist into an object model from any File, io.Reader or string. +* Provides ability to write playlist to a string via String() +* Distinction between a master and media playlist is handled automatically (single Playlist class). +* Optionally, the library can automatically generate the audio/video codecs string used in the CODEC attribute based on specified H.264, AAC, or MP3 options (such as Profile/Level). + +## Installation +`go get github.com/quangngotan95/go-m3u8` + +## Usage (creating playlists) +Create a master playlist and child playlists for adaptive bitrate streaming: +```go +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/AlekSi/pointer" +) + +playlist := m3u8.NewPlaylist() +``` +Create a new playlist item: +```go +item := &m3u8.PlaylistItem{ + Width: pointer.ToInt(1920), + Height: pointer.ToInt(1080), + Profile: pointer.ToString("high"), + Level: pointer.ToString("4.1"), + AudioCodec: pointer.ToString("aac-lc"), + Bandwidth: 540, + URI: "test.url", +} +playlist.AppendItem(item) +``` +Add alternate audio, camera angles, closed captions and subtitles by creating MediaItem instances and adding them to the Playlist: +```go +item := &m3u8.MediaItem{ + Type: "AUDIO", + GroupID: "audio-lo", + Name: "Francais", + Language: pointer.ToString("fre"), + AssocLanguage: pointer.ToString("spoken"), + AutoSelect: pointer.ToBool(true), + Default: pointer.ToBool(false), + Forced: pointer.ToBool(true), + URI: pointer.ToString("frelo/prog_index.m3u8"), +} +playlist.AppendItem(item) +``` +Create a standard playlist and add MPEG-TS segments via SegmentItem. You can also specify options for this type of playlist, however these options are ignored if playlist becomes a master playlist (anything but segments added): +```go +playlist := &m3u8.Playlist{ + Target: 12, + Sequence: 1, + Version: pointer.ToInt(1), + Cache: pointer.ToBool(false), + Items: []m3u8.Item{ + &m3u8.SegmentItem{ + Duration: 11, + Segment: "test.ts", + }, + }, +} +``` +You can also access the playlist as a string: +```go +var str string +str = playlist.String() +... +fmt.Print(playlist) +``` +Alternatively you can set codecs rather than having it generated automatically: +```go +item := &m3u8.PlaylistItem{ + Width: pointer.ToInt(1920), + Height: pointer.ToInt(1080), + Codecs: pointer.ToString("avc1.66.30,mp4a.40.2"), + Bandwidth: 540, + URI: "test.url", +} +``` + +## Usage (parsing playlists) +Parse from file +```go +playlist, err := m3u8.ReadFile("path/to/file") +``` +Read from string +```go +playlist, err := m3u8.ReadString(string) +``` +Read from generic `io.Reader` +```go +playlist, err := m3u8.Read(reader) +``` + +Access items in playlist: +```go +gore> playlist.Items[0] +(*m3u8.SessionKeyItem)#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" +gore> playlist.Items[1] +(*m3u8.PlaybackStart)#EXT-X-START:TIME-OFFSET=20.2 +``` + +## Misc +Codecs: +* Values for audio_codec (codec name): aac-lc, he-aac, mp3 +* Values for profile (H.264 Profile): baseline, main, high. +* Values for level (H.264 Level): 3.0, 3.1, 4.0, 4.1. + +Not all Levels and Profiles can be combined and validation is not currently implemented, consult H.264 documentation for further details. + +## Contributing +1. Fork it https://github.com/quangngotan95/go-m3u8/fork +2. Create your feature branch `git checkout -b my-new-feature` +3. Run tests `go test ./test/...`, make sure they all pass and new features are covered +4. Commit your changes `git commit -am "Add new features"` +5. Push to the branch `git push origin my-new-feature` +6. Create a new Pull Request + +## License +MIT License - See [LICENSE.txt](https://github.com/quangngotan95/go-m3u8/blob/master/LICENSE) for details \ No newline at end of file diff --git a/m3u8/byteRange.go b/m3u8/byteRange.go new file mode 100644 index 0000000..383256f --- /dev/null +++ b/m3u8/byteRange.go @@ -0,0 +1,46 @@ +package m3u8 + +import ( + "fmt" + "strconv" + "strings" +) + +// ByteRange represents sub range of a resource +type ByteRange struct { + Length *int + Start *int +} + +func NewByteRange(text string) (*ByteRange, error) { + if text == "" { + return nil, nil + } + + values := strings.Split(text, "@") + + lengthValue, err := strconv.Atoi(values[0]) + if err != nil { + return nil, err + } + + br := ByteRange{Length: &lengthValue} + + if len(values) >= 2 { + startValue, err := strconv.Atoi(values[1]) + if err != nil { + return &br, err + } + br.Start = &startValue + } + + return &br, nil +} + +func (br *ByteRange) String() string { + if br.Start == nil { + return fmt.Sprintf("%d", *br.Length) + } + + return fmt.Sprintf("%d@%d", *br.Length, *br.Start) +} diff --git a/m3u8/codecs.go b/m3u8/codecs.go new file mode 100644 index 0000000..a9ed5b7 --- /dev/null +++ b/m3u8/codecs.go @@ -0,0 +1,74 @@ +package m3u8 + +import "strings" + +var ( + AudioCodecMap = map[string]string{ + "aac-lc": "mp4a.40.2", + "he-aac": "mp4a.40.5", + "mp3": "mp4a.40.34", + } + + BaselineCodecMap = map[string]string{ + "3.0": "avc1.66.30", + "3.1": "avc1.42001f", + } + + MainCodecMap = map[string]string{ + "3.0": "avc1.77.30", + "3.1": "avc1.4d001f", + "4.0": "avc1.4d0028", + "4.1": "avc1.4d0029", + } + + HighCodecMap = map[string]string{ + "3.0": "avc1.64001e", + "3.1": "avc1.64001f", + "3.2": "avc1.640020", + "4.0": "avc1.640028", + "4.1": "avc1.640029", + "4.2": "avc1.64002a", + "5.0": "avc1.640032", + "5.1": "avc1.640033", + "5.2": "avc1.640034", + } +) + +func audioCodec(codec *string) *string { + if codec == nil { + return nil + } + + key := strings.ToLower(*codec) + value, ok := AudioCodecMap[key] + + if !ok { + return nil + } + + return &value +} + +func videoCodec(profile *string, level *string) *string { + if profile == nil || level == nil { + return nil + } + + var value string + var ok bool + + switch *profile { + case "baseline": + value, ok = BaselineCodecMap[*level] + case "main": + value, ok = MainCodecMap[*level] + case "high": + value, ok = HighCodecMap[*level] + } + + if !ok { + return nil + } + + return &value +} diff --git a/m3u8/common.go b/m3u8/common.go new file mode 100644 index 0000000..381f2b6 --- /dev/null +++ b/m3u8/common.go @@ -0,0 +1,101 @@ +package m3u8 + +import ( + "regexp" + "strconv" + "strings" +) + +const ( + quotedFormatString = `%s="%v"` + formatString = `%s=%v` + frameRateFormatString = `%s=%.3f` +) + +var ( + parseRegex = regexp.MustCompile(`([A-z0-9-]+)\s*=\s*("[^"]*"|[^,]*)`) +) + +func ParseAttributes(text string) map[string]string { + res := make(map[string]string) + value := strings.Replace(text, "\n", "", -1) + matches := parseRegex.FindAllStringSubmatch(value, -1) + for _, match := range matches { + if len(match) >= 3 { + key := match[1] + value := strings.Replace(match[2], `"`, "", -1) + res[key] = value + } + } + + return res +} + +func parseFloat(attributes map[string]string, key string) (*float64, error) { + stringValue, ok := attributes[key] + if !ok { + return nil, nil + } + + value, err := strconv.ParseFloat(stringValue, 64) + if err != nil { + return nil, err + } + + return &value, nil +} + +func parseInt(attributes map[string]string, key string) (*int, error) { + stringValue, ok := attributes[key] + if !ok { + return nil, nil + } + + int64Value, err := strconv.ParseInt(stringValue, 0, 0) + if err != nil { + return nil, err + } + + value := int(int64Value) + + return &value, nil +} + +func parseYesNo(attributes map[string]string, key string) *bool { + stringValue, ok := attributes[key] + + if !ok { + return nil + } + + val := false + + if stringValue == YesValue { + val = true + } + + return &val +} + +func formatYesNo(value bool) string { + if value { + return YesValue + } + + return NoValue +} + +func attributeExists(key string, attributes map[string]string) bool { + _, ok := attributes[key] + return ok +} + +func pointerTo(attributes map[string]string, key string) *string { + value, ok := attributes[key] + + if !ok { + return nil + } + + return &value +} diff --git a/m3u8/dateRangeItem.go b/m3u8/dateRangeItem.go new file mode 100644 index 0000000..b0a390a --- /dev/null +++ b/m3u8/dateRangeItem.go @@ -0,0 +1,121 @@ +package m3u8 + +import ( + "fmt" + "strconv" + "strings" +) + +// DateRangeItem represents a #EXT-X-DATERANGE tag +type DateRangeItem struct { + ID string + Class *string + StartDate string + EndDate *string + Duration *float64 + PlannedDuration *float64 + Scte35Cmd *string + Scte35Out *string + Scte35In *string + EndOnNext bool + ClientAttributes map[string]string +} + +func NewDateRangeItem(text string) (*DateRangeItem, error) { + attributes := ParseAttributes(text) + duration, err := parseFloat(attributes, DurationTag) + if err != nil { + return nil, err + } + plannedDuartion, err := parseFloat(attributes, PlannedDurationTag) + if err != nil { + return nil, err + } + + return &DateRangeItem{ + ID: attributes[IDTag], + Class: pointerTo(attributes, ClassTag), + StartDate: attributes[StartDateTag], + EndDate: pointerTo(attributes, EndDateTag), + Duration: duration, + PlannedDuration: plannedDuartion, + Scte35Cmd: pointerTo(attributes, Scte35CmdTag), + Scte35Out: pointerTo(attributes, Scte35OutTag), + Scte35In: pointerTo(attributes, Scte35InTag), + EndOnNext: attributeExists(EndOnNextTag, attributes), + ClientAttributes: parseClientAttributes(attributes), + }, nil +} + +func (dri *DateRangeItem) String() string { + var slice []string + + slice = append(slice, fmt.Sprintf(quotedFormatString, IDTag, dri.ID)) + if dri.Class != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, ClassTag, *dri.Class)) + } + slice = append(slice, fmt.Sprintf(quotedFormatString, StartDateTag, dri.StartDate)) + if dri.EndDate != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, EndDateTag, *dri.EndDate)) + } + if dri.Duration != nil { + slice = append(slice, fmt.Sprintf(formatString, DurationTag, *dri.Duration)) + } + if dri.PlannedDuration != nil { + slice = append(slice, fmt.Sprintf(formatString, PlannedDurationTag, *dri.PlannedDuration)) + } + clientAttributes := formatClientAttributes(dri.ClientAttributes) + slice = append(slice, clientAttributes...) + + if dri.Scte35Cmd != nil { + slice = append(slice, fmt.Sprintf(formatString, Scte35CmdTag, *dri.Scte35Cmd)) + } + if dri.Scte35Out != nil { + slice = append(slice, fmt.Sprintf(formatString, Scte35OutTag, *dri.Scte35Out)) + } + if dri.Scte35In != nil { + slice = append(slice, fmt.Sprintf(formatString, Scte35InTag, *dri.Scte35In)) + } + if dri.EndOnNext { + slice = append(slice, fmt.Sprintf(`%s=YES`, EndOnNextTag)) + } + + return fmt.Sprintf("%s:%s", DateRangeItemTag, strings.Join(slice, ",")) +} + +func parseClientAttributes(attributes map[string]string) map[string]string { + result := make(map[string]string) + hasCA := false + + for key, value := range attributes { + if strings.HasPrefix(key, "X-") { + result[key] = value + hasCA = true + } + } + + if hasCA { + return result + } + + return nil +} + +func formatClientAttributes(ca map[string]string) []string { + if ca == nil { + return nil + } + + var slice []string + + for key, value := range ca { + formatString := `%s=%s` + _, err := strconv.ParseFloat(value, 64) + if err != nil { + formatString = `%s="%s"` + } + slice = append(slice, fmt.Sprintf(formatString, key, value)) + } + + return slice +} diff --git a/m3u8/discontinuityItem.go b/m3u8/discontinuityItem.go new file mode 100644 index 0000000..d12d133 --- /dev/null +++ b/m3u8/discontinuityItem.go @@ -0,0 +1,15 @@ +package m3u8 + +import "fmt" + +// DiscontinuityItem represents a EXT-X-DISCONTINUITY tag to indicate a +// discontinuity between the SegmentItems that proceed and follow it. +type DiscontinuityItem struct{} + +func NewDiscontinuityItem() (*DiscontinuityItem, error) { + return &DiscontinuityItem{}, nil +} + +func (di *DiscontinuityItem) String() string { + return fmt.Sprintf("%s\n", DiscontinuityItemTag) +} diff --git a/m3u8/encryptable.go b/m3u8/encryptable.go new file mode 100644 index 0000000..6ce20d0 --- /dev/null +++ b/m3u8/encryptable.go @@ -0,0 +1,44 @@ +package m3u8 + +import ( + "fmt" + "strings" +) + +type Encryptable struct { + Method string + URI *string + IV *string + KeyFormat *string + KeyFormatVersions *string +} + +func NewEncryptable(attributes map[string]string) *Encryptable { + return &Encryptable{ + Method: attributes[MethodTag], + URI: pointerTo(attributes, URITag), + IV: pointerTo(attributes, IVTag), + KeyFormat: pointerTo(attributes, KeyFormatTag), + KeyFormatVersions: pointerTo(attributes, KeyFormatVersionsTag), + } +} + +func (e *Encryptable) String() string { + var slice []string + + slice = append(slice, fmt.Sprintf(formatString, MethodTag, e.Method)) + if e.URI != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, URITag, *e.URI)) + } + if e.IV != nil { + slice = append(slice, fmt.Sprintf(formatString, IVTag, *e.IV)) + } + if e.KeyFormat != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, KeyFormatTag, *e.KeyFormat)) + } + if e.KeyFormatVersions != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, KeyFormatVersionsTag, *e.KeyFormatVersions)) + } + + return strings.Join(slice, ",") +} diff --git a/m3u8/errors.go b/m3u8/errors.go new file mode 100644 index 0000000..ff8d213 --- /dev/null +++ b/m3u8/errors.go @@ -0,0 +1,13 @@ +package m3u8 + +import "errors" + +var ( + ErrPlaylistInvalid = errors.New("invalid playlist, must start with #EXTM3U") + ErrPlaylistInvalidType = errors.New("invalid playlist, mixed master and media") + ErrResolutionInvalid = errors.New("invalid resolution") + ErrBandwidthMissing = errors.New("missing bandwidth") + ErrBandwidthInvalid = errors.New("invalid bandwidth") + ErrSegmentItemInvalid = errors.New("invalid segment item") + ErrPlaylistItemInvalid = errors.New("invalid playlist item") +) diff --git a/m3u8/keyItem.go b/m3u8/keyItem.go new file mode 100644 index 0000000..44285de --- /dev/null +++ b/m3u8/keyItem.go @@ -0,0 +1,19 @@ +package m3u8 + +import "fmt" + +// KeyItem represents a set of EXT-X-KEY attributes +type KeyItem struct { + Encryptable *Encryptable +} + +func NewKeyItem(text string) (*KeyItem, error) { + attributes := ParseAttributes(text) + return &KeyItem{ + Encryptable: NewEncryptable(attributes), + }, nil +} + +func (ki *KeyItem) String() string { + return fmt.Sprintf("%s:%v", KeyItemTag, ki.Encryptable.String()) +} diff --git a/m3u8/mapItem.go b/m3u8/mapItem.go new file mode 100644 index 0000000..84185c5 --- /dev/null +++ b/m3u8/mapItem.go @@ -0,0 +1,32 @@ +package m3u8 + +import "fmt" + +// MapItem represents a EXT-X-MAP tag which specifies how to obtain the Media +// Initialization Section +type MapItem struct { + URI string + ByteRange *ByteRange +} + +func NewMapItem(text string) (*MapItem, error) { + attributes := ParseAttributes(text) + + br, err := NewByteRange(attributes[ByteRangeTag]) + if err != nil { + return nil, err + } + + return &MapItem{ + URI: attributes[URITag], + ByteRange: br, + }, nil +} + +func (mi *MapItem) String() string { + if mi.ByteRange == nil { + return fmt.Sprintf(`%s:%s="%s"`, MapItemTag, URITag, mi.URI) + } + + return fmt.Sprintf(`%s:%s="%s",%s="%v"`, MapItemTag, URITag, mi.URI, ByteRangeTag, mi.ByteRange) +} diff --git a/m3u8/mediaItem.go b/m3u8/mediaItem.go new file mode 100644 index 0000000..6d22aae --- /dev/null +++ b/m3u8/mediaItem.go @@ -0,0 +1,79 @@ +package m3u8 + +import ( + "fmt" + "strings" +) + +// MediaItem represents a set of EXT-X-MEDIA attributes +type MediaItem struct { + Type string + GroupID string + Name string + Language *string + AssocLanguage *string + AutoSelect *bool + Default *bool + Forced *bool + URI *string + InStreamID *string + Characteristics *string + Channels *string +} + +func NewMediaItem(text string) (*MediaItem, error) { + attributes := ParseAttributes(text) + + return &MediaItem{ + Type: attributes[TypeTag], + GroupID: attributes[GroupIDTag], + Name: attributes[NameTag], + Language: pointerTo(attributes, LanguageTag), + AssocLanguage: pointerTo(attributes, AssocLanguageTag), + AutoSelect: parseYesNo(attributes, AutoSelectTag), + Default: parseYesNo(attributes, DefaultTag), + Forced: parseYesNo(attributes, ForcedTag), + URI: pointerTo(attributes, URITag), + InStreamID: pointerTo(attributes, InStreamIDTag), + Characteristics: pointerTo(attributes, CharacteristicsTag), + Channels: pointerTo(attributes, ChannelsTag), + }, nil +} + +func (mi *MediaItem) String() string { + slice := []string{ + fmt.Sprintf(formatString, TypeTag, mi.Type), + fmt.Sprintf(quotedFormatString, GroupIDTag, mi.GroupID), + } + + if mi.Language != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, LanguageTag, *mi.Language)) + } + if mi.AssocLanguage != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, AssocLanguageTag, *mi.AssocLanguage)) + } + slice = append(slice, fmt.Sprintf(quotedFormatString, NameTag, mi.Name)) + if mi.AutoSelect != nil { + slice = append(slice, fmt.Sprintf(formatString, AutoSelectTag, formatYesNo(*mi.AutoSelect))) + } + if mi.Default != nil { + slice = append(slice, fmt.Sprintf(formatString, DefaultTag, formatYesNo(*mi.Default))) + } + if mi.URI != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, URITag, *mi.URI)) + } + if mi.Forced != nil { + slice = append(slice, fmt.Sprintf(formatString, ForcedTag, formatYesNo(*mi.Forced))) + } + if mi.InStreamID != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, InStreamIDTag, *mi.InStreamID)) + } + if mi.Characteristics != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, CharacteristicsTag, *mi.Characteristics)) + } + if mi.Channels != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, ChannelsTag, *mi.Channels)) + } + + return fmt.Sprintf("%s:%s", MediaItemTag, strings.Join(slice, ",")) +} diff --git a/m3u8/playbackStart.go b/m3u8/playbackStart.go new file mode 100644 index 0000000..dda331a --- /dev/null +++ b/m3u8/playbackStart.go @@ -0,0 +1,36 @@ +package m3u8 + +import ( + "fmt" + "strconv" + "strings" +) + +// PlaybackStart represents a #EXT-X-START tag and attributes +type PlaybackStart struct { + TimeOffset float64 + Precise *bool +} + +func NewPlaybackStart(text string) (*PlaybackStart, error) { + attributes := ParseAttributes(text) + + timeOffset, err := strconv.ParseFloat(attributes[TimeOffsetTag], 64) + if err != nil { + return nil, err + } + + return &PlaybackStart{ + TimeOffset: timeOffset, + Precise: parseYesNo(attributes, PreciseTag), + }, nil +} + +func (ps *PlaybackStart) String() string { + slice := []string{fmt.Sprintf(formatString, TimeOffsetTag, ps.TimeOffset)} + if ps.Precise != nil { + slice = append(slice, fmt.Sprintf(formatString, PreciseTag, formatYesNo(*ps.Precise))) + } + + return fmt.Sprintf(`%s:%s`, PlaybackStartTag, strings.Join(slice, ",")) +} diff --git a/m3u8/playlist.go b/m3u8/playlist.go new file mode 100644 index 0000000..c493992 --- /dev/null +++ b/m3u8/playlist.go @@ -0,0 +1,113 @@ +package m3u8 + +type Item interface { + String() string +} + +// Playlist represents an m3u8 playlist, it can be a master playlist or a set +// of media segments +type Playlist struct { + Items []Item + Version *int + Cache *bool + Target int + Sequence int + DiscontinuitySequence *int + Type *string + IFramesOnly bool + IndependentSegments bool + Live bool + Master *bool +} + +func NewPlaylist() *Playlist { + return &Playlist{ + Target: 10, + } +} + +func NewPlaylistWithItems(items []Item) *Playlist { + return &Playlist{ + Target: 10, + Items: items, + } +} + +func (pl *Playlist) String() string { + s, err := Write(pl) + if err != nil { + return "" + } + + return s +} + +func (pl *Playlist) AppendItem(item Item) { + pl.Items = append(pl.Items, item) +} + +func (pl *Playlist) IsLive() bool { + if pl.IsMaster() { + return false + } + + return pl.Live +} + +func (pl *Playlist) IsMaster() bool { + if pl.Master != nil { + return *pl.Master + } + + plSize := pl.PlaylistSize() + smSize := pl.SegmentSize() + if plSize <= 0 && smSize <= 0 { + return false + } + + return plSize > 0 +} + +func (pl *Playlist) PlaylistSize() int { + result := 0 + + for _, item := range pl.Items { + if _, ok := item.(*PlaylistItem); ok { + result++ + } + } + + return result +} + +func (pl *Playlist) SegmentSize() int { + result := 0 + + for _, item := range pl.Items { + if _, ok := (item).(*SegmentItem); ok { + result++ + } + } + + return result +} + +func (pl *Playlist) ItemSize() int { + return len(pl.Items) +} + +func (pl *Playlist) IsValid() bool { + return !(pl.PlaylistSize() > 0 && pl.SegmentSize() > 0) +} + +func (pl *Playlist) Duration() float64 { + duration := 0.0 + + for _, item := range pl.Items { + if segmentItem, ok := item.(*SegmentItem); ok { + duration += segmentItem.Duration + } + } + + return duration +} diff --git a/m3u8/playlistItem.go b/m3u8/playlistItem.go new file mode 100644 index 0000000..4c26096 --- /dev/null +++ b/m3u8/playlistItem.go @@ -0,0 +1,207 @@ +package m3u8 + +import ( + "fmt" + "strconv" + "strings" +) + +// PlaylistItem represents a set of EXT-X-STREAM-INF or +// EXT-X-I-FRAME-STREAM-INF attributes +type PlaylistItem struct { + Bandwidth int + URI string + IFrame bool + + Name *string + Width *int + Height *int + AverageBandwidth *int + ProgramID *string + Codecs *string + AudioCodec *string + Profile *string + Level *string + Video *string + Audio *string + Subtitles *string + ClosedCaptions *string + FrameRate *float64 + HDCPLevel *string + Resolution *Resolution +} + +func NewPlaylistItem(text string) (*PlaylistItem, error) { + attributes := ParseAttributes(text) + + resolution, err := parseResolution(attributes, ResolutionTag) + if err != nil { + return nil, err + } + var width, height *int + if resolution != nil { + width = &resolution.Width + height = &resolution.Height + } + + averageBandwidth, err := parseInt(attributes, AverageBandwidthTag) + if err != nil { + return nil, err + } + + frameRate, err := parseFloat(attributes, FrameRateTag) + if err != nil { + return nil, err + } + if frameRate != nil && *frameRate <= 0 { + frameRate = nil + } + + bandwidth, err := parseBandwidth(attributes, BandwidthTag) + if err != nil { + return nil, err + } + + return &PlaylistItem{ + ProgramID: pointerTo(attributes, ProgramIDTag), + Codecs: pointerTo(attributes, CodecsTag), + Width: width, + Height: height, + Bandwidth: bandwidth, + AverageBandwidth: averageBandwidth, + FrameRate: frameRate, + Video: pointerTo(attributes, VideoTag), + Audio: pointerTo(attributes, AudioTag), + URI: attributes[URITag], + Subtitles: pointerTo(attributes, SubtitlesTag), + ClosedCaptions: pointerTo(attributes, ClosedCaptionsTag), + Name: pointerTo(attributes, NameTag), + HDCPLevel: pointerTo(attributes, HDCPLevelTag), + Resolution: resolution, + }, nil +} + +func (pi *PlaylistItem) String() string { + var slice []string + // Check resolution + if pi.Resolution == nil && pi.Width != nil && pi.Height != nil { + r := &Resolution{ + Width: *pi.Width, + Height: *pi.Height, + } + pi.Resolution = r + } + if pi.ProgramID != nil { + slice = append(slice, fmt.Sprintf(formatString, ProgramIDTag, *pi.ProgramID)) + } + if pi.Resolution != nil { + slice = append(slice, fmt.Sprintf(formatString, ResolutionTag, pi.Resolution.String())) + } + codecs := formatCodecs(pi) + if codecs != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, CodecsTag, *codecs)) + } + slice = append(slice, fmt.Sprintf(formatString, BandwidthTag, pi.Bandwidth)) + if pi.AverageBandwidth != nil { + slice = append(slice, fmt.Sprintf(formatString, AverageBandwidthTag, *pi.AverageBandwidth)) + } + if pi.FrameRate != nil { + slice = append(slice, fmt.Sprintf(frameRateFormatString, FrameRateTag, *pi.FrameRate)) + } + if pi.HDCPLevel != nil { + slice = append(slice, fmt.Sprintf(formatString, HDCPLevelTag, *pi.HDCPLevel)) + } + if pi.Audio != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, AudioTag, *pi.Audio)) + } + if pi.Video != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, VideoTag, *pi.Video)) + } + if pi.Subtitles != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, SubtitlesTag, *pi.Subtitles)) + } + if pi.ClosedCaptions != nil { + cc := *pi.ClosedCaptions + fs := quotedFormatString + if cc == NoneValue { + fs = formatString + } + slice = append(slice, fmt.Sprintf(fs, ClosedCaptionsTag, cc)) + } + if pi.Name != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, NameTag, *pi.Name)) + } + + attributesString := strings.Join(slice, ",") + + if pi.IFrame { + return fmt.Sprintf(`%s:%s,%s="%s"`, PlaylistIframeTag, attributesString, URITag, pi.URI) + } + + return fmt.Sprintf("%s:%s\n%s", PlaylistItemTag, attributesString, pi.URI) +} + +func (pi *PlaylistItem) CodecsString() string { + codecsPtr := formatCodecs(pi) + if codecsPtr == nil { + return "" + } + + return *codecsPtr +} + +func formatCodecs(pi *PlaylistItem) *string { + if pi.Codecs != nil { + return pi.Codecs + } + + videoCodecPtr := videoCodec(pi.Profile, pi.Level) + // profile or level were specified but not recognized any codecs + if !(pi.Profile == nil && pi.Level == nil) && videoCodecPtr == nil { + return nil + } + + audioCodecPtr := audioCodec(pi.AudioCodec) + // audio codec was specified but not recognized + if !(pi.AudioCodec == nil) && audioCodecPtr == nil { + return nil + } + + var slice []string + if videoCodecPtr != nil { + slice = append(slice, *videoCodecPtr) + } + if audioCodecPtr != nil { + slice = append(slice, *audioCodecPtr) + } + + if len(slice) <= 0 { + return nil + } + + value := strings.Join(slice, ",") + return &value +} + +func parseBandwidth(attributes map[string]string, key string) (int, error) { + bw, ok := attributes[key] + if !ok { + return 0, ErrBandwidthMissing + } + + bandwidth, err := strconv.ParseInt(bw, 0, 0) + if err != nil { + return 0, ErrBandwidthInvalid + } + + return int(bandwidth), nil +} + +func parseResolution(attributes map[string]string, key string) (*Resolution, error) { + resolution, ok := attributes[key] + if !ok { + return nil, nil + } + + return NewResolution(resolution) +} diff --git a/m3u8/reader.go b/m3u8/reader.go new file mode 100644 index 0000000..6418568 --- /dev/null +++ b/m3u8/reader.go @@ -0,0 +1,274 @@ +package m3u8 + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "strings" +) + +type state struct { + open bool + currentItem Item + master bool +} + +func ReadString(text string) (*Playlist, error) { + return Read(strings.NewReader(text)) +} + +func ReadFile(path string) (*Playlist, error) { + f, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + return Read(bytes.NewReader(f)) +} + +func Read(reader io.Reader) (*Playlist, error) { + var buf bytes.Buffer + _, err := buf.ReadFrom(reader) + if err != nil { + return nil, err + } + + pl := NewPlaylist() + st := &state{} + eof := false + header := true + + for !eof { + line, err := buf.ReadString('\n') + if err == io.EOF { + eof = true + } else if err != nil { + return nil, err + } + + value := strings.TrimSpace(line) + if header && value != HeaderTag { + return nil, ErrPlaylistInvalid + } + + if err := parseLine(value, pl, st); err != nil { + return nil, err + } + + header = false + } + + return pl, nil +} + +func parseLine(line string, pl *Playlist, st *state) error { + var err error + switch { + // basic tags + case matchTag(line, VersionTag): + pl.Version, err = parseIntPtr(line, VersionTag) + // media segment tags + case matchTag(line, SegmentItemTag): + st.currentItem, err = NewSegmentItem(line) + st.master = false + st.open = true + case matchTag(line, DiscontinuityItemTag): + st.master = false + st.open = false + item, err := NewDiscontinuityItem() + if err != nil { + return parseError(line, err) + } + pl.Items = append(pl.Items, item) + case matchTag(line, ByteRangeItemTag): + value := strings.Replace(line, ByteRangeItemTag+":", "", -1) + value = strings.Replace(value, "\n", "", -1) + br, err := NewByteRange(value) + if err != nil { + return parseError(line, err) + } + mit, ok := st.currentItem.(*MapItem) + if ok { + mit.ByteRange = br + st.currentItem = mit + } else { + sit, ok := st.currentItem.(*SegmentItem) + if ok { + sit.ByteRange = br + st.currentItem = sit + } + } + + case matchTag(line, KeyItemTag): + item, err := NewKeyItem(line) + if err != nil { + return parseError(line, err) + } + pl.Items = append(pl.Items, item) + case matchTag(line, MapItemTag): + item, err := NewMapItem(line) + if err != nil { + return parseError(line, err) + } + pl.Items = append(pl.Items, item) + case matchTag(line, TimeItemTag): + pdt, err := NewTimeItem(line) + if err != nil { + return parseError(line, err) + } + if st.open { + if item, ok := st.currentItem.(*SegmentItem); !ok { + return parseError(line, ErrSegmentItemInvalid) + } else { + item.ProgramDateTime = pdt + } + } else { + pl.Items = append(pl.Items, pdt) + } + case matchTag(line, DateRangeItemTag): + dri, err := NewDateRangeItem(line) + if err != nil { + return parseError(line, err) + } + pl.Items = append(pl.Items, dri) + + // media playlist tags + case matchTag(line, MediaSequenceTag): + pl.Sequence, err = parseIntValue(line, MediaSequenceTag) + case matchTag(line, DiscontinuitySequenceTag): + pl.DiscontinuitySequence, err = parseIntPtr(line, DiscontinuitySequenceTag) + case matchTag(line, CacheTag): + ptr := parseYesNoPtr(line, CacheTag) + pl.Cache = ptr + case matchTag(line, TargetDurationTag): + pl.Target, err = parseIntValue(line, TargetDurationTag) + case matchTag(line, IFramesOnlyTag): + pl.IFramesOnly = true + case matchTag(line, PlaylistTypeTag): + pl.Type = parseStringPtr(line, PlaylistTypeTag) + + // master playlist tags + case matchTag(line, MediaItemTag): + st.open = false + mi, err := NewMediaItem(line) + if err != nil { + return parseError(line, err) + } + pl.Items = append(pl.Items, mi) + case matchTag(line, SessionDataItemTag): + sdi, err := NewSessionDataItem(line) + if err != nil { + return parseError(line, err) + } + pl.Items = append(pl.Items, sdi) + case matchTag(line, SessionKeyItemTag): + ski, err := NewSessionKeyItem(line) + if err != nil { + return parseError(line, err) + } + pl.Items = append(pl.Items, ski) + case matchTag(line, PlaylistItemTag): + st.master = true + st.open = true + pi, err := NewPlaylistItem(line) + if err != nil { + return parseError(line, err) + } + st.currentItem = pi + case matchTag(line, PlaylistIframeTag): + st.master = true + st.open = false + pi, err := NewPlaylistItem(line) + if err != nil { + return parseError(line, err) + } + pi.IFrame = true + pl.Items = append(pl.Items, pi) + st.currentItem = pi + + // universal tags + case matchTag(line, PlaybackStartTag): + ps, err := NewPlaybackStart(line) + if err != nil { + return parseError(line, err) + } + pl.Items = append(pl.Items, ps) + case matchTag(line, IndependentSegmentsTag): + pl.IndependentSegments = true + default: + if st.currentItem != nil && st.open { + return parseNextLine(line, pl, st) + } + } + + return parseError(line, err) +} + +func parseNextLine(line string, pl *Playlist, st *state) error { + value := strings.Replace(line, "\n", "", -1) + value = strings.Replace(value, "\r", "", -1) + if st.master { + // PlaylistItem + it, ok := st.currentItem.(*PlaylistItem) + if !ok { + return parseError(line, ErrPlaylistItemInvalid) + } + it.URI = value + pl.Items = append(pl.Items, it) + } else { + // SegmentItem + it, ok := st.currentItem.(*SegmentItem) + if !ok { + return parseError(line, ErrSegmentItemInvalid) + } + it.Segment = value + pl.Items = append(pl.Items, it) + } + + st.open = false + + return nil +} + +func matchTag(line, tag string) bool { + return strings.HasPrefix(line, tag) && !strings.HasPrefix(line, tag+"-") +} + +func parseIntValue(line string, tag string) (int, error) { + var v int + _, err := fmt.Sscanf(line, tag+":%d", &v) + return v, err +} + +func parseIntPtr(line string, tag string) (*int, error) { + var ptr int + _, err := fmt.Sscanf(line, tag+":%d", &ptr) + return &ptr, err +} + +func parseStringPtr(line string, tag string) *string { + value := strings.Replace(line, tag+":", "", -1) + if value == "" { + return nil + } + return &value +} + +func parseYesNoPtr(line string, tag string) *bool { + value := strings.Replace(line, tag+":", "", -1) + var b bool + if value == YesValue { + b = true + } else { + b = false + } + + return &b +} + +func parseError(line string, err error) error { + if err == nil { + return nil + } + return fmt.Errorf("error: %v when parsing playlist error for line: %s", err, line) +} diff --git a/m3u8/resolution.go b/m3u8/resolution.go new file mode 100644 index 0000000..53aebf6 --- /dev/null +++ b/m3u8/resolution.go @@ -0,0 +1,42 @@ +package m3u8 + +import ( + "fmt" + "strconv" + "strings" +) + +type Resolution struct { + Width int + Height int +} + +func (r *Resolution) String() string { + if r == nil { + return "" + } + + return fmt.Sprintf("%dx%d", r.Width, r.Height) +} + +func NewResolution(text string) (*Resolution, error) { + values := strings.Split(text, "x") + if len(values) <= 1 { + return nil, ErrResolutionInvalid + } + + width, err := strconv.ParseInt(values[0], 0, 0) + if err != nil { + return nil, err + } + + height, err := strconv.ParseInt(values[1], 0, 0) + if err != nil { + return nil, err + } + + return &Resolution{ + Width: int(width), + Height: int(height), + }, nil +} diff --git a/m3u8/segmentItem.go b/m3u8/segmentItem.go new file mode 100644 index 0000000..bcca9d8 --- /dev/null +++ b/m3u8/segmentItem.go @@ -0,0 +1,53 @@ +package m3u8 + +import ( + "fmt" + "strconv" + "strings" +) + +// SegmentItem represents EXTINF attributes with the URI that follows, +// optionally allowing an EXT-X-BYTERANGE tag to be set. +type SegmentItem struct { + Duration float64 + Segment string + Comment *string + ProgramDateTime *TimeItem + ByteRange *ByteRange +} + +func NewSegmentItem(text string) (*SegmentItem, error) { + var si SegmentItem + line := strings.Replace(text, SegmentItemTag+":", "", -1) + line = strings.Replace(line, "\n", "", -1) + values := strings.Split(line, ",") + d, err := strconv.ParseFloat(values[0], 64) + if err != nil { + return nil, err + } + + si.Duration = d + if len(values) > 1 && values[1] != "" { + si.Comment = &values[1] + } + + return &si, nil +} + +func (si *SegmentItem) String() string { + date := "" + if si.ProgramDateTime != nil { + date = fmt.Sprintf("%v\n", si.ProgramDateTime) + } + byteRange := "" + if si.ByteRange != nil { + byteRange = fmt.Sprintf("\n%s:%v", ByteRangeItemTag, si.ByteRange.String()) + } + + comment := "" + if si.Comment != nil { + comment = *si.Comment + } + + return fmt.Sprintf("%s:%v,%s%s\n%s%s", SegmentItemTag, si.Duration, comment, byteRange, date, si.Segment) +} diff --git a/m3u8/sessionDataItem.go b/m3u8/sessionDataItem.go new file mode 100644 index 0000000..2af8809 --- /dev/null +++ b/m3u8/sessionDataItem.go @@ -0,0 +1,41 @@ +package m3u8 + +import ( + "fmt" + "strings" +) + +// SessionDataItem represents a set of EXT-X-SESSION-DATA attributes +type SessionDataItem struct { + DataID string + Value *string + URI *string + Language *string +} + +func NewSessionDataItem(text string) (*SessionDataItem, error) { + attributes := ParseAttributes(text) + + return &SessionDataItem{ + DataID: attributes[DataIDTag], + Value: pointerTo(attributes, ValueTag), + URI: pointerTo(attributes, URITag), + Language: pointerTo(attributes, LanguageTag), + }, nil +} + +func (sdi *SessionDataItem) String() string { + slice := []string{fmt.Sprintf(quotedFormatString, DataIDTag, sdi.DataID)} + + if sdi.Value != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, ValueTag, *sdi.Value)) + } + if sdi.URI != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, URITag, *sdi.URI)) + } + if sdi.Language != nil { + slice = append(slice, fmt.Sprintf(quotedFormatString, LanguageTag, *sdi.Language)) + } + + return fmt.Sprintf(`%s:%s`, SessionDataItemTag, strings.Join(slice, ",")) +} diff --git a/m3u8/sessionKeyItem.go b/m3u8/sessionKeyItem.go new file mode 100644 index 0000000..a464a61 --- /dev/null +++ b/m3u8/sessionKeyItem.go @@ -0,0 +1,19 @@ +package m3u8 + +import "fmt" + +// SessionKeyItem represents a set of EXT-X-SESSION-KEY attributes +type SessionKeyItem struct { + Encryptable *Encryptable +} + +func NewSessionKeyItem(text string) (*SessionKeyItem, error) { + attributes := ParseAttributes(text) + return &SessionKeyItem{ + Encryptable: NewEncryptable(attributes), + }, nil +} + +func (ski *SessionKeyItem) String() string { + return fmt.Sprintf("%s:%v", SessionKeyItemTag, ski.Encryptable.String()) +} diff --git a/m3u8/tags.go b/m3u8/tags.go new file mode 100644 index 0000000..2548487 --- /dev/null +++ b/m3u8/tags.go @@ -0,0 +1,91 @@ +package m3u8 + +const ( + // Item tags + SessionKeyItemTag = `#EXT-X-SESSION-KEY` + KeyItemTag = `#EXT-X-KEY` + DiscontinuityItemTag = `#EXT-X-DISCONTINUITY` + TimeItemTag = `#EXT-X-PROGRAM-DATE-TIME` + DateRangeItemTag = `#EXT-X-DATERANGE` + MapItemTag = `#EXT-X-MAP` + SessionDataItemTag = `#EXT-X-SESSION-DATA` + SegmentItemTag = `#EXTINF` + ByteRangeItemTag = `#EXT-X-BYTERANGE` + PlaybackStartTag = `#EXT-X-START` + MediaItemTag = `#EXT-X-MEDIA` + PlaylistItemTag = `#EXT-X-STREAM-INF` + PlaylistIframeTag = `#EXT-X-I-FRAME-STREAM-INF` + + // Playlist tags + HeaderTag = `#EXTM3U` + FooterTag = `#EXT-X-ENDLIST` + TargetDurationTag = `#EXT-X-TARGETDURATION` + CacheTag = `#EXT-X-ALLOW-CACHE` + DiscontinuitySequenceTag = `#EXT-X-DISCONTINUITY-SEQUENCE` + IndependentSegmentsTag = `#EXT-X-INDEPENDENT-SEGMENTS` + PlaylistTypeTag = `#EXT-X-PLAYLIST-TYPE` + IFramesOnlyTag = `#EXT-X-I-FRAMES-ONLY` + MediaSequenceTag = `#EXT-X-MEDIA-SEQUENCE` + VersionTag = `#EXT-X-VERSION` + + // ByteRange tags + ByteRangeTag = "BYTERANGE" + + // Encryptable tags + MethodTag = "METHOD" + URITag = "URI" + IVTag = "IV" + KeyFormatTag = "KEYFORMAT" + KeyFormatVersionsTag = "KEYFORMATVERSIONS" + + // DateRangeItem tags + IDTag = "ID" + ClassTag = "CLASS" + StartDateTag = "START-DATE" + EndDateTag = "END-DATE" + DurationTag = "DURATION" + PlannedDurationTag = "PLANNED-DURATION" + Scte35CmdTag = "SCTE35-CMD" + Scte35OutTag = "SCTE35-OUT" + Scte35InTag = "SCTE35-IN" + EndOnNextTag = "END-ON-NEXT" + + // PlaybackStart tags + TimeOffsetTag = "TIME-OFFSET" + PreciseTag = "PRECISE" + + // SessionDataItem tags + DataIDTag = "DATA-ID" + ValueTag = "VALUE" + LanguageTag = "LANGUAGE" + + // MediaItem tags + TypeTag = "TYPE" + GroupIDTag = "GROUP-ID" + AssocLanguageTag = "ASSOC-LANGUAGE" + NameTag = "NAME" + AutoSelectTag = "AUTOSELECT" + DefaultTag = "DEFAULT" + ForcedTag = "FORCED" + InStreamIDTag = "INSTREAM-ID" + CharacteristicsTag = "CHARACTERISTICS" + ChannelsTag = "CHANNELS" + + /// PlaylistItem tags + ResolutionTag = "RESOLUTION" + ProgramIDTag = "PROGRAM-ID" + CodecsTag = "CODECS" + BandwidthTag = "BANDWIDTH" + AverageBandwidthTag = "AVERAGE-BANDWIDTH" + FrameRateTag = "FRAME-RATE" + VideoTag = "VIDEO" + AudioTag = "AUDIO" + SubtitlesTag = "SUBTITLES" + ClosedCaptionsTag = "CLOSED-CAPTIONS" + HDCPLevelTag = "HDCP-LEVEL" + + // Values + NoneValue = "NONE" + YesValue = "YES" + NoValue = "NO" +) diff --git a/m3u8/timeItem.go b/m3u8/timeItem.go new file mode 100644 index 0000000..57e87d9 --- /dev/null +++ b/m3u8/timeItem.go @@ -0,0 +1,57 @@ +package m3u8 + +import ( + "fmt" + "strings" + "time" +) + +const ( + dateTimeFormat = time.RFC3339Nano +) + +// TimeItem represents EXT-X-PROGRAM-DATE-TIME +type TimeItem struct { + Time time.Time +} + +func NewTimeItem(text string) (*TimeItem, error) { + timeString := strings.Replace(text, TimeItemTag+":", "", -1) + + t, err := ParseTime(timeString) + + if err != nil { + return nil, err + } + + return &TimeItem{ + Time: t, + }, nil + +} + +func (ti *TimeItem) String() string { + return fmt.Sprintf("%s:%s", TimeItemTag, ti.Time.Format(dateTimeFormat)) +} + +func FormatTime(time time.Time) string { + return time.Format(dateTimeFormat) +} + +func ParseTime(value string) (time.Time, error) { + layouts := []string{ + "2006-01-02T15:04:05.999999999Z0700", + "2006-01-02T15:04:05.999999999Z07:00", + "2006-01-02T15:04:05.999999999Z07", + } + var ( + err error + t time.Time + ) + for _, layout := range layouts { + if t, err = time.Parse(layout, value); err == nil { + return t, nil + } + } + return t, err +} diff --git a/m3u8/writer.go b/m3u8/writer.go new file mode 100644 index 0000000..2432216 --- /dev/null +++ b/m3u8/writer.go @@ -0,0 +1,94 @@ +package m3u8 + +import ( + "fmt" + "strings" +) + +func Write(pl *Playlist) (string, error) { + var sb strings.Builder + + if !pl.IsValid() { + return "", ErrPlaylistInvalidType + } + writeHeader(&sb, pl) + for _, item := range pl.Items { + sb.WriteString(item.String()) + sb.WriteRune('\n') + } + writeFooter(&sb, pl) + + return sb.String(), nil +} + +func writeHeader(sb *strings.Builder, pl *Playlist) { + sb.WriteString(HeaderTag) + sb.WriteRune('\n') + + if pl.IsMaster() { + writeVersionTag(sb, pl.Version) + writeIndependentSegmentsTag(sb, pl.IndependentSegments) + } else { + if pl.Type != nil { + sb.WriteString(fmt.Sprintf("%s:%s", PlaylistTypeTag, *pl.Type)) + sb.WriteRune('\n') + } + writeVersionTag(sb, pl.Version) + writeIndependentSegmentsTag(sb, pl.IndependentSegments) + if pl.IFramesOnly { + sb.WriteString(IFramesOnlyTag) + sb.WriteRune('\n') + } + sb.WriteString(fmt.Sprintf("%s:%v", MediaSequenceTag, pl.Sequence)) + sb.WriteRune('\n') + writeDiscontinuitySequenceTag(sb, pl.DiscontinuitySequence) + writeCacheTag(sb, pl.Cache) + sb.WriteString(fmt.Sprintf("%s:%v", TargetDurationTag, pl.Target)) + sb.WriteRune('\n') + } +} + +func writeFooter(sb *strings.Builder, pl *Playlist) { + if pl.IsLive() || pl.IsMaster() { + return + } + + sb.WriteString(FooterTag) + sb.WriteRune('\n') +} + +func writeVersionTag(sb *strings.Builder, version *int) { + if version == nil { + return + } + + sb.WriteString(fmt.Sprintf("%s:%v", VersionTag, *version)) + sb.WriteRune('\n') +} + +func writeIndependentSegmentsTag(sb *strings.Builder, toWrite bool) { + if !toWrite { + return + } + + sb.WriteString(IndependentSegmentsTag) + sb.WriteRune('\n') +} + +func writeDiscontinuitySequenceTag(sb *strings.Builder, sequence *int) { + if sequence == nil { + return + } + + sb.WriteString(fmt.Sprintf("%s:%v", DiscontinuitySequenceTag, *sequence)) + sb.WriteRune('\n') +} + +func writeCacheTag(sb *strings.Builder, cache *bool) { + if cache == nil { + return + } + + sb.WriteString(fmt.Sprintf("%s:%s", CacheTag, formatYesNo(*cache))) + sb.WriteRune('\n') +} diff --git a/test/byteRange_test.go b/test/byteRange_test.go new file mode 100644 index 0000000..4db6c9f --- /dev/null +++ b/test/byteRange_test.go @@ -0,0 +1,50 @@ +package test + +import ( + "github.com/AlekSi/pointer" + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestByteRange_Parse(t *testing.T) { + text := "4500@600" + br, err := m3u8.NewByteRange(text) + + assert.Nil(t, err) + assert.NotNil(t, br.Length) + assert.NotNil(t, br.Start) + + assert.Equal(t, 4500, *br.Length) + assert.Equal(t, 600, *br.Start) + + assertToString(t, text, br) +} + +func TestByteRange_Parse_2(t *testing.T) { + text := "4500" + br, err := m3u8.NewByteRange(text) + + assert.Nil(t, err) + assert.NotNil(t, br.Length) + assert.Nil(t, br.Start) + + assert.Equal(t, 4500, *br.Length) + + assertToString(t, text, br) +} + +func TestByteRange_New(t *testing.T) { + br := &m3u8.ByteRange{ + Length: pointer.ToInt(4500), + Start: pointer.ToInt(200), + } + assert.Equal(t, "4500@200", br.String()) +} + +func TestByteRange_New_2(t *testing.T) { + br := &m3u8.ByteRange{ + Length: pointer.ToInt(4500), + } + assert.Equal(t, "4500", br.String()) +} diff --git a/test/common.go b/test/common.go new file mode 100644 index 0000000..7c917a6 --- /dev/null +++ b/test/common.go @@ -0,0 +1,41 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func assertNotNilEqual(t *testing.T, expected interface{}, ptr interface{}) { + assert.NotNil(t, ptr) + switch ptr.(type) { + case *string: + s, ok := ptr.(*string) + assert.True(t, ok) + assert.Equal(t, expected, *s) + case *float64: + f, ok := ptr.(*float64) + assert.True(t, ok) + assert.Equal(t, expected, *f) + case *int: + i, ok := ptr.(*int) + assert.True(t, ok) + assert.Equal(t, expected, *i) + case *bool: + b, ok := ptr.(*bool) + assert.True(t, ok) + assert.Equal(t, expected, *b) + default: + t.Fatal("not supported assert type") + } +} + +func assertEqualWithoutNewLine(t *testing.T, expected string, actual string) { + removedNewLine := strings.Replace(expected, "\n", "", -1) + assert.Equal(t, removedNewLine, actual) +} + +func assertToString(t *testing.T, expected string, item m3u8.Item) { + assertEqualWithoutNewLine(t, expected, item.String()) +} diff --git a/test/common_test.go b/test/common_test.go new file mode 100644 index 0000000..fc8ece4 --- /dev/null +++ b/test/common_test.go @@ -0,0 +1,17 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestParseAttributes(t *testing.T) { + line := "TEST-ID=\"Help\",URI=\"http://test\",ID=33\n" + mapAttr := m3u8.ParseAttributes(line) + + assert.NotNil(t, mapAttr) + assert.Equal(t, "Help", mapAttr["TEST-ID"]) + assert.Equal(t, "http://test", mapAttr["URI"]) + assert.Equal(t, "33", mapAttr["ID"]) +} diff --git a/test/dateRangeItem_test.go b/test/dateRangeItem_test.go new file mode 100644 index 0000000..2c3c4a5 --- /dev/null +++ b/test/dateRangeItem_test.go @@ -0,0 +1,73 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDateRangeItem_Parse(t *testing.T) { + line := `#EXT-X-DATERANGE:ID="splice-6FFFFFF0",CLASS="test_class", +START-DATE="2014-03-05T11:15:00Z", +END-DATE="2014-03-05T11:16:00Z",DURATION=60.1, +PLANNED-DURATION=59.993, +SCTE35-CMD=0xFC002F0000000000FF2, +SCTE35-OUT=0xFC002F0000000000FF0, +SCTE35-IN=0xFC002F0000000000FF1, +END-ON-NEXT=YES +` + dri, err := m3u8.NewDateRangeItem(line) + + assert.Nil(t, err) + assert.Equal(t, "splice-6FFFFFF0", dri.ID) + assert.Equal(t, "2014-03-05T11:15:00Z", dri.StartDate) + + assertNotNilEqual(t, "test_class", dri.Class) + assertNotNilEqual(t, "2014-03-05T11:16:00Z", dri.EndDate) + assertNotNilEqual(t, 60.1, dri.Duration) + assertNotNilEqual(t, 59.993, dri.PlannedDuration) + assertNotNilEqual(t, "0xFC002F0000000000FF2", dri.Scte35Cmd) + assertNotNilEqual(t, "0xFC002F0000000000FF0", dri.Scte35Out) + assertNotNilEqual(t, "0xFC002F0000000000FF1", dri.Scte35In) + assert.True(t, dri.EndOnNext) + assert.Nil(t, dri.ClientAttributes) + + assertToString(t, line, dri) +} + +func TestDateRangeItem_Parse_2(t *testing.T) { + line := `#EXT-X-DATERANGE:ID="splice-6FFFFFF0", +START-DATE="2014-03-05T11:15:00Z" +` + dri, err := m3u8.NewDateRangeItem(line) + + assert.Nil(t, err) + assert.Equal(t, "splice-6FFFFFF0", dri.ID) + assert.Equal(t, "2014-03-05T11:15:00Z", dri.StartDate) + + assert.Nil(t, dri.Class) + assert.Nil(t, dri.EndDate) + assert.Nil(t, dri.Duration) + assert.Nil(t, dri.PlannedDuration) + assert.Nil(t, dri.Scte35In) + assert.Nil(t, dri.Scte35Out) + assert.Nil(t, dri.Scte35Cmd) + assert.Nil(t, dri.ClientAttributes) + assert.False(t, dri.EndOnNext) + + assertToString(t, line, dri) +} + +func TestDateRangeItem_Parse_3(t *testing.T) { + line := `#EXT-X-DATERANGE:ID="splice-6FFFFFF0", +START-DATE="2014-03-05T11:15:00Z", +X-CUSTOM-VALUE="test_value" +` + dri, err := m3u8.NewDateRangeItem(line) + + assert.Nil(t, err) + assert.NotNil(t, dri.ClientAttributes) + assert.Equal(t, "test_value", dri.ClientAttributes["X-CUSTOM-VALUE"]) + + assertToString(t, line, dri) +} diff --git a/test/discontinuityItem_test.go b/test/discontinuityItem_test.go new file mode 100644 index 0000000..e7c9755 --- /dev/null +++ b/test/discontinuityItem_test.go @@ -0,0 +1,13 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDiscontinuityItem_Parse(t *testing.T) { + di, err := m3u8.NewDiscontinuityItem() + assert.Nil(t, err) + assert.Equal(t, m3u8.DiscontinuityItemTag+"\n", di.String()) +} diff --git a/test/fixtures/dateRangeScte35.m3u8 b/test/fixtures/dateRangeScte35.m3u8 new file mode 100644 index 0000000..c2655fc --- /dev/null +++ b/test/fixtures/dateRangeScte35.m3u8 @@ -0,0 +1,16 @@ +#EXTM3U +#EXT-X-TARGETDURATION:60 +#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11: +15:00Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF0 +00014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50 +000008700000000 +#EXTINF:20.0, +http://media.example.com/first.ts +#EXTINF:20.0, +http://media.example.com/second.ts +#EXTINF:20.0, +http://media.example.com/third.ts +#EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN= +0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A00 +08029896F50000008700000000 +#EXT-X-ENDLIST diff --git a/test/fixtures/encrypted.m3u8 b/test/fixtures/encrypted.m3u8 new file mode 100644 index 0000000..b91c02c --- /dev/null +++ b/test/fixtures/encrypted.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:7794 +#EXT-X-TARGETDURATION:15 + +#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" + +#EXTINF:2.833, +http://media.example.com/fileSequence52-A.ts +#EXTINF:15.0, +http://media.example.com/fileSequence52-B.ts +#EXTINF:13.333, +http://media.example.com/fileSequence52-C.ts + +#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53" + +#EXTINF:15.0, +http://media.example.com/fileSequence53-A.ts \ No newline at end of file diff --git a/test/fixtures/iframes.m3u8 b/test/fixtures/iframes.m3u8 new file mode 100644 index 0000000..49fb43e --- /dev/null +++ b/test/fixtures/iframes.m3u8 @@ -0,0 +1,12 @@ +#EXTM3U +#EXT-X-VERSION:4 +#EXT-X-I-FRAMES-ONLY +#EXTINF:4.12, +#EXT-X-BYTERANGE:9400@376 +segment1.ts +#EXTINF:3.56, +#EXT-X-BYTERANGE:7144 +segment1.ts +#EXTINF:3.82, +#EXT-X-BYTERANGE:10340@1880 +segment2.ts \ No newline at end of file diff --git a/test/fixtures/mapPlaylist.m3u8 b/test/fixtures/mapPlaylist.m3u8 new file mode 100644 index 0000000..f916b4c --- /dev/null +++ b/test/fixtures/mapPlaylist.m3u8 @@ -0,0 +1,8 @@ +#EXTM3U +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-VERSION:5 +#EXT-X-MEDIA-SEQUENCE:1 +#EXT-X-ALLOW-CACHE:NO +#EXT-X-TARGETDURATION:12 +#EXT-X-MAP:URI="frelo/prog_index.m3u8",BYTERANGE="4500@600" +#EXT-X-ENDLIST \ No newline at end of file diff --git a/test/fixtures/master.m3u8 b/test/fixtures/master.m3u8 new file mode 100644 index 0000000..00c690d --- /dev/null +++ b/test/fixtures/master.m3u8 @@ -0,0 +1,16 @@ +#EXTM3U +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" +#EXT-X-START:TIME-OFFSET=20.2 +#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=5042000 +hls/1080-7mbps/1080-7mbps.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=4853000 +hls/1080/1080.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1280x720,CODECS="avc1.4d001f,mp4a.40.2",BANDWIDTH=2387000 +hls/720/720.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=896x504,CODECS="avc1.4d001f,mp4a.40.2",BANDWIDTH=1365000 +hls/504/504.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=640x360,CODECS="avc1.66.30,mp4a.40.2",BANDWIDTH=861000 +hls/360/360.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.2",BANDWIDTH=6400 +hls/64k/64k.m3u8 diff --git a/test/fixtures/masterIframes.m3u8 b/test/fixtures/masterIframes.m3u8 new file mode 100644 index 0000000..0ed5549 --- /dev/null +++ b/test/fixtures/masterIframes.m3u8 @@ -0,0 +1,12 @@ +#EXTM3U +#EXT-X-STREAM-INF:BANDWIDTH=1280000 +low/audio-video.m3u8 +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=2560000 +mid/audio-video.m3u8 +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=7680000 +hi/audio-video.m3u8 +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" +audio-only.m3u8 diff --git a/test/fixtures/playlist.m3u8 b/test/fixtures/playlist.m3u8 new file mode 100644 index 0000000..566e5aa --- /dev/null +++ b/test/fixtures/playlist.m3u8 @@ -0,0 +1,286 @@ +#EXTM3U +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-VERSION:4 +#EXT-X-MEDIA-SEQUENCE:1 +#EXT-X-DISCONTINUITY-SEQUENCE:8 +#EXT-X-ALLOW-CACHE:NO +#EXT-X-TARGETDURATION:12 +#EXTINF:11.344644, +1080-7mbps00000.ts +#EXT-X-DISCONTINUITY +#EXTINF:11.261233, +1080-7mbps00001.ts +#EXTINF:7.507489, +1080-7mbps00002.ts +#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23Z +#EXTINF:11.261233, +1080-7mbps00003.ts +#EXTINF:11.261233, +1080-7mbps00004.ts +#EXTINF:7.507489, +1080-7mbps00005.ts +#EXTINF:11.261233, +1080-7mbps00006.ts +#EXTINF:11.261233, +1080-7mbps00007.ts +#EXTINF:7.507478, +1080-7mbps00008.ts +#EXTINF:11.261233, +1080-7mbps00009.ts +#EXTINF:11.261233, +1080-7mbps00010.ts +#EXTINF:7.507478, +1080-7mbps00011.ts +#EXTINF:11.261233, +1080-7mbps00012.ts +#EXTINF:11.261233, +1080-7mbps00013.ts +#EXTINF:7.507489, +1080-7mbps00014.ts +#EXTINF:11.261233, +1080-7mbps00015.ts +#EXTINF:11.261233, +1080-7mbps00016.ts +#EXTINF:7.507489, +1080-7mbps00017.ts +#EXTINF:11.261233, +1080-7mbps00018.ts +#EXTINF:11.261233, +1080-7mbps00019.ts +#EXTINF:7.507478, +1080-7mbps00020.ts +#EXTINF:11.261233, +1080-7mbps00021.ts +#EXTINF:11.261233, +1080-7mbps00022.ts +#EXTINF:7.507478, +1080-7mbps00023.ts +#EXTINF:11.261233, +1080-7mbps00024.ts +#EXTINF:11.261233, +1080-7mbps00025.ts +#EXTINF:7.507489, +1080-7mbps00026.ts +#EXTINF:11.261233, +1080-7mbps00027.ts +#EXTINF:11.261233, +1080-7mbps00028.ts +#EXTINF:7.507489, +1080-7mbps00029.ts +#EXTINF:11.261233, +1080-7mbps00030.ts +#EXTINF:11.261233, +1080-7mbps00031.ts +#EXTINF:7.507478, +1080-7mbps00032.ts +#EXTINF:11.261233, +1080-7mbps00033.ts +#EXTINF:11.261233, +1080-7mbps00034.ts +#EXTINF:7.507489, +1080-7mbps00035.ts +#EXTINF:11.261233, +1080-7mbps00036.ts +#EXTINF:11.261222, +1080-7mbps00037.ts +#EXTINF:7.507489, +1080-7mbps00038.ts +#EXTINF:11.261233, +1080-7mbps00039.ts +#EXTINF:11.261233, +1080-7mbps00040.ts +#EXTINF:7.507489, +1080-7mbps00041.ts +#EXTINF:11.261233, +1080-7mbps00042.ts +#EXTINF:11.261233, +1080-7mbps00043.ts +#EXTINF:7.507478, +1080-7mbps00044.ts +#EXTINF:11.261233, +1080-7mbps00045.ts +#EXTINF:11.261233, +1080-7mbps00046.ts +#EXTINF:7.507489, +1080-7mbps00047.ts +#EXTINF:11.261233, +1080-7mbps00048.ts +#EXTINF:11.261222, +1080-7mbps00049.ts +#EXTINF:7.507489, +1080-7mbps00050.ts +#EXTINF:11.261233, +1080-7mbps00051.ts +#EXTINF:11.261233, +1080-7mbps00052.ts +#EXTINF:7.507489, +1080-7mbps00053.ts +#EXTINF:11.261233, +1080-7mbps00054.ts +#EXTINF:11.261233, +1080-7mbps00055.ts +#EXTINF:7.507478, +1080-7mbps00056.ts +#EXTINF:11.261233, +1080-7mbps00057.ts +#EXTINF:11.261233, +1080-7mbps00058.ts +#EXTINF:7.507489, +1080-7mbps00059.ts +#EXTINF:11.261233, +1080-7mbps00060.ts +#EXTINF:11.261222, +1080-7mbps00061.ts +#EXTINF:7.507489, +1080-7mbps00062.ts +#EXTINF:11.261233, +1080-7mbps00063.ts +#EXTINF:11.261233, +1080-7mbps00064.ts +#EXTINF:7.507489, +1080-7mbps00065.ts +#EXTINF:11.261233, +1080-7mbps00066.ts +#EXTINF:11.261233, +1080-7mbps00067.ts +#EXTINF:7.507478, +1080-7mbps00068.ts +#EXTINF:11.261233, +1080-7mbps00069.ts +#EXTINF:11.261233, +1080-7mbps00070.ts +#EXTINF:7.507489, +1080-7mbps00071.ts +#EXTINF:11.261233, +1080-7mbps00072.ts +#EXTINF:11.261233, +1080-7mbps00073.ts +#EXTINF:7.507489, +1080-7mbps00074.ts +#EXTINF:11.261222, +1080-7mbps00075.ts +#EXTINF:11.261233, +1080-7mbps00076.ts +#EXTINF:7.507489, +1080-7mbps00077.ts +#EXTINF:11.261233, +1080-7mbps00078.ts +#EXTINF:11.261233, +1080-7mbps00079.ts +#EXTINF:7.507478, +1080-7mbps00080.ts +#EXTINF:11.261233, +1080-7mbps00081.ts +#EXTINF:11.261233, +1080-7mbps00082.ts +#EXTINF:7.507489, +1080-7mbps00083.ts +#EXTINF:11.261233, +1080-7mbps00084.ts +#EXTINF:11.261233, +1080-7mbps00085.ts +#EXTINF:7.507489, +1080-7mbps00086.ts +#EXTINF:11.261222, +1080-7mbps00087.ts +#EXTINF:11.261233, +1080-7mbps00088.ts +#EXTINF:7.507489, +1080-7mbps00089.ts +#EXTINF:11.261233, +1080-7mbps00090.ts +#EXTINF:11.261233, +1080-7mbps00091.ts +#EXTINF:7.507478, +1080-7mbps00092.ts +#EXTINF:11.261233, +1080-7mbps00093.ts +#EXTINF:11.261233, +1080-7mbps00094.ts +#EXTINF:7.507489, +1080-7mbps00095.ts +#EXTINF:11.261233, +1080-7mbps00096.ts +#EXTINF:11.261233, +1080-7mbps00097.ts +#EXTINF:7.507489, +1080-7mbps00098.ts +#EXTINF:11.261222, +1080-7mbps00099.ts +#EXTINF:11.261233, +1080-7mbps00100.ts +#EXTINF:7.507489, +1080-7mbps00101.ts +#EXTINF:11.261233, +1080-7mbps00102.ts +#EXTINF:11.261233, +1080-7mbps00103.ts +#EXTINF:7.507478, +1080-7mbps00104.ts +#EXTINF:11.261233, +1080-7mbps00105.ts +#EXTINF:11.261233, +1080-7mbps00106.ts +#EXTINF:7.507489, +1080-7mbps00107.ts +#EXTINF:11.261233, +1080-7mbps00108.ts +#EXTINF:11.261233, +1080-7mbps00109.ts +#EXTINF:7.507489, +1080-7mbps00110.ts +#EXTINF:11.261233, +1080-7mbps00111.ts +#EXTINF:11.261233, +1080-7mbps00112.ts +#EXTINF:7.507478, +1080-7mbps00113.ts +#EXTINF:11.261233, +1080-7mbps00114.ts +#EXTINF:11.261233, +1080-7mbps00115.ts +#EXTINF:7.507478, +1080-7mbps00116.ts +#EXTINF:11.261233, +1080-7mbps00117.ts +#EXTINF:7.507478, +1080-7mbps00118.ts +#EXTINF:11.261233, +1080-7mbps00119.ts +#EXTINF:11.261233, +1080-7mbps00120.ts +#EXTINF:7.507489, +1080-7mbps00121.ts +#EXTINF:11.261233, +1080-7mbps00122.ts +#EXTINF:11.261233, +1080-7mbps00123.ts +#EXTINF:7.507489, +1080-7mbps00124.ts +#EXTINF:11.261222, +1080-7mbps00125.ts +#EXTINF:11.261233, +1080-7mbps00126.ts +#EXTINF:7.507489, +1080-7mbps00127.ts +#EXTINF:11.261233, +1080-7mbps00128.ts +#EXTINF:11.261233, +1080-7mbps00129.ts +#EXTINF:7.507478, +1080-7mbps00130.ts +#EXTINF:11.261233, +1080-7mbps00131.ts +#EXTINF:11.261233, +1080-7mbps00132.ts +#EXTINF:7.507489, +1080-7mbps00133.ts +#EXTINF:11.261233, +1080-7mbps00134.ts +#EXTINF:11.261233, +1080-7mbps00135.ts +#EXTINF:7.507489, +1080-7mbps00136.ts +#EXTINF:1.793444, +1080-7mbps00137.ts +#EXT-X-ENDLIST diff --git a/test/fixtures/playlistWithComments.m3u8 b/test/fixtures/playlistWithComments.m3u8 new file mode 100644 index 0000000..839c681 --- /dev/null +++ b/test/fixtures/playlistWithComments.m3u8 @@ -0,0 +1,284 @@ +#EXTM3U +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-VERSION:4 +#EXT-X-MEDIA-SEQUENCE:1 +#EXT-X-ALLOW-CACHE:NO +#EXT-X-TARGETDURATION:12 +#EXTINF:11.344644,anything +1080-7mbps00000.ts +#EXT-X-DISCONTINUITY +#EXTINF:11.261233,anything +1080-7mbps00001.ts +#EXTINF:7.507489,anything +1080-7mbps00002.ts +#EXTINF:11.261233,anything +1080-7mbps00003.ts +#EXTINF:11.261233,anything +1080-7mbps00004.ts +#EXTINF:7.507489,anything +1080-7mbps00005.ts +#EXTINF:11.261233,anything +1080-7mbps00006.ts +#EXTINF:11.261233,anything +1080-7mbps00007.ts +#EXTINF:7.507478,anything +1080-7mbps00008.ts +#EXTINF:11.261233,anything +1080-7mbps00009.ts +#EXTINF:11.261233,anything +1080-7mbps00010.ts +#EXTINF:7.507478,anything +1080-7mbps00011.ts +#EXTINF:11.261233,anything +1080-7mbps00012.ts +#EXTINF:11.261233,anything +1080-7mbps00013.ts +#EXTINF:7.507489,anything +1080-7mbps00014.ts +#EXTINF:11.261233,anything +1080-7mbps00015.ts +#EXTINF:11.261233,anything +1080-7mbps00016.ts +#EXTINF:7.507489,anything +1080-7mbps00017.ts +#EXTINF:11.261233,anything +1080-7mbps00018.ts +#EXTINF:11.261233,anything +1080-7mbps00019.ts +#EXTINF:7.507478,anything +1080-7mbps00020.ts +#EXTINF:11.261233,anything +1080-7mbps00021.ts +#EXTINF:11.261233,anything +1080-7mbps00022.ts +#EXTINF:7.507478,anything +1080-7mbps00023.ts +#EXTINF:11.261233,anything +1080-7mbps00024.ts +#EXTINF:11.261233,anything +1080-7mbps00025.ts +#EXTINF:7.507489,anything +1080-7mbps00026.ts +#EXTINF:11.261233,anything +1080-7mbps00027.ts +#EXTINF:11.261233,anything +1080-7mbps00028.ts +#EXTINF:7.507489,anything +1080-7mbps00029.ts +#EXTINF:11.261233,anything +1080-7mbps00030.ts +#EXTINF:11.261233,anything +1080-7mbps00031.ts +#EXTINF:7.507478,anything +1080-7mbps00032.ts +#EXTINF:11.261233,anything +1080-7mbps00033.ts +#EXTINF:11.261233,anything +1080-7mbps00034.ts +#EXTINF:7.507489,anything +1080-7mbps00035.ts +#EXTINF:11.261233,anything +1080-7mbps00036.ts +#EXTINF:11.261222,anything +1080-7mbps00037.ts +#EXTINF:7.507489,anything +1080-7mbps00038.ts +#EXTINF:11.261233,anything +1080-7mbps00039.ts +#EXTINF:11.261233,anything +1080-7mbps00040.ts +#EXTINF:7.507489,anything +1080-7mbps00041.ts +#EXTINF:11.261233,anything +1080-7mbps00042.ts +#EXTINF:11.261233,anything +1080-7mbps00043.ts +#EXTINF:7.507478,anything +1080-7mbps00044.ts +#EXTINF:11.261233,anything +1080-7mbps00045.ts +#EXTINF:11.261233,anything +1080-7mbps00046.ts +#EXTINF:7.507489,anything +1080-7mbps00047.ts +#EXTINF:11.261233,anything +1080-7mbps00048.ts +#EXTINF:11.261222,anything +1080-7mbps00049.ts +#EXTINF:7.507489,anything +1080-7mbps00050.ts +#EXTINF:11.261233,anything +1080-7mbps00051.ts +#EXTINF:11.261233,anything +1080-7mbps00052.ts +#EXTINF:7.507489,anything +1080-7mbps00053.ts +#EXTINF:11.261233,anything +1080-7mbps00054.ts +#EXTINF:11.261233,anything +1080-7mbps00055.ts +#EXTINF:7.507478,anything +1080-7mbps00056.ts +#EXTINF:11.261233,anything +1080-7mbps00057.ts +#EXTINF:11.261233,anything +1080-7mbps00058.ts +#EXTINF:7.507489,anything +1080-7mbps00059.ts +#EXTINF:11.261233,anything +1080-7mbps00060.ts +#EXTINF:11.261222,anything +1080-7mbps00061.ts +#EXTINF:7.507489,anything +1080-7mbps00062.ts +#EXTINF:11.261233,anything +1080-7mbps00063.ts +#EXTINF:11.261233,anything +1080-7mbps00064.ts +#EXTINF:7.507489,anything +1080-7mbps00065.ts +#EXTINF:11.261233,anything +1080-7mbps00066.ts +#EXTINF:11.261233,anything +1080-7mbps00067.ts +#EXTINF:7.507478,anything +1080-7mbps00068.ts +#EXTINF:11.261233,anything +1080-7mbps00069.ts +#EXTINF:11.261233,anything +1080-7mbps00070.ts +#EXTINF:7.507489,anything +1080-7mbps00071.ts +#EXTINF:11.261233,anything +1080-7mbps00072.ts +#EXTINF:11.261233,anything +1080-7mbps00073.ts +#EXTINF:7.507489,anything +1080-7mbps00074.ts +#EXTINF:11.261222,anything +1080-7mbps00075.ts +#EXTINF:11.261233,anything +1080-7mbps00076.ts +#EXTINF:7.507489,anything +1080-7mbps00077.ts +#EXTINF:11.261233,anything +1080-7mbps00078.ts +#EXTINF:11.261233,anything +1080-7mbps00079.ts +#EXTINF:7.507478,anything +1080-7mbps00080.ts +#EXTINF:11.261233,anything +1080-7mbps00081.ts +#EXTINF:11.261233,anything +1080-7mbps00082.ts +#EXTINF:7.507489,anything +1080-7mbps00083.ts +#EXTINF:11.261233,anything +1080-7mbps00084.ts +#EXTINF:11.261233,anything +1080-7mbps00085.ts +#EXTINF:7.507489,anything +1080-7mbps00086.ts +#EXTINF:11.261222,anything +1080-7mbps00087.ts +#EXTINF:11.261233,anything +1080-7mbps00088.ts +#EXTINF:7.507489,anything +1080-7mbps00089.ts +#EXTINF:11.261233,anything +1080-7mbps00090.ts +#EXTINF:11.261233,anything +1080-7mbps00091.ts +#EXTINF:7.507478,anything +1080-7mbps00092.ts +#EXTINF:11.261233,anything +1080-7mbps00093.ts +#EXTINF:11.261233,anything +1080-7mbps00094.ts +#EXTINF:7.507489,anything +1080-7mbps00095.ts +#EXTINF:11.261233,anything +1080-7mbps00096.ts +#EXTINF:11.261233,anything +1080-7mbps00097.ts +#EXTINF:7.507489,anything +1080-7mbps00098.ts +#EXTINF:11.261222,anything +1080-7mbps00099.ts +#EXTINF:11.261233,anything +1080-7mbps00100.ts +#EXTINF:7.507489,anything +1080-7mbps00101.ts +#EXTINF:11.261233,anything +1080-7mbps00102.ts +#EXTINF:11.261233,anything +1080-7mbps00103.ts +#EXTINF:7.507478,anything +1080-7mbps00104.ts +#EXTINF:11.261233,anything +1080-7mbps00105.ts +#EXTINF:11.261233,anything +1080-7mbps00106.ts +#EXTINF:7.507489,anything +1080-7mbps00107.ts +#EXTINF:11.261233,anything +1080-7mbps00108.ts +#EXTINF:11.261233,anything +1080-7mbps00109.ts +#EXTINF:7.507489,anything +1080-7mbps00110.ts +#EXTINF:11.261233,anything +1080-7mbps00111.ts +#EXTINF:11.261233,anything +1080-7mbps00112.ts +#EXTINF:7.507478,anything +1080-7mbps00113.ts +#EXTINF:11.261233,anything +1080-7mbps00114.ts +#EXTINF:11.261233,anything +1080-7mbps00115.ts +#EXTINF:7.507478,anything +1080-7mbps00116.ts +#EXTINF:11.261233,anything +1080-7mbps00117.ts +#EXTINF:7.507478,anything +1080-7mbps00118.ts +#EXTINF:11.261233,anything +1080-7mbps00119.ts +#EXTINF:11.261233,anything +1080-7mbps00120.ts +#EXTINF:7.507489,anything +1080-7mbps00121.ts +#EXTINF:11.261233,anything +1080-7mbps00122.ts +#EXTINF:11.261233,anything +1080-7mbps00123.ts +#EXTINF:7.507489,anything +1080-7mbps00124.ts +#EXTINF:11.261222,anything +1080-7mbps00125.ts +#EXTINF:11.261233,anything +1080-7mbps00126.ts +#EXTINF:7.507489,anything +1080-7mbps00127.ts +#EXTINF:11.261233,anything +1080-7mbps00128.ts +#EXTINF:11.261233,anything +1080-7mbps00129.ts +#EXTINF:7.507478,anything +1080-7mbps00130.ts +#EXTINF:11.261233,anything +1080-7mbps00131.ts +#EXTINF:11.261233,anything +1080-7mbps00132.ts +#EXTINF:7.507489,anything +1080-7mbps00133.ts +#EXTINF:11.261233,anything +1080-7mbps00134.ts +#EXTINF:11.261233,anything +1080-7mbps00135.ts +#EXTINF:7.507489,anything +1080-7mbps00136.ts +#EXTINF:1.793444,anything +1080-7mbps00137.ts +#EXT-X-ENDLIST diff --git a/test/fixtures/sessionData.m3u8 b/test/fixtures/sessionData.m3u8 new file mode 100644 index 0000000..8235de3 --- /dev/null +++ b/test/fixtures/sessionData.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-SESSION-DATA:DATA-ID="com.example.lyrics",URI="lyrics.json" + +#EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" +#EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="sp",VALUE="Este es un ejemplo" diff --git a/test/fixtures/timestampPlaylist.m3u8 b/test/fixtures/timestampPlaylist.m3u8 new file mode 100644 index 0000000..7c246b4 --- /dev/null +++ b/test/fixtures/timestampPlaylist.m3u8 @@ -0,0 +1,22 @@ +#EXTM3U +#EXT-X-VERSION:2 +#EXT-X-TARGETDURATION:10 +#EXT-X-MEDIA-SEQUENCE:1736515 +#EXTINF:10, no desc +#EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:24:31Z +20160408T084506-01-1736515.ts +#EXTINF:10, no desc +#EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:24:41Z +20160408T084506-01-1736516.ts +#EXTINF:10, no desc +#EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:24:51Z +20160408T084506-01-1736517.ts +#EXTINF:10, no desc +#EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:25:01Z +20160408T084506-01-1736518.ts +#EXTINF:10, no desc +#EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:25:11Z +20160408T084506-01-1736519.ts +#EXTINF:10, no desc +#EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:25:21Z +20160408T084506-01-1736520.ts diff --git a/test/fixtures/variantAngles.m3u8 b/test/fixtures/variantAngles.m3u8 new file mode 100644 index 0000000..5e03712 --- /dev/null +++ b/test/fixtures/variantAngles.m3u8 @@ -0,0 +1,14 @@ +#EXTM3U +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/200kbs/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/200kbs/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/500kbs/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/500kbs/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Subtiles",AUTOSELECT=NO,DEFAULT=NO,URI="titles_file" +#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="captions",NAME="Closed Captions",AUTOSELECT=NO,DEFAULT=NO,URI="captions_file" +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="200kbs",AUDIO="aac",AVERAGE-BANDWIDTH=300001,SUBTITLES="subs",CLOSED-CAPTIONS="captions" +Angle1/200kbs/prog_index.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=754857,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="500kbs",AUDIO="aac" +Angle1/500kbs/prog_index.m3u8 diff --git a/test/fixtures/variantAudio.m3u8 b/test/fixtures/variantAudio.m3u8 new file mode 100644 index 0000000..fe9b600 --- /dev/null +++ b/test/fixtures/variantAudio.m3u8 @@ -0,0 +1,15 @@ +#EXTM3U +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="eng",ASSOC-LANGUAGE="spoken",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="englo/prog_index.m3u8",FORCED=YES +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES,DEFAULT=NO,URI="frelo/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES,DEFAULT=NO,URI="splo/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES,DEFAULT=NO,URI="fre/prog_index.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES,DEFAULT=NO,URI="sp/prog_index.m3u8" +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="mp4a.40.5",AUDIO="audio-lo" +lo/prog_index.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=260000,CODECS="avc1.42e01e,mp4a.40.2",AUDIO="audio-lo" +hi/prog_index.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="mp4a.40.2, avc1.64001e",AUDIO="audio-hi" +lo/prog_index.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=650000,CODECS="avc1.42e01e,mp4a.40.2",AUDIO="audio-hi" +hi/prog_index.m3u8 diff --git a/test/keyItem_test.go b/test/keyItem_test.go new file mode 100644 index 0000000..20ee8ec --- /dev/null +++ b/test/keyItem_test.go @@ -0,0 +1,22 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestKeyItem_Parse(t *testing.T) { + line := `#EXT-X-KEY:METHOD=AES-128,URI="http://test.key",IV=D512BBF,KEYFORMAT="identity",KEYFORMATVERSIONS="1/3"` + + ki, err := m3u8.NewKeyItem(line) + assert.Nil(t, err) + assert.NotNil(t, ki.Encryptable) + assert.Equal(t, "AES-128", ki.Encryptable.Method) + assertNotNilEqual(t, "http://test.key", ki.Encryptable.URI) + assertNotNilEqual(t, "D512BBF", ki.Encryptable.IV) + assertNotNilEqual(t, "identity", ki.Encryptable.KeyFormat) + assertNotNilEqual(t, "1/3", ki.Encryptable.KeyFormatVersions) + + assertToString(t, line, ki) +} diff --git a/test/mapItem_test.go b/test/mapItem_test.go new file mode 100644 index 0000000..b03935b --- /dev/null +++ b/test/mapItem_test.go @@ -0,0 +1,31 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestMapItem_Parse(t *testing.T) { + line := `#EXT-X-MAP:URI="frelo/prog_index.m3u8",BYTERANGE="3500@300"` + + mi, err := m3u8.NewMapItem(line) + assert.Nil(t, err) + assert.Equal(t, "frelo/prog_index.m3u8", mi.URI) + assert.NotNil(t, mi.ByteRange) + assertNotNilEqual(t, 3500, mi.ByteRange.Length) + assertNotNilEqual(t, 300, mi.ByteRange.Start) + + assertToString(t, line, mi) +} + +func TestMapItem_Parse_2(t *testing.T) { + line := `#EXT-X-MAP:URI="frelo/prog_index.m3u8"` + + mi, err := m3u8.NewMapItem(line) + assert.Nil(t, err) + assert.Equal(t, "frelo/prog_index.m3u8", mi.URI) + assert.Nil(t, mi.ByteRange) + + assertToString(t, line, mi) +} diff --git a/test/mediaItem_test.go b/test/mediaItem_test.go new file mode 100644 index 0000000..0897b3a --- /dev/null +++ b/test/mediaItem_test.go @@ -0,0 +1,35 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestMediaItem_Parse(t *testing.T) { + line := `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre", +ASSOC-LANGUAGE="spoken",NAME="Francais",AUTOSELECT=YES, +INSTREAM-ID="SERVICE3",CHARACTERISTICS="public.html", +CHANNELS="6", +"DEFAULT=NO,URI="frelo/prog_index.m3u8",FORCED=YES +"` + + mi, err := m3u8.NewMediaItem(line) + assert.Nil(t, err) + assert.Equal(t, "AUDIO", mi.Type) + assert.Equal(t, "audio-lo", mi.GroupID) + assert.Equal(t, "Francais", mi.Name) + + assertNotNilEqual(t, "fre", mi.Language) + assertNotNilEqual(t, "spoken", mi.AssocLanguage) + assertNotNilEqual(t, true, mi.AutoSelect) + assertNotNilEqual(t, false, mi.Default) + assertNotNilEqual(t, "frelo/prog_index.m3u8", mi.URI) + assertNotNilEqual(t, true, mi.Forced) + assertNotNilEqual(t, "SERVICE3", mi.InStreamID) + assertNotNilEqual(t, "public.html", mi.Characteristics) + assertNotNilEqual(t, "6", mi.Channels) + + expected := "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio-lo\",LANGUAGE=\"fre\",ASSOC-LANGUAGE=\"spoken\",NAME=\"Francais\",AUTOSELECT=YES,DEFAULT=NO,URI=\"frelo/prog_index.m3u8\",FORCED=YES,INSTREAM-ID=\"SERVICE3\",CHARACTERISTICS=\"public.html\",CHANNELS=\"6\"" + assertToString(t, expected, mi) +} diff --git a/test/playbackStart_test.go b/test/playbackStart_test.go new file mode 100644 index 0000000..3f3e83f --- /dev/null +++ b/test/playbackStart_test.go @@ -0,0 +1,29 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestPlaybackStart_Parse(t *testing.T) { + line := `#EXT-X-START:TIME-OFFSET=20.2,PRECISE=YES` + + ps, err := m3u8.NewPlaybackStart(line) + assert.Nil(t, err) + assert.Equal(t, 20.2, ps.TimeOffset) + assertNotNilEqual(t, true, ps.Precise) + + assertToString(t, line, ps) +} + +func TestPlaybackStart_Parse_2(t *testing.T) { + line := `#EXT-X-START:TIME-OFFSET=-12.9` + + ps, err := m3u8.NewPlaybackStart(line) + assert.Nil(t, err) + assert.Equal(t, -12.9, ps.TimeOffset) + assert.Nil(t, ps.Precise) + + assertToString(t, line, ps) +} diff --git a/test/playlistItem_test.go b/test/playlistItem_test.go new file mode 100644 index 0000000..2133693 --- /dev/null +++ b/test/playlistItem_test.go @@ -0,0 +1,218 @@ +package test + +import ( + "github.com/AlekSi/pointer" + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestPlaylistItem_Parse(t *testing.T) { + line := `#EXT-X-STREAM-INF:CODECS="avc",BANDWIDTH=540, +PROGRAM-ID=1,RESOLUTION=1920x1080,FRAME-RATE=23.976, +AVERAGE-BANDWIDTH=550,AUDIO="test",VIDEO="test2", +SUBTITLES="subs",CLOSED-CAPTIONS="caps",URI="test.url", +NAME="1080p",HDCP-LEVEL=TYPE-0` + + pi, err := m3u8.NewPlaylistItem(line) + assert.Nil(t, err) + assertNotNilEqual(t, "1", pi.ProgramID) + assertNotNilEqual(t, "avc", pi.Codecs) + assert.Equal(t, 540, pi.Bandwidth) + assertNotNilEqual(t, 550, pi.AverageBandwidth) + assertNotNilEqual(t, 1920, pi.Width) + assertNotNilEqual(t, 1080, pi.Height) + assertNotNilEqual(t, 23.976, pi.FrameRate) + assertNotNilEqual(t, "test", pi.Audio) + assertNotNilEqual(t, "test2", pi.Video) + assertNotNilEqual(t, "subs", pi.Subtitles) + assertNotNilEqual(t, "caps", pi.ClosedCaptions) + assert.Equal(t, "test.url", pi.URI) + assertNotNilEqual(t, "1080p", pi.Name) + assert.False(t, pi.IFrame) + assertNotNilEqual(t, "TYPE-0", pi.HDCPLevel) +} + +func TestPlaylistItem_ToString(t *testing.T) { + // No codecs specified + p := &m3u8.PlaylistItem{ + Bandwidth: 540, + URI: "test.url", + } + assert.NotContains(t, p.String(), "CODECS") + + // Level not recognized + p = &m3u8.PlaylistItem{ + Bandwidth: 540, + URI: "test.url", + Level: pointer.ToString("9001"), + } + assert.NotContains(t, p.String(), "CODECS") + + // Audio codec recognized but profile not recognized + p = &m3u8.PlaylistItem{ + Bandwidth: 540, + URI: "test.url", + Profile: pointer.ToString("best"), + Level: pointer.ToString("9001"), + AudioCodec: pointer.ToString("aac-lc"), + } + assert.NotContains(t, p.String(), "CODECS") + + // Profile and level not set, Audio codec recognized + p = &m3u8.PlaylistItem{ + Bandwidth: 540, + URI: "test.url", + AudioCodec: pointer.ToString("aac-lc"), + } + assert.Contains(t, p.String(), "CODECS") + + // Profile and level recognized, audio codec not recognized + p = &m3u8.PlaylistItem{ + Bandwidth: 540, + URI: "test.url", + Profile: pointer.ToString("high"), + Level: pointer.ToString("4.1"), + AudioCodec: pointer.ToString("fuzzy"), + } + assert.NotContains(t, p.String(), "CODECS") + + // Audio codec not set + p = &m3u8.PlaylistItem{ + Bandwidth: 540, + URI: "test.url", + Profile: pointer.ToString("high"), + Level: pointer.ToString("4.1"), + } + assert.Contains(t, p.String(), `CODECS="avc1.640029"`) + + // Audio codec recognized + p = &m3u8.PlaylistItem{ + Bandwidth: 540, + URI: "test.url", + Profile: pointer.ToString("high"), + Level: pointer.ToString("4.1"), + AudioCodec: pointer.ToString("aac-lc"), + } + assert.Contains(t, p.String(), `CODECS="avc1.640029,mp4a.40.2"`) +} + +func TestPlaylistItem_ToString_2(t *testing.T) { + // All fields set + p := &m3u8.PlaylistItem{ + Codecs: pointer.ToString("avc"), + Bandwidth: 540, + URI: "test.url", + Audio: pointer.ToString("test"), + Video: pointer.ToString("test2"), + AverageBandwidth: pointer.ToInt(500), + Subtitles: pointer.ToString("subs"), + FrameRate: pointer.ToFloat64(30), + ClosedCaptions: pointer.ToString("caps"), + Name: pointer.ToString("SD"), + HDCPLevel: pointer.ToString("TYPE-0"), + ProgramID: pointer.ToString("1"), + } + + expected := `#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="avc",BANDWIDTH=540,AVERAGE-BANDWIDTH=500,FRAME-RATE=30.000,HDCP-LEVEL=TYPE-0,AUDIO="test",VIDEO="test2",SUBTITLES="subs",CLOSED-CAPTIONS="caps",NAME="SD" +test.url` + assert.Equal(t, expected, p.String()) + + // Closed captions is NONE + p = &m3u8.PlaylistItem{ + ProgramID: pointer.ToString("1"), + Width: pointer.ToInt(1920), + Height: pointer.ToInt(1080), + Codecs: pointer.ToString("avc"), + Bandwidth: 540, + URI: "test.url", + ClosedCaptions: pointer.ToString("NONE"), + } + + expected = `#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc",BANDWIDTH=540,CLOSED-CAPTIONS=NONE +test.url` + assert.Equal(t, expected, p.String()) + + // IFrame is true + p = &m3u8.PlaylistItem{ + Codecs: pointer.ToString("avc"), + Bandwidth: 540, + URI: "test.url", + IFrame: true, + Video: pointer.ToString("test2"), + AverageBandwidth: pointer.ToInt(550), + } + + expected = `#EXT-X-I-FRAME-STREAM-INF:CODECS="avc",BANDWIDTH=540,AVERAGE-BANDWIDTH=550,VIDEO="test2",URI="test.url"` + assert.Equal(t, expected, p.String()) +} + +func TestPlaylistItem_GenerateCodecs(t *testing.T) { + assertCodecs(t, "", &m3u8.PlaylistItem{}) + assertCodecs(t, "test", &m3u8.PlaylistItem{Codecs: pointer.ToString("test")}) + assertCodecs(t, "mp4a.40.2", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("aac-lc")}) + assertCodecs(t, "mp4a.40.2", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("AAC-LC")}) + assertCodecs(t, "mp4a.40.5", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("he-aac")}) + assertCodecs(t, "", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("he-aac1")}) + assertCodecs(t, "mp4a.40.34", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("mp3")}) + assertCodecs(t, "avc1.66.30", &m3u8.PlaylistItem{ + Profile: pointer.ToString("baseline"), + Level: pointer.ToString("3.0"), + }) + assertCodecs(t, "avc1.66.30,mp4a.40.2", &m3u8.PlaylistItem{ + Profile: pointer.ToString("baseline"), + Level: pointer.ToString("3.0"), + AudioCodec: pointer.ToString("aac-lc"), + }) + assertCodecs(t, "avc1.66.30,mp4a.40.34", &m3u8.PlaylistItem{ + Profile: pointer.ToString("baseline"), + Level: pointer.ToString("3.0"), + AudioCodec: pointer.ToString("mp3"), + }) + assertCodecs(t, "avc1.42001f", &m3u8.PlaylistItem{ + Profile: pointer.ToString("baseline"), + Level: pointer.ToString("3.1"), + }) + assertCodecs(t, "avc1.42001f,mp4a.40.5", &m3u8.PlaylistItem{ + Profile: pointer.ToString("baseline"), + Level: pointer.ToString("3.1"), + AudioCodec: pointer.ToString("he-aac"), + }) + assertCodecs(t, "avc1.77.30", &m3u8.PlaylistItem{ + Profile: pointer.ToString("main"), + Level: pointer.ToString("3.0"), + }) + assertCodecs(t, "avc1.77.30,mp4a.40.2", &m3u8.PlaylistItem{ + Profile: pointer.ToString("main"), + Level: pointer.ToString("3.0"), + AudioCodec: pointer.ToString("aac-lc"), + }) + assertCodecs(t, "avc1.4d001f", &m3u8.PlaylistItem{ + Profile: pointer.ToString("main"), + Level: pointer.ToString("3.1"), + }) + assertCodecs(t, "avc1.4d0028", &m3u8.PlaylistItem{ + Profile: pointer.ToString("main"), + Level: pointer.ToString("4.0"), + }) + assertCodecs(t, "avc1.4d0029", &m3u8.PlaylistItem{ + Profile: pointer.ToString("main"), + Level: pointer.ToString("4.1"), + }) + assertCodecs(t, "avc1.64001f", &m3u8.PlaylistItem{ + Profile: pointer.ToString("high"), + Level: pointer.ToString("3.1"), + }) + assertCodecs(t, "avc1.640028", &m3u8.PlaylistItem{ + Profile: pointer.ToString("high"), + Level: pointer.ToString("4.0"), + }) + assertCodecs(t, "avc1.640029", &m3u8.PlaylistItem{ + Profile: pointer.ToString("high"), + Level: pointer.ToString("4.1"), + }) +} + +func assertCodecs(t *testing.T, codecs string, p *m3u8.PlaylistItem) { + assert.Equal(t, codecs, p.CodecsString()) +} diff --git a/test/playlist_test.go b/test/playlist_test.go new file mode 100644 index 0000000..6d15a10 --- /dev/null +++ b/test/playlist_test.go @@ -0,0 +1,171 @@ +package test + +import ( + "fmt" + "github.com/AlekSi/pointer" + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestPlaylist_New(t *testing.T) { + p := &m3u8.Playlist{Master: pointer.ToBool(true)} + assert.True(t, p.IsMaster()) + + p, err := m3u8.ReadFile("fixtures/master.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsMaster()) + assert.Equal(t, len(p.Items), 8) +} + +func TestPlaylist_Duration(t *testing.T) { + p := &m3u8.Playlist{ + Items: []m3u8.Item{ + &m3u8.SegmentItem{Duration: 10.991, Segment: "test_01.ts"}, + &m3u8.SegmentItem{Duration: 9.891, Segment: "test_02.ts"}, + &m3u8.SegmentItem{Duration: 10.556, Segment: "test_03.ts"}, + &m3u8.SegmentItem{Duration: 8.790, Segment: "test_04.ts"}, + }, + } + + assert.Equal(t, "40.228", fmt.Sprintf("%.3f", p.Duration())) +} + +func TestPlaylist_Master(t *testing.T) { + // Normal master playlist + p := &m3u8.Playlist{ + Items: []m3u8.Item{ + &m3u8.PlaylistItem{ + ProgramID: pointer.ToString("1"), + URI: "playlist_url", + Bandwidth: 6400, + AudioCodec: pointer.ToString("mp3"), + }, + }, + } + assert.True(t, p.IsMaster()) + + // Media playlist + p = &m3u8.Playlist{ + Items: []m3u8.Item{ + &m3u8.SegmentItem{Duration: 10.991, Segment: "test_01.ts"}, + }, + } + assert.False(t, p.IsMaster()) + + // Forced master tag + p = &m3u8.Playlist{ + Master: pointer.ToBool(true), + } + assert.True(t, p.IsMaster()) +} + +func TestPlaylist_Live(t *testing.T) { + // Normal master playlist + p := &m3u8.Playlist{ + Items: []m3u8.Item{ + &m3u8.PlaylistItem{ + ProgramID: pointer.ToString("1"), + URI: "playlist_url", + Bandwidth: 6400, + AudioCodec: pointer.ToString("mp3"), + }, + }, + } + assert.False(t, p.IsLive()) + + // Media playlist set as live + p = &m3u8.Playlist{ + Items: []m3u8.Item{ + &m3u8.SegmentItem{Duration: 10.991, Segment: "test_01.ts"}, + }, + Live: true, + } + assert.True(t, p.IsLive()) +} + +func TestPlaylist_ToString(t *testing.T) { + p := &m3u8.Playlist{ + Items: []m3u8.Item{ + &m3u8.PlaylistItem{ + ProgramID: pointer.ToString("1"), + URI: "playlist_url", + Bandwidth: 6400, + AudioCodec: pointer.ToString("mp3"), + }, + &m3u8.PlaylistItem{ + ProgramID: pointer.ToString("2"), + URI: "playlist_url", + Bandwidth: 50000, + Width: pointer.ToInt(1920), + Height: pointer.ToInt(1080), + Profile: pointer.ToString("high"), + Level: pointer.ToString("4.1"), + AudioCodec: pointer.ToString("aac-lc"), + }, + }, + } + + expected := `#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34",BANDWIDTH=6400 +playlist_url +#EXT-X-STREAM-INF:PROGRAM-ID=2,RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2",BANDWIDTH=50000 +playlist_url +` + + assert.Equal(t, expected, p.String()) + + p = m3u8.NewPlaylistWithItems( + []m3u8.Item{ + &m3u8.SegmentItem{Duration: 11.344644, Segment: "1080-7mbps00000.ts"}, + &m3u8.SegmentItem{Duration: 11.261233, Segment: "1080-7mbps00001.ts"}, + }, + ) + expected = `#EXTM3U +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-TARGETDURATION:10 +#EXTINF:11.344644, +1080-7mbps00000.ts +#EXTINF:11.261233, +1080-7mbps00001.ts +#EXT-X-ENDLIST +` + + assert.Equal(t, expected, p.String()) +} + +func TestPlaylist_Valid(t *testing.T) { + p := m3u8.NewPlaylist() + assert.True(t, p.IsValid()) + + p.AppendItem(&m3u8.PlaylistItem{ + ProgramID: pointer.ToString("1"), + URI: "playlist_url", + Bandwidth: 540, + Width: pointer.ToInt(1920), + Height: pointer.ToInt(1080), + Codecs: pointer.ToString("avc"), + }) + + assert.True(t, p.IsValid()) + assert.Equal(t, 1, len(p.Items)) + + p.AppendItem(&m3u8.PlaylistItem{ + ProgramID: pointer.ToString("1"), + URI: "playlist_url", + Bandwidth: 540, + Width: pointer.ToInt(1920), + Height: pointer.ToInt(1080), + Codecs: pointer.ToString("avc"), + }) + + assert.True(t, p.IsValid()) + assert.Equal(t, 2, len(p.Items)) + + p.AppendItem(&m3u8.SegmentItem{ + Duration: 10.991, + Segment: "test.ts", + }) + + assert.False(t, p.IsValid()) +} diff --git a/test/reader_test.go b/test/reader_test.go new file mode 100644 index 0000000..5bc1245 --- /dev/null +++ b/test/reader_test.go @@ -0,0 +1,277 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestReader(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/master.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + assert.True(t, p.IsMaster()) + assert.Nil(t, p.DiscontinuitySequence) + assert.True(t, p.IndependentSegments) + + item := p.Items[0] + assert.IsType(t, &m3u8.SessionKeyItem{}, item) + keyItem := item.(*m3u8.SessionKeyItem) + assert.Equal(t, "AES-128", keyItem.Encryptable.Method) + assertNotNilEqual(t, "https://priv.example.com/key.php?r=52", keyItem.Encryptable.URI) + + item = p.Items[1] + assert.IsType(t, &m3u8.PlaybackStart{}, item) + psi := item.(*m3u8.PlaybackStart) + assert.Equal(t, 20.2, psi.TimeOffset) + + item = p.Items[2] + assert.IsType(t, &m3u8.PlaylistItem{}, item) + pi := item.(*m3u8.PlaylistItem) + assert.Equal(t, "hls/1080-7mbps/1080-7mbps.m3u8", pi.URI) + assertNotNilEqual(t, "1", pi.ProgramID) + assertNotNilEqual(t, 1920, pi.Width) + assertNotNilEqual(t, 1080, pi.Height) + assert.Equal(t, "1920x1080", pi.Resolution.String()) + assert.Equal(t, "avc1.640028,mp4a.40.2", pi.CodecsString()) + assert.Equal(t, 5042000, pi.Bandwidth) + assert.False(t, pi.IFrame) + assert.Nil(t, pi.AverageBandwidth) + + item = p.Items[7] + assert.IsType(t, &m3u8.PlaylistItem{}, item) + pi = item.(*m3u8.PlaylistItem) + assert.Equal(t, "hls/64k/64k.m3u8", pi.URI) + assertNotNilEqual(t, "1", pi.ProgramID) + assert.Nil(t, pi.Height) + assert.Nil(t, pi.Width) + assert.Empty(t, pi.Resolution.String()) + assert.Equal(t, 6400, pi.Bandwidth) + assert.False(t, pi.IFrame) + assert.Nil(t, pi.AverageBandwidth) + + assert.Equal(t, 8, p.ItemSize()) + + item = p.Items[len(p.Items)-1] + assert.IsType(t, &m3u8.PlaylistItem{}, item) + pi = item.(*m3u8.PlaylistItem) + assert.Empty(t, pi.Resolution.String()) +} + +func TestReader_IFrame(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/masterIframes.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + assert.True(t, p.IsMaster()) + + assert.Equal(t, 7, p.ItemSize()) + + item := p.Items[1] + assert.IsType(t, &m3u8.PlaylistItem{}, item) + pi := item.(*m3u8.PlaylistItem) + assert.Equal(t, "low/iframe.m3u8", pi.URI) + assert.Equal(t, 86000, pi.Bandwidth) + assert.True(t, pi.IFrame) +} + +func TestReader_MediaPlaylist(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/playlist.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + assert.False(t, p.IsMaster()) + + assertNotNilEqual(t, 4, p.Version) + assert.Equal(t, 1, p.Sequence) + assertNotNilEqual(t, 8, p.DiscontinuitySequence) + assertNotNilEqual(t, false, p.Cache) + assert.Equal(t, 12, p.Target) + assertNotNilEqual(t, "VOD", p.Type) + + item := p.Items[0] + assert.IsType(t, &m3u8.SegmentItem{}, item) + si := item.(*m3u8.SegmentItem) + assert.Equal(t, 11.344644, si.Duration) + assert.Nil(t, si.Comment) + + item = p.Items[4] + assert.IsType(t, &m3u8.TimeItem{}, item) + ti := item.(*m3u8.TimeItem) + assert.Equal(t, "2010-02-19T14:54:23Z", m3u8.FormatTime(ti.Time)) + + assert.Equal(t, 140, p.ItemSize()) +} + +func TestReader_IFramePlaylist(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/iframes.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + + assert.True(t, p.IFramesOnly) + assert.Equal(t, 3, p.ItemSize()) + + item := p.Items[0] + assert.IsType(t, &m3u8.SegmentItem{}, item) + si := item.(*m3u8.SegmentItem) + assert.Equal(t, 4.12, si.Duration) + assert.NotNil(t, si.ByteRange) + assertNotNilEqual(t, 9400, si.ByteRange.Length) + assertNotNilEqual(t, 376, si.ByteRange.Start) + assert.Equal(t, "segment1.ts", si.Segment) + + item = p.Items[1] + assert.IsType(t, &m3u8.SegmentItem{}, item) + si = item.(*m3u8.SegmentItem) + assert.NotNil(t, si.ByteRange) + assertNotNilEqual(t, 7144, si.ByteRange.Length) + assert.Nil(t, si.ByteRange.Start) +} + +func TestReader_PlaylistWithComments(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/playlistWithComments.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + + assert.False(t, p.IsMaster()) + assertNotNilEqual(t, 4, p.Version) + assert.Equal(t, 1, p.Sequence) + assertNotNilEqual(t, false, p.Cache) + assert.Equal(t, 12, p.Target) + assertNotNilEqual(t, "VOD", p.Type) + + item := p.Items[0] + assert.IsType(t, &m3u8.SegmentItem{}, item) + si := item.(*m3u8.SegmentItem) + + assert.Equal(t, 11.344644, si.Duration) + assertNotNilEqual(t, "anything", si.Comment) + + item = p.Items[1] + assert.IsType(t, &m3u8.DiscontinuityItem{}, item) + + assert.Equal(t, 139, p.ItemSize()) +} + +func TestReader_VariantAudio(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/variantAudio.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + assert.True(t, p.IsMaster()) + assert.Equal(t, 10, p.ItemSize()) + + item := p.Items[0] + assert.IsType(t, &m3u8.MediaItem{}, item) + mi := item.(*m3u8.MediaItem) + + assert.Equal(t, "AUDIO", mi.Type) + assert.Equal(t, "audio-lo", mi.GroupID) + assert.Equal(t, "English", mi.Name) + assertNotNilEqual(t, "eng", mi.Language) + assertNotNilEqual(t, "spoken", mi.AssocLanguage) + assertNotNilEqual(t, true, mi.AutoSelect) + assertNotNilEqual(t, true, mi.Default) + assertNotNilEqual(t, "englo/prog_index.m3u8", mi.URI) + assertNotNilEqual(t, true, mi.Forced) +} + +func TestReader_VariantAngles(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/variantAngles.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + assert.True(t, p.IsMaster()) + assert.Equal(t, 11, p.ItemSize()) + + item := p.Items[1] + assert.IsType(t, &m3u8.MediaItem{}, item) + mi := item.(*m3u8.MediaItem) + + assert.Equal(t, "VIDEO", mi.Type) + assert.Equal(t, "200kbs", mi.GroupID) + assert.Equal(t, "Angle2", mi.Name) + assert.Nil(t, mi.Language) + assertNotNilEqual(t, true, mi.AutoSelect) + assertNotNilEqual(t, false, mi.Default) + assertNotNilEqual(t, "Angle2/200kbs/prog_index.m3u8", mi.URI) + + item = p.Items[9] + assert.IsType(t, &m3u8.PlaylistItem{}, item) + pi := item.(*m3u8.PlaylistItem) + assertNotNilEqual(t, 300001, pi.AverageBandwidth) + assertNotNilEqual(t, "aac", pi.Audio) + assertNotNilEqual(t, "200kbs", pi.Video) + assertNotNilEqual(t, "captions", pi.ClosedCaptions) + assertNotNilEqual(t, "subs", pi.Subtitles) +} + +func TestReader_SessionData(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/sessionData.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + assert.Equal(t, 3, p.ItemSize()) + + item := p.Items[0] + assert.IsType(t, &m3u8.SessionDataItem{}, item) + sdi := item.(*m3u8.SessionDataItem) + + assert.Equal(t, "com.example.lyrics", sdi.DataID) + assertNotNilEqual(t, "lyrics.json", sdi.URI) +} + +func TestReader_Encrypted(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/encrypted.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + assert.Equal(t, 6, p.ItemSize()) + + item := p.Items[0] + assert.IsType(t, &m3u8.KeyItem{}, item) + ki := item.(*m3u8.KeyItem) + + assert.Equal(t, "AES-128", ki.Encryptable.Method) + assertNotNilEqual(t, "https://priv.example.com/key.php?r=52", ki.Encryptable.URI) +} + +func TestReader_Map(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/mapPlaylist.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + assert.Equal(t, 1, p.ItemSize()) + + item := p.Items[0] + assert.IsType(t, &m3u8.MapItem{}, item) + mi := item.(*m3u8.MapItem) + + assert.Equal(t, "frelo/prog_index.m3u8", mi.URI) + assert.NotNil(t, mi.ByteRange) + assertNotNilEqual(t, 4500, mi.ByteRange.Length) + assertNotNilEqual(t, 600, mi.ByteRange.Start) +} + +func TestReader_Timestamp(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/timestampPlaylist.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + assert.Equal(t, 6, p.ItemSize()) + + item := p.Items[0] + assert.IsType(t, &m3u8.SegmentItem{}, item) + si := item.(*m3u8.SegmentItem) + + assert.NotNil(t, si.ProgramDateTime) + assert.Equal(t, "2016-04-11T15:24:31Z", m3u8.FormatTime(si.ProgramDateTime.Time)) +} + +func TestReader_DateRange(t *testing.T) { + p, err := m3u8.ReadFile("fixtures/dateRangeScte35.m3u8") + assert.Nil(t, err) + assert.True(t, p.IsValid()) + assert.Equal(t, 5, p.ItemSize()) + + item := &m3u8.DateRangeItem{} + assert.IsType(t, item, p.Items[0]) + assert.IsType(t, item, p.Items[4]) +} + +func TestReader_Invalid(t *testing.T) { + _, err := m3u8.ReadFile("path/to/file") + assert.NotNil(t, err) +} diff --git a/test/segmentItem_test.go b/test/segmentItem_test.go new file mode 100644 index 0000000..ccf74be --- /dev/null +++ b/test/segmentItem_test.go @@ -0,0 +1,54 @@ +package test + +import ( + "github.com/AlekSi/pointer" + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSegmentItem_Parse(t *testing.T) { + time, err := m3u8.ParseTime("2010-02-19T14:54:23Z") + assert.Nil(t, err) + + item := &m3u8.SegmentItem{ + Duration: 10.991, + Segment: "test.ts", + ProgramDateTime: &m3u8.TimeItem{ + Time: time, + }, + } + + assert.Equal(t, "#EXTINF:10.991,\n#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23Z\ntest.ts", item.String()) + + item = &m3u8.SegmentItem{ + Duration: 10.991, + Segment: "test.ts", + Comment: pointer.ToString("anything"), + } + + assert.Equal(t, "#EXTINF:10.991,anything\ntest.ts", item.String()) + + item = &m3u8.SegmentItem{ + Duration: 10.991, + Segment: "test.ts", + Comment: pointer.ToString("anything"), + ByteRange: &m3u8.ByteRange{ + Length: pointer.ToInt(4500), + Start: pointer.ToInt(600), + }, + } + + assert.Equal(t, "#EXTINF:10.991,anything\n#EXT-X-BYTERANGE:4500@600\ntest.ts", item.String()) + + item = &m3u8.SegmentItem{ + Duration: 10.991, + Segment: "test.ts", + Comment: pointer.ToString("anything"), + ByteRange: &m3u8.ByteRange{ + Length: pointer.ToInt(4500), + }, + } + + assert.Equal(t, "#EXTINF:10.991,anything\n#EXT-X-BYTERANGE:4500\ntest.ts", item.String()) +} diff --git a/test/sessionDataItem_test.go b/test/sessionDataItem_test.go new file mode 100644 index 0000000..c873845 --- /dev/null +++ b/test/sessionDataItem_test.go @@ -0,0 +1,30 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSessionDataItem_Parse(t *testing.T) { + line := `#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",VALUE="Test",LANGUAGE="en"` + + sdi, err := m3u8.NewSessionDataItem(line) + assert.Nil(t, err) + + assert.Equal(t, "com.test.movie.title", sdi.DataID) + assertNotNilEqual(t, "Test", sdi.Value) + assert.Nil(t, sdi.URI) + assertNotNilEqual(t, "en", sdi.Language) + assertToString(t, line, sdi) + + line = `#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",URI="http://test",LANGUAGE="en"` + sdi, err = m3u8.NewSessionDataItem(line) + assert.Nil(t, err) + + assert.Equal(t, "com.test.movie.title", sdi.DataID) + assert.Nil(t, sdi.Value) + assertNotNilEqual(t, "http://test", sdi.URI) + assertNotNilEqual(t, "en", sdi.Language) + assertToString(t, line, sdi) +} diff --git a/test/sessionKeyItem_test.go b/test/sessionKeyItem_test.go new file mode 100644 index 0000000..7315ac8 --- /dev/null +++ b/test/sessionKeyItem_test.go @@ -0,0 +1,23 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSessionKeyItem_Parse(t *testing.T) { + line := `#EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://test.key",IV=D512BBF,KEYFORMAT="identity",KEYFORMATVERSIONS="1/3"` + + ski, err := m3u8.NewSessionKeyItem(line) + assert.Nil(t, err) + assert.NotNil(t, ski.Encryptable) + + assert.Equal(t, "AES-128", ski.Encryptable.Method) + assertNotNilEqual(t, "http://test.key", ski.Encryptable.URI) + assertNotNilEqual(t, "D512BBF", ski.Encryptable.IV) + assertNotNilEqual(t, "identity", ski.Encryptable.KeyFormat) + assertNotNilEqual(t, "1/3", ski.Encryptable.KeyFormatVersions) + + assertToString(t, line, ski) +} diff --git a/test/timeItem_test.go b/test/timeItem_test.go new file mode 100644 index 0000000..e54a879 --- /dev/null +++ b/test/timeItem_test.go @@ -0,0 +1,28 @@ +package test + +import ( + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestTimeItem_New(t *testing.T) { + timeVar, err := m3u8.ParseTime("2010-02-19T14:54:23.031Z") + assert.Nil(t, err) + ti := &m3u8.TimeItem{ + Time: timeVar, + } + + assert.Equal(t, "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z", ti.String()) +} + +func TestTimeItem_Parse(t *testing.T) { + ti, err := m3u8.NewTimeItem("#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z") + assert.Nil(t, err) + + expected, err := time.Parse(time.RFC3339Nano, "2010-02-19T14:54:23.031Z") + assert.Nil(t, err) + + assert.Equal(t, expected, ti.Time) +} diff --git a/test/writer_test.go b/test/writer_test.go new file mode 100644 index 0000000..e53f21e --- /dev/null +++ b/test/writer_test.go @@ -0,0 +1,207 @@ +package test + +import ( + "github.com/AlekSi/pointer" + "github.com/quangngotan95/go-m3u8/m3u8" + "github.com/stretchr/testify/assert" + "testing" +) + +type testCase struct { + p *m3u8.Playlist + expected string +} + +func TestWriter_Master(t *testing.T) { + testCases := []testCase{ + // Master playlist + { + &m3u8.Playlist{ + Target: 10, + Items: []m3u8.Item{ + &m3u8.PlaylistItem{ + ProgramID: pointer.ToString("1"), + URI: "playlist_url", + Bandwidth: 6400, + AudioCodec: pointer.ToString("mp3"), + }, + &m3u8.PlaylistItem{ + ProgramID: pointer.ToString("2"), + URI: "playlist_url", + Bandwidth: 50000, + AudioCodec: pointer.ToString("aac-lc"), + Width: pointer.ToInt(1920), + Height: pointer.ToInt(1080), + Profile: pointer.ToString("high"), + Level: pointer.ToString("4.1"), + }, + &m3u8.SessionDataItem{ + DataID: "com.test.movie.title", + Value: pointer.ToString("Test"), + URI: pointer.ToString("http://test"), + Language: pointer.ToString("en"), + }, + }, + }, + `#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34",BANDWIDTH=6400 +playlist_url +#EXT-X-STREAM-INF:PROGRAM-ID=2,RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2",BANDWIDTH=50000 +playlist_url +#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",VALUE="Test",URI="http://test",LANGUAGE="en" +`, + }, + // Master playlist with single stream + { + &m3u8.Playlist{ + Target: 10, + Items: []m3u8.Item{ + &m3u8.PlaylistItem{ + ProgramID: pointer.ToString("1"), + URI: "playlist_url", + Bandwidth: 6400, + AudioCodec: pointer.ToString("mp3"), + }, + }, + }, + `#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34",BANDWIDTH=6400 +playlist_url +`, + }, + // Master playlist with header options + { + &m3u8.Playlist{ + Target: 10, + Version: pointer.ToInt(6), + IndependentSegments: true, + Items: []m3u8.Item{ + &m3u8.PlaylistItem{ + URI: "playlist_url", + Bandwidth: 6400, + AudioCodec: pointer.ToString("mp3"), + }, + }, + }, + `#EXTM3U +#EXT-X-VERSION:6 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:CODECS="mp4a.40.34",BANDWIDTH=6400 +playlist_url +`, + }, + // New master playlist + { + &m3u8.Playlist{ + Master: pointer.ToBool(true), + }, + `#EXTM3U +`, + }, + // New media playlist + { + &m3u8.Playlist{ + Target: 10, + }, + `#EXTM3U +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-TARGETDURATION:10 +#EXT-X-ENDLIST +`, + }, + // Media playlist + { + &m3u8.Playlist{ + Version: pointer.ToInt(4), + Cache: pointer.ToBool(false), + Target: 6, + Sequence: 1, + DiscontinuitySequence: pointer.ToInt(10), + Type: pointer.ToString("EVENT"), + IFramesOnly: true, + Items: []m3u8.Item{ + &m3u8.SegmentItem{ + Duration: 11.344644, + Segment: "1080-7mbps00000.ts", + }, + }, + }, + `#EXTM3U +#EXT-X-PLAYLIST-TYPE:EVENT +#EXT-X-VERSION:4 +#EXT-X-I-FRAMES-ONLY +#EXT-X-MEDIA-SEQUENCE:1 +#EXT-X-DISCONTINUITY-SEQUENCE:10 +#EXT-X-ALLOW-CACHE:NO +#EXT-X-TARGETDURATION:6 +#EXTINF:11.344644, +1080-7mbps00000.ts +#EXT-X-ENDLIST +`, + }, + // Media playlist with keys + { + &m3u8.Playlist{ + Target: 10, + Version: pointer.ToInt(7), + Items: []m3u8.Item{ + &m3u8.SegmentItem{ + Duration: 11.344644, + Segment: "1080-7mbps00000.ts", + }, + &m3u8.KeyItem{ + Encryptable: &m3u8.Encryptable{ + Method: "AES-128", + URI: pointer.ToString("http://test.key"), + IV: pointer.ToString("D512BBF"), + KeyFormat: pointer.ToString("identity"), + KeyFormatVersions: pointer.ToString("1/3"), + }, + }, + &m3u8.SegmentItem{ + Duration: 11.261233, + Segment: "1080-7mbps00001.ts", + }, + }, + }, + `#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-TARGETDURATION:10 +#EXTINF:11.344644, +1080-7mbps00000.ts +#EXT-X-KEY:METHOD=AES-128,URI="http://test.key",IV=D512BBF,KEYFORMAT="identity",KEYFORMATVERSIONS="1/3" +#EXTINF:11.261233, +1080-7mbps00001.ts +#EXT-X-ENDLIST +`, + }, + } + for _, tc := range testCases { + tc.assert(t) + } + + p := &m3u8.Playlist{ + Target: 10, + Items: []m3u8.Item{ + &m3u8.PlaylistItem{ + ProgramID: pointer.ToString("1"), + Width: pointer.ToInt(1920), + Height: pointer.ToInt(1080), + Codecs: pointer.ToString("avc"), + Bandwidth: 540, + URI: "test.url", + }, + &m3u8.SegmentItem{ + Duration: 10.991, + Segment: "test.ts", + }, + }, + } + _, err := m3u8.Write(p) + assert.Equal(t, m3u8.ErrPlaylistInvalidType, err) +} + +func (tc testCase) assert(t *testing.T) { + assert.Equal(t, tc.expected, tc.p.String()) +} diff --git a/vendor/github.com/AlekSi/pointer/.travis.yml b/vendor/github.com/AlekSi/pointer/.travis.yml new file mode 100644 index 0000000..53e438f --- /dev/null +++ b/vendor/github.com/AlekSi/pointer/.travis.yml @@ -0,0 +1,9 @@ +language: go +sudo: false + +go: + - 1.6.4 + - 1.7.4 + - tip + +script: go test -v diff --git a/vendor/github.com/AlekSi/pointer/LICENSE b/vendor/github.com/AlekSi/pointer/LICENSE new file mode 100644 index 0000000..55f8794 --- /dev/null +++ b/vendor/github.com/AlekSi/pointer/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Alexey Palazhchenko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/AlekSi/pointer/README.md b/vendor/github.com/AlekSi/pointer/README.md new file mode 100644 index 0000000..d67f42d --- /dev/null +++ b/vendor/github.com/AlekSi/pointer/README.md @@ -0,0 +1,42 @@ +# pointer [![GoDoc](https://godoc.org/github.com/AlekSi/pointer?status.svg)](https://godoc.org/github.com/AlekSi/pointer) [![Build Status](https://travis-ci.org/AlekSi/pointer.svg)](https://travis-ci.org/AlekSi/pointer) + +Go package pointer provides helpers to get pointers to values of build-in types. + +``` +go get github.com/AlekSi/pointer +``` + +API is stable. [Documentation](http://godoc.org/github.com/AlekSi/pointer). + + +```go +package motivationalexample + +import ( + "encoding/json" + + "github.com/AlekSi/pointer" +) + +const ( + defaultName = "some name" +) + +// Stuff contains optional fields. +type Stuff struct { + Name *string + Comment *string + Value *int64 + Time *time.Time +} + +// SomeStuff makes some JSON-encoded stuff. +func SomeStuff() (data []byte, err error) { + return json.Marshal(&Stuff{ + Name: pointer.ToString(defaultName), // can't say &defaultName + Comment: pointer.ToString("not yet"), // can't say &"not yet" + Value: pointer.ToInt64(42), // can't say &42 or &int64(42) + Time: pointer.ToTime(time.Date(2014, 6, 25, 12, 24, 40, 0, time.UTC)), // can't say &time.Date(…) + }) +} +``` diff --git a/vendor/github.com/AlekSi/pointer/pointer.go b/vendor/github.com/AlekSi/pointer/pointer.go new file mode 100644 index 0000000..e27df7f --- /dev/null +++ b/vendor/github.com/AlekSi/pointer/pointer.go @@ -0,0 +1,28 @@ +// Package pointer provides helpers to get pointers to values of build-in types. +package pointer // import "github.com/AlekSi/pointer" + +import ( + "time" +) + +func ToBool(b bool) *bool { return &b } +func ToByte(b byte) *byte { return &b } +func ToComplex128(c complex128) *complex128 { return &c } +func ToComplex64(c complex64) *complex64 { return &c } +func ToError(e error) *error { return &e } +func ToFloat32(f float32) *float32 { return &f } +func ToFloat64(f float64) *float64 { return &f } +func ToInt(i int) *int { return &i } +func ToInt16(i int16) *int16 { return &i } +func ToInt32(i int32) *int32 { return &i } +func ToInt64(i int64) *int64 { return &i } +func ToInt8(i int8) *int8 { return &i } +func ToRune(r rune) *rune { return &r } +func ToString(s string) *string { return &s } +func ToTime(t time.Time) *time.Time { return &t } +func ToUint(u uint) *uint { return &u } +func ToUint16(u uint16) *uint16 { return &u } +func ToUint32(u uint32) *uint32 { return &u } +func ToUint64(u uint64) *uint64 { return &u } +func ToUint8(u uint8) *uint8 { return &u } +func ToUintptr(u uintptr) *uintptr { return &u } diff --git a/vendor/github.com/davecgh/go-spew/LICENSE b/vendor/github.com/davecgh/go-spew/LICENSE new file mode 100644 index 0000000..bc52e96 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2012-2016 Dave Collins + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/vendor/github.com/davecgh/go-spew/spew/bypass.go b/vendor/github.com/davecgh/go-spew/spew/bypass.go new file mode 100644 index 0000000..7929947 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/bypass.go @@ -0,0 +1,145 @@ +// Copyright (c) 2015-2016 Dave Collins +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when the code is not running on Google App Engine, compiled by GopherJS, and +// "-tags safe" is not added to the go build command line. The "disableunsafe" +// tag is deprecated and thus should not be used. +// Go versions prior to 1.4 are disabled because they use a different layout +// for interfaces which make the implementation of unsafeReflectValue more complex. +// +build !js,!appengine,!safe,!disableunsafe,go1.4 + +package spew + +import ( + "reflect" + "unsafe" +) + +const ( + // UnsafeDisabled is a build-time constant which specifies whether or + // not access to the unsafe package is available. + UnsafeDisabled = false + + // ptrSize is the size of a pointer on the current arch. + ptrSize = unsafe.Sizeof((*byte)(nil)) +) + +type flag uintptr + +var ( + // flagRO indicates whether the value field of a reflect.Value + // is read-only. + flagRO flag + + // flagAddr indicates whether the address of the reflect.Value's + // value may be taken. + flagAddr flag +) + +// flagKindMask holds the bits that make up the kind +// part of the flags field. In all the supported versions, +// it is in the lower 5 bits. +const flagKindMask = flag(0x1f) + +// Different versions of Go have used different +// bit layouts for the flags type. This table +// records the known combinations. +var okFlags = []struct { + ro, addr flag +}{{ + // From Go 1.4 to 1.5 + ro: 1 << 5, + addr: 1 << 7, +}, { + // Up to Go tip. + ro: 1<<5 | 1<<6, + addr: 1 << 8, +}} + +var flagValOffset = func() uintptr { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") + } + return field.Offset +}() + +// flagField returns a pointer to the flag field of a reflect.Value. +func flagField(v *reflect.Value) *flag { + return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset)) +} + +// unsafeReflectValue converts the passed reflect.Value into a one that bypasses +// the typical safety restrictions preventing access to unaddressable and +// unexported data. It works by digging the raw pointer to the underlying +// value out of the protected value and generating a new unprotected (unsafe) +// reflect.Value to it. +// +// This allows us to check for implementations of the Stringer and error +// interfaces to be used for pretty printing ordinarily unaddressable and +// inaccessible values such as unexported struct fields. +func unsafeReflectValue(v reflect.Value) reflect.Value { + if !v.IsValid() || (v.CanInterface() && v.CanAddr()) { + return v + } + flagFieldPtr := flagField(&v) + *flagFieldPtr &^= flagRO + *flagFieldPtr |= flagAddr + return v +} + +// Sanity checks against future reflect package changes +// to the type or semantics of the Value.flag field. +func init() { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") + } + if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() { + panic("reflect.Value flag field has changed kind") + } + type t0 int + var t struct { + A t0 + // t0 will have flagEmbedRO set. + t0 + // a will have flagStickyRO set + a t0 + } + vA := reflect.ValueOf(t).FieldByName("A") + va := reflect.ValueOf(t).FieldByName("a") + vt0 := reflect.ValueOf(t).FieldByName("t0") + + // Infer flagRO from the difference between the flags + // for the (otherwise identical) fields in t. + flagPublic := *flagField(&vA) + flagWithRO := *flagField(&va) | *flagField(&vt0) + flagRO = flagPublic ^ flagWithRO + + // Infer flagAddr from the difference between a value + // taken from a pointer and not. + vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A") + flagNoPtr := *flagField(&vA) + flagPtr := *flagField(&vPtrA) + flagAddr = flagNoPtr ^ flagPtr + + // Check that the inferred flags tally with one of the known versions. + for _, f := range okFlags { + if flagRO == f.ro && flagAddr == f.addr { + return + } + } + panic("reflect.Value read-only flag has changed semantics") +} diff --git a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go new file mode 100644 index 0000000..205c28d --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go @@ -0,0 +1,38 @@ +// Copyright (c) 2015-2016 Dave Collins +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when the code is running on Google App Engine, compiled by GopherJS, or +// "-tags safe" is added to the go build command line. The "disableunsafe" +// tag is deprecated and thus should not be used. +// +build js appengine safe disableunsafe !go1.4 + +package spew + +import "reflect" + +const ( + // UnsafeDisabled is a build-time constant which specifies whether or + // not access to the unsafe package is available. + UnsafeDisabled = true +) + +// unsafeReflectValue typically converts the passed reflect.Value into a one +// that bypasses the typical safety restrictions preventing access to +// unaddressable and unexported data. However, doing this relies on access to +// the unsafe package. This is a stub version which simply returns the passed +// reflect.Value when the unsafe package is not available. +func unsafeReflectValue(v reflect.Value) reflect.Value { + return v +} diff --git a/vendor/github.com/davecgh/go-spew/spew/common.go b/vendor/github.com/davecgh/go-spew/spew/common.go new file mode 100644 index 0000000..1be8ce9 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/common.go @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "io" + "reflect" + "sort" + "strconv" +) + +// Some constants in the form of bytes to avoid string overhead. This mirrors +// the technique used in the fmt package. +var ( + panicBytes = []byte("(PANIC=") + plusBytes = []byte("+") + iBytes = []byte("i") + trueBytes = []byte("true") + falseBytes = []byte("false") + interfaceBytes = []byte("(interface {})") + commaNewlineBytes = []byte(",\n") + newlineBytes = []byte("\n") + openBraceBytes = []byte("{") + openBraceNewlineBytes = []byte("{\n") + closeBraceBytes = []byte("}") + asteriskBytes = []byte("*") + colonBytes = []byte(":") + colonSpaceBytes = []byte(": ") + openParenBytes = []byte("(") + closeParenBytes = []byte(")") + spaceBytes = []byte(" ") + pointerChainBytes = []byte("->") + nilAngleBytes = []byte("") + maxNewlineBytes = []byte("\n") + maxShortBytes = []byte("") + circularBytes = []byte("") + circularShortBytes = []byte("") + invalidAngleBytes = []byte("") + openBracketBytes = []byte("[") + closeBracketBytes = []byte("]") + percentBytes = []byte("%") + precisionBytes = []byte(".") + openAngleBytes = []byte("<") + closeAngleBytes = []byte(">") + openMapBytes = []byte("map[") + closeMapBytes = []byte("]") + lenEqualsBytes = []byte("len=") + capEqualsBytes = []byte("cap=") +) + +// hexDigits is used to map a decimal value to a hex digit. +var hexDigits = "0123456789abcdef" + +// catchPanic handles any panics that might occur during the handleMethods +// calls. +func catchPanic(w io.Writer, v reflect.Value) { + if err := recover(); err != nil { + w.Write(panicBytes) + fmt.Fprintf(w, "%v", err) + w.Write(closeParenBytes) + } +} + +// handleMethods attempts to call the Error and String methods on the underlying +// type the passed reflect.Value represents and outputes the result to Writer w. +// +// It handles panics in any called methods by catching and displaying the error +// as the formatted value. +func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) { + // We need an interface to check if the type implements the error or + // Stringer interface. However, the reflect package won't give us an + // interface on certain things like unexported struct fields in order + // to enforce visibility rules. We use unsafe, when it's available, + // to bypass these restrictions since this package does not mutate the + // values. + if !v.CanInterface() { + if UnsafeDisabled { + return false + } + + v = unsafeReflectValue(v) + } + + // Choose whether or not to do error and Stringer interface lookups against + // the base type or a pointer to the base type depending on settings. + // Technically calling one of these methods with a pointer receiver can + // mutate the value, however, types which choose to satisify an error or + // Stringer interface with a pointer receiver should not be mutating their + // state inside these interface methods. + if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() { + v = unsafeReflectValue(v) + } + if v.CanAddr() { + v = v.Addr() + } + + // Is it an error or Stringer? + switch iface := v.Interface().(type) { + case error: + defer catchPanic(w, v) + if cs.ContinueOnMethod { + w.Write(openParenBytes) + w.Write([]byte(iface.Error())) + w.Write(closeParenBytes) + w.Write(spaceBytes) + return false + } + + w.Write([]byte(iface.Error())) + return true + + case fmt.Stringer: + defer catchPanic(w, v) + if cs.ContinueOnMethod { + w.Write(openParenBytes) + w.Write([]byte(iface.String())) + w.Write(closeParenBytes) + w.Write(spaceBytes) + return false + } + w.Write([]byte(iface.String())) + return true + } + return false +} + +// printBool outputs a boolean value as true or false to Writer w. +func printBool(w io.Writer, val bool) { + if val { + w.Write(trueBytes) + } else { + w.Write(falseBytes) + } +} + +// printInt outputs a signed integer value to Writer w. +func printInt(w io.Writer, val int64, base int) { + w.Write([]byte(strconv.FormatInt(val, base))) +} + +// printUint outputs an unsigned integer value to Writer w. +func printUint(w io.Writer, val uint64, base int) { + w.Write([]byte(strconv.FormatUint(val, base))) +} + +// printFloat outputs a floating point value using the specified precision, +// which is expected to be 32 or 64bit, to Writer w. +func printFloat(w io.Writer, val float64, precision int) { + w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision))) +} + +// printComplex outputs a complex value using the specified float precision +// for the real and imaginary parts to Writer w. +func printComplex(w io.Writer, c complex128, floatPrecision int) { + r := real(c) + w.Write(openParenBytes) + w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision))) + i := imag(c) + if i >= 0 { + w.Write(plusBytes) + } + w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision))) + w.Write(iBytes) + w.Write(closeParenBytes) +} + +// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x' +// prefix to Writer w. +func printHexPtr(w io.Writer, p uintptr) { + // Null pointer. + num := uint64(p) + if num == 0 { + w.Write(nilAngleBytes) + return + } + + // Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix + buf := make([]byte, 18) + + // It's simpler to construct the hex string right to left. + base := uint64(16) + i := len(buf) - 1 + for num >= base { + buf[i] = hexDigits[num%base] + num /= base + i-- + } + buf[i] = hexDigits[num] + + // Add '0x' prefix. + i-- + buf[i] = 'x' + i-- + buf[i] = '0' + + // Strip unused leading bytes. + buf = buf[i:] + w.Write(buf) +} + +// valuesSorter implements sort.Interface to allow a slice of reflect.Value +// elements to be sorted. +type valuesSorter struct { + values []reflect.Value + strings []string // either nil or same len and values + cs *ConfigState +} + +// newValuesSorter initializes a valuesSorter instance, which holds a set of +// surrogate keys on which the data should be sorted. It uses flags in +// ConfigState to decide if and how to populate those surrogate keys. +func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface { + vs := &valuesSorter{values: values, cs: cs} + if canSortSimply(vs.values[0].Kind()) { + return vs + } + if !cs.DisableMethods { + vs.strings = make([]string, len(values)) + for i := range vs.values { + b := bytes.Buffer{} + if !handleMethods(cs, &b, vs.values[i]) { + vs.strings = nil + break + } + vs.strings[i] = b.String() + } + } + if vs.strings == nil && cs.SpewKeys { + vs.strings = make([]string, len(values)) + for i := range vs.values { + vs.strings[i] = Sprintf("%#v", vs.values[i].Interface()) + } + } + return vs +} + +// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted +// directly, or whether it should be considered for sorting by surrogate keys +// (if the ConfigState allows it). +func canSortSimply(kind reflect.Kind) bool { + // This switch parallels valueSortLess, except for the default case. + switch kind { + case reflect.Bool: + return true + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return true + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return true + case reflect.Float32, reflect.Float64: + return true + case reflect.String: + return true + case reflect.Uintptr: + return true + case reflect.Array: + return true + } + return false +} + +// Len returns the number of values in the slice. It is part of the +// sort.Interface implementation. +func (s *valuesSorter) Len() int { + return len(s.values) +} + +// Swap swaps the values at the passed indices. It is part of the +// sort.Interface implementation. +func (s *valuesSorter) Swap(i, j int) { + s.values[i], s.values[j] = s.values[j], s.values[i] + if s.strings != nil { + s.strings[i], s.strings[j] = s.strings[j], s.strings[i] + } +} + +// valueSortLess returns whether the first value should sort before the second +// value. It is used by valueSorter.Less as part of the sort.Interface +// implementation. +func valueSortLess(a, b reflect.Value) bool { + switch a.Kind() { + case reflect.Bool: + return !a.Bool() && b.Bool() + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return a.Int() < b.Int() + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return a.Uint() < b.Uint() + case reflect.Float32, reflect.Float64: + return a.Float() < b.Float() + case reflect.String: + return a.String() < b.String() + case reflect.Uintptr: + return a.Uint() < b.Uint() + case reflect.Array: + // Compare the contents of both arrays. + l := a.Len() + for i := 0; i < l; i++ { + av := a.Index(i) + bv := b.Index(i) + if av.Interface() == bv.Interface() { + continue + } + return valueSortLess(av, bv) + } + } + return a.String() < b.String() +} + +// Less returns whether the value at index i should sort before the +// value at index j. It is part of the sort.Interface implementation. +func (s *valuesSorter) Less(i, j int) bool { + if s.strings == nil { + return valueSortLess(s.values[i], s.values[j]) + } + return s.strings[i] < s.strings[j] +} + +// sortValues is a sort function that handles both native types and any type that +// can be converted to error or Stringer. Other inputs are sorted according to +// their Value.String() value to ensure display stability. +func sortValues(values []reflect.Value, cs *ConfigState) { + if len(values) == 0 { + return + } + sort.Sort(newValuesSorter(values, cs)) +} diff --git a/vendor/github.com/davecgh/go-spew/spew/config.go b/vendor/github.com/davecgh/go-spew/spew/config.go new file mode 100644 index 0000000..2e3d22f --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/config.go @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "io" + "os" +) + +// ConfigState houses the configuration options used by spew to format and +// display values. There is a global instance, Config, that is used to control +// all top-level Formatter and Dump functionality. Each ConfigState instance +// provides methods equivalent to the top-level functions. +// +// The zero value for ConfigState provides no indentation. You would typically +// want to set it to a space or a tab. +// +// Alternatively, you can use NewDefaultConfig to get a ConfigState instance +// with default settings. See the documentation of NewDefaultConfig for default +// values. +type ConfigState struct { + // Indent specifies the string to use for each indentation level. The + // global config instance that all top-level functions use set this to a + // single space by default. If you would like more indentation, you might + // set this to a tab with "\t" or perhaps two spaces with " ". + Indent string + + // MaxDepth controls the maximum number of levels to descend into nested + // data structures. The default, 0, means there is no limit. + // + // NOTE: Circular data structures are properly detected, so it is not + // necessary to set this value unless you specifically want to limit deeply + // nested data structures. + MaxDepth int + + // DisableMethods specifies whether or not error and Stringer interfaces are + // invoked for types that implement them. + DisableMethods bool + + // DisablePointerMethods specifies whether or not to check for and invoke + // error and Stringer interfaces on types which only accept a pointer + // receiver when the current type is not a pointer. + // + // NOTE: This might be an unsafe action since calling one of these methods + // with a pointer receiver could technically mutate the value, however, + // in practice, types which choose to satisify an error or Stringer + // interface with a pointer receiver should not be mutating their state + // inside these interface methods. As a result, this option relies on + // access to the unsafe package, so it will not have any effect when + // running in environments without access to the unsafe package such as + // Google App Engine or with the "safe" build tag specified. + DisablePointerMethods bool + + // DisablePointerAddresses specifies whether to disable the printing of + // pointer addresses. This is useful when diffing data structures in tests. + DisablePointerAddresses bool + + // DisableCapacities specifies whether to disable the printing of capacities + // for arrays, slices, maps and channels. This is useful when diffing + // data structures in tests. + DisableCapacities bool + + // ContinueOnMethod specifies whether or not recursion should continue once + // a custom error or Stringer interface is invoked. The default, false, + // means it will print the results of invoking the custom error or Stringer + // interface and return immediately instead of continuing to recurse into + // the internals of the data type. + // + // NOTE: This flag does not have any effect if method invocation is disabled + // via the DisableMethods or DisablePointerMethods options. + ContinueOnMethod bool + + // SortKeys specifies map keys should be sorted before being printed. Use + // this to have a more deterministic, diffable output. Note that only + // native types (bool, int, uint, floats, uintptr and string) and types + // that support the error or Stringer interfaces (if methods are + // enabled) are supported, with other types sorted according to the + // reflect.Value.String() output which guarantees display stability. + SortKeys bool + + // SpewKeys specifies that, as a last resort attempt, map keys should + // be spewed to strings and sorted by those strings. This is only + // considered if SortKeys is true. + SpewKeys bool +} + +// Config is the active configuration of the top-level functions. +// The configuration can be changed by modifying the contents of spew.Config. +var Config = ConfigState{Indent: " "} + +// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the formatted string as a value that satisfies error. See NewFormatter +// for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) { + return fmt.Errorf(format, c.convertArgs(a)...) +} + +// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprint(w, c.convertArgs(a)...) +} + +// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, format, c.convertArgs(a)...) +} + +// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it +// passed with a Formatter interface returned by c.NewFormatter. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, c.convertArgs(a)...) +} + +// Print is a wrapper for fmt.Print that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Print(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Print(a ...interface{}) (n int, err error) { + return fmt.Print(c.convertArgs(a)...) +} + +// Printf is a wrapper for fmt.Printf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(format, c.convertArgs(a)...) +} + +// Println is a wrapper for fmt.Println that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Println(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Println(a ...interface{}) (n int, err error) { + return fmt.Println(c.convertArgs(a)...) +} + +// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprint(a ...interface{}) string { + return fmt.Sprint(c.convertArgs(a)...) +} + +// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, c.convertArgs(a)...) +} + +// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it +// were passed with a Formatter interface returned by c.NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprintln(a ...interface{}) string { + return fmt.Sprintln(c.convertArgs(a)...) +} + +/* +NewFormatter returns a custom formatter that satisfies the fmt.Formatter +interface. As a result, it integrates cleanly with standard fmt package +printing functions. The formatter is useful for inline printing of smaller data +types similar to the standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Typically this function shouldn't be called directly. It is much easier to make +use of the custom formatter by calling one of the convenience functions such as +c.Printf, c.Println, or c.Printf. +*/ +func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter { + return newFormatter(c, v) +} + +// Fdump formats and displays the passed arguments to io.Writer w. It formats +// exactly the same as Dump. +func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) { + fdump(c, w, a...) +} + +/* +Dump displays the passed parameters to standard out with newlines, customizable +indentation, and additional debug information such as complete types and all +pointer addresses used to indirect to the final value. It provides the +following features over the built-in printing facilities provided by the fmt +package: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output + +The configuration options are controlled by modifying the public members +of c. See ConfigState for options documentation. + +See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to +get the formatted result as a string. +*/ +func (c *ConfigState) Dump(a ...interface{}) { + fdump(c, os.Stdout, a...) +} + +// Sdump returns a string with the passed arguments formatted exactly the same +// as Dump. +func (c *ConfigState) Sdump(a ...interface{}) string { + var buf bytes.Buffer + fdump(c, &buf, a...) + return buf.String() +} + +// convertArgs accepts a slice of arguments and returns a slice of the same +// length with each argument converted to a spew Formatter interface using +// the ConfigState associated with s. +func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) { + formatters = make([]interface{}, len(args)) + for index, arg := range args { + formatters[index] = newFormatter(c, arg) + } + return formatters +} + +// NewDefaultConfig returns a ConfigState with the following default settings. +// +// Indent: " " +// MaxDepth: 0 +// DisableMethods: false +// DisablePointerMethods: false +// ContinueOnMethod: false +// SortKeys: false +func NewDefaultConfig() *ConfigState { + return &ConfigState{Indent: " "} +} diff --git a/vendor/github.com/davecgh/go-spew/spew/doc.go b/vendor/github.com/davecgh/go-spew/spew/doc.go new file mode 100644 index 0000000..aacaac6 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/doc.go @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +Package spew implements a deep pretty printer for Go data structures to aid in +debugging. + +A quick overview of the additional features spew provides over the built-in +printing facilities for Go data types are as follows: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output (only when using + Dump style) + +There are two different approaches spew allows for dumping Go data structures: + + * Dump style which prints with newlines, customizable indentation, + and additional debug information such as types and all pointer addresses + used to indirect to the final value + * A custom Formatter interface that integrates cleanly with the standard fmt + package and replaces %v, %+v, %#v, and %#+v to provide inline printing + similar to the default %v while providing the additional functionality + outlined above and passing unsupported format verbs such as %x and %q + along to fmt + +Quick Start + +This section demonstrates how to quickly get started with spew. See the +sections below for further details on formatting and configuration options. + +To dump a variable with full newlines, indentation, type, and pointer +information use Dump, Fdump, or Sdump: + spew.Dump(myVar1, myVar2, ...) + spew.Fdump(someWriter, myVar1, myVar2, ...) + str := spew.Sdump(myVar1, myVar2, ...) + +Alternatively, if you would prefer to use format strings with a compacted inline +printing style, use the convenience wrappers Printf, Fprintf, etc with +%v (most compact), %+v (adds pointer addresses), %#v (adds types), or +%#+v (adds types and pointer addresses): + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + +Configuration Options + +Configuration of spew is handled by fields in the ConfigState type. For +convenience, all of the top-level functions use a global state available +via the spew.Config global. + +It is also possible to create a ConfigState instance that provides methods +equivalent to the top-level functions. This allows concurrent configuration +options. See the ConfigState documentation for more details. + +The following configuration options are available: + * Indent + String to use for each indentation level for Dump functions. + It is a single space by default. A popular alternative is "\t". + + * MaxDepth + Maximum number of levels to descend into nested data structures. + There is no limit by default. + + * DisableMethods + Disables invocation of error and Stringer interface methods. + Method invocation is enabled by default. + + * DisablePointerMethods + Disables invocation of error and Stringer interface methods on types + which only accept pointer receivers from non-pointer variables. + Pointer method invocation is enabled by default. + + * DisablePointerAddresses + DisablePointerAddresses specifies whether to disable the printing of + pointer addresses. This is useful when diffing data structures in tests. + + * DisableCapacities + DisableCapacities specifies whether to disable the printing of + capacities for arrays, slices, maps and channels. This is useful when + diffing data structures in tests. + + * ContinueOnMethod + Enables recursion into types after invoking error and Stringer interface + methods. Recursion after method invocation is disabled by default. + + * SortKeys + Specifies map keys should be sorted before being printed. Use + this to have a more deterministic, diffable output. Note that + only native types (bool, int, uint, floats, uintptr and string) + and types which implement error or Stringer interfaces are + supported with other types sorted according to the + reflect.Value.String() output which guarantees display + stability. Natural map order is used by default. + + * SpewKeys + Specifies that, as a last resort attempt, map keys should be + spewed to strings and sorted by those strings. This is only + considered if SortKeys is true. + +Dump Usage + +Simply call spew.Dump with a list of variables you want to dump: + + spew.Dump(myVar1, myVar2, ...) + +You may also call spew.Fdump if you would prefer to output to an arbitrary +io.Writer. For example, to dump to standard error: + + spew.Fdump(os.Stderr, myVar1, myVar2, ...) + +A third option is to call spew.Sdump to get the formatted output as a string: + + str := spew.Sdump(myVar1, myVar2, ...) + +Sample Dump Output + +See the Dump example for details on the setup of the types and variables being +shown here. + + (main.Foo) { + unexportedField: (*main.Bar)(0xf84002e210)({ + flag: (main.Flag) flagTwo, + data: (uintptr) + }), + ExportedField: (map[interface {}]interface {}) (len=1) { + (string) (len=3) "one": (bool) true + } + } + +Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C +command as shown. + ([]uint8) (len=32 cap=32) { + 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | + 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| + 00000020 31 32 |12| + } + +Custom Formatter + +Spew provides a custom formatter that implements the fmt.Formatter interface +so that it integrates cleanly with standard fmt package printing functions. The +formatter is useful for inline printing of smaller data types similar to the +standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Custom Formatter Usage + +The simplest way to make use of the spew custom formatter is to call one of the +convenience functions such as spew.Printf, spew.Println, or spew.Printf. The +functions have syntax you are most likely already familiar with: + + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + spew.Println(myVar, myVar2) + spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + +See the Index for the full list convenience functions. + +Sample Formatter Output + +Double pointer to a uint8: + %v: <**>5 + %+v: <**>(0xf8400420d0->0xf8400420c8)5 + %#v: (**uint8)5 + %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 + +Pointer to circular struct with a uint8 field and a pointer to itself: + %v: <*>{1 <*>} + %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)} + %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)} + %#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)} + +See the Printf example for details on the setup of variables being shown +here. + +Errors + +Since it is possible for custom Stringer/error interfaces to panic, spew +detects them and handles them internally by printing the panic information +inline with the output. Since spew is intended to provide deep pretty printing +capabilities on structures, it intentionally does not return any errors. +*/ +package spew diff --git a/vendor/github.com/davecgh/go-spew/spew/dump.go b/vendor/github.com/davecgh/go-spew/spew/dump.go new file mode 100644 index 0000000..f78d89f --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/dump.go @@ -0,0 +1,509 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "os" + "reflect" + "regexp" + "strconv" + "strings" +) + +var ( + // uint8Type is a reflect.Type representing a uint8. It is used to + // convert cgo types to uint8 slices for hexdumping. + uint8Type = reflect.TypeOf(uint8(0)) + + // cCharRE is a regular expression that matches a cgo char. + // It is used to detect character arrays to hexdump them. + cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`) + + // cUnsignedCharRE is a regular expression that matches a cgo unsigned + // char. It is used to detect unsigned character arrays to hexdump + // them. + cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`) + + // cUint8tCharRE is a regular expression that matches a cgo uint8_t. + // It is used to detect uint8_t arrays to hexdump them. + cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`) +) + +// dumpState contains information about the state of a dump operation. +type dumpState struct { + w io.Writer + depth int + pointers map[uintptr]int + ignoreNextType bool + ignoreNextIndent bool + cs *ConfigState +} + +// indent performs indentation according to the depth level and cs.Indent +// option. +func (d *dumpState) indent() { + if d.ignoreNextIndent { + d.ignoreNextIndent = false + return + } + d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth)) +} + +// unpackValue returns values inside of non-nil interfaces when possible. +// This is useful for data types like structs, arrays, slices, and maps which +// can contain varying types packed inside an interface. +func (d *dumpState) unpackValue(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Interface && !v.IsNil() { + v = v.Elem() + } + return v +} + +// dumpPtr handles formatting of pointers by indirecting them as necessary. +func (d *dumpState) dumpPtr(v reflect.Value) { + // Remove pointers at or below the current depth from map used to detect + // circular refs. + for k, depth := range d.pointers { + if depth >= d.depth { + delete(d.pointers, k) + } + } + + // Keep list of all dereferenced pointers to show later. + pointerChain := make([]uintptr, 0) + + // Figure out how many levels of indirection there are by dereferencing + // pointers and unpacking interfaces down the chain while detecting circular + // references. + nilFound := false + cycleFound := false + indirects := 0 + ve := v + for ve.Kind() == reflect.Ptr { + if ve.IsNil() { + nilFound = true + break + } + indirects++ + addr := ve.Pointer() + pointerChain = append(pointerChain, addr) + if pd, ok := d.pointers[addr]; ok && pd < d.depth { + cycleFound = true + indirects-- + break + } + d.pointers[addr] = d.depth + + ve = ve.Elem() + if ve.Kind() == reflect.Interface { + if ve.IsNil() { + nilFound = true + break + } + ve = ve.Elem() + } + } + + // Display type information. + d.w.Write(openParenBytes) + d.w.Write(bytes.Repeat(asteriskBytes, indirects)) + d.w.Write([]byte(ve.Type().String())) + d.w.Write(closeParenBytes) + + // Display pointer information. + if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 { + d.w.Write(openParenBytes) + for i, addr := range pointerChain { + if i > 0 { + d.w.Write(pointerChainBytes) + } + printHexPtr(d.w, addr) + } + d.w.Write(closeParenBytes) + } + + // Display dereferenced value. + d.w.Write(openParenBytes) + switch { + case nilFound: + d.w.Write(nilAngleBytes) + + case cycleFound: + d.w.Write(circularBytes) + + default: + d.ignoreNextType = true + d.dump(ve) + } + d.w.Write(closeParenBytes) +} + +// dumpSlice handles formatting of arrays and slices. Byte (uint8 under +// reflection) arrays and slices are dumped in hexdump -C fashion. +func (d *dumpState) dumpSlice(v reflect.Value) { + // Determine whether this type should be hex dumped or not. Also, + // for types which should be hexdumped, try to use the underlying data + // first, then fall back to trying to convert them to a uint8 slice. + var buf []uint8 + doConvert := false + doHexDump := false + numEntries := v.Len() + if numEntries > 0 { + vt := v.Index(0).Type() + vts := vt.String() + switch { + // C types that need to be converted. + case cCharRE.MatchString(vts): + fallthrough + case cUnsignedCharRE.MatchString(vts): + fallthrough + case cUint8tCharRE.MatchString(vts): + doConvert = true + + // Try to use existing uint8 slices and fall back to converting + // and copying if that fails. + case vt.Kind() == reflect.Uint8: + // We need an addressable interface to convert the type + // to a byte slice. However, the reflect package won't + // give us an interface on certain things like + // unexported struct fields in order to enforce + // visibility rules. We use unsafe, when available, to + // bypass these restrictions since this package does not + // mutate the values. + vs := v + if !vs.CanInterface() || !vs.CanAddr() { + vs = unsafeReflectValue(vs) + } + if !UnsafeDisabled { + vs = vs.Slice(0, numEntries) + + // Use the existing uint8 slice if it can be + // type asserted. + iface := vs.Interface() + if slice, ok := iface.([]uint8); ok { + buf = slice + doHexDump = true + break + } + } + + // The underlying data needs to be converted if it can't + // be type asserted to a uint8 slice. + doConvert = true + } + + // Copy and convert the underlying type if needed. + if doConvert && vt.ConvertibleTo(uint8Type) { + // Convert and copy each element into a uint8 byte + // slice. + buf = make([]uint8, numEntries) + for i := 0; i < numEntries; i++ { + vv := v.Index(i) + buf[i] = uint8(vv.Convert(uint8Type).Uint()) + } + doHexDump = true + } + } + + // Hexdump the entire slice as needed. + if doHexDump { + indent := strings.Repeat(d.cs.Indent, d.depth) + str := indent + hex.Dump(buf) + str = strings.Replace(str, "\n", "\n"+indent, -1) + str = strings.TrimRight(str, d.cs.Indent) + d.w.Write([]byte(str)) + return + } + + // Recursively call dump for each item. + for i := 0; i < numEntries; i++ { + d.dump(d.unpackValue(v.Index(i))) + if i < (numEntries - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } +} + +// dump is the main workhorse for dumping a value. It uses the passed reflect +// value to figure out what kind of object we are dealing with and formats it +// appropriately. It is a recursive function, however circular data structures +// are detected and handled properly. +func (d *dumpState) dump(v reflect.Value) { + // Handle invalid reflect values immediately. + kind := v.Kind() + if kind == reflect.Invalid { + d.w.Write(invalidAngleBytes) + return + } + + // Handle pointers specially. + if kind == reflect.Ptr { + d.indent() + d.dumpPtr(v) + return + } + + // Print type information unless already handled elsewhere. + if !d.ignoreNextType { + d.indent() + d.w.Write(openParenBytes) + d.w.Write([]byte(v.Type().String())) + d.w.Write(closeParenBytes) + d.w.Write(spaceBytes) + } + d.ignoreNextType = false + + // Display length and capacity if the built-in len and cap functions + // work with the value's kind and the len/cap itself is non-zero. + valueLen, valueCap := 0, 0 + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.Chan: + valueLen, valueCap = v.Len(), v.Cap() + case reflect.Map, reflect.String: + valueLen = v.Len() + } + if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 { + d.w.Write(openParenBytes) + if valueLen != 0 { + d.w.Write(lenEqualsBytes) + printInt(d.w, int64(valueLen), 10) + } + if !d.cs.DisableCapacities && valueCap != 0 { + if valueLen != 0 { + d.w.Write(spaceBytes) + } + d.w.Write(capEqualsBytes) + printInt(d.w, int64(valueCap), 10) + } + d.w.Write(closeParenBytes) + d.w.Write(spaceBytes) + } + + // Call Stringer/error interfaces if they exist and the handle methods flag + // is enabled + if !d.cs.DisableMethods { + if (kind != reflect.Invalid) && (kind != reflect.Interface) { + if handled := handleMethods(d.cs, d.w, v); handled { + return + } + } + } + + switch kind { + case reflect.Invalid: + // Do nothing. We should never get here since invalid has already + // been handled above. + + case reflect.Bool: + printBool(d.w, v.Bool()) + + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + printInt(d.w, v.Int(), 10) + + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + printUint(d.w, v.Uint(), 10) + + case reflect.Float32: + printFloat(d.w, v.Float(), 32) + + case reflect.Float64: + printFloat(d.w, v.Float(), 64) + + case reflect.Complex64: + printComplex(d.w, v.Complex(), 32) + + case reflect.Complex128: + printComplex(d.w, v.Complex(), 64) + + case reflect.Slice: + if v.IsNil() { + d.w.Write(nilAngleBytes) + break + } + fallthrough + + case reflect.Array: + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + d.dumpSlice(v) + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.String: + d.w.Write([]byte(strconv.Quote(v.String()))) + + case reflect.Interface: + // The only time we should get here is for nil interfaces due to + // unpackValue calls. + if v.IsNil() { + d.w.Write(nilAngleBytes) + } + + case reflect.Ptr: + // Do nothing. We should never get here since pointers have already + // been handled above. + + case reflect.Map: + // nil maps should be indicated as different than empty maps + if v.IsNil() { + d.w.Write(nilAngleBytes) + break + } + + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + numEntries := v.Len() + keys := v.MapKeys() + if d.cs.SortKeys { + sortValues(keys, d.cs) + } + for i, key := range keys { + d.dump(d.unpackValue(key)) + d.w.Write(colonSpaceBytes) + d.ignoreNextIndent = true + d.dump(d.unpackValue(v.MapIndex(key))) + if i < (numEntries - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.Struct: + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + vt := v.Type() + numFields := v.NumField() + for i := 0; i < numFields; i++ { + d.indent() + vtf := vt.Field(i) + d.w.Write([]byte(vtf.Name)) + d.w.Write(colonSpaceBytes) + d.ignoreNextIndent = true + d.dump(d.unpackValue(v.Field(i))) + if i < (numFields - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.Uintptr: + printHexPtr(d.w, uintptr(v.Uint())) + + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + printHexPtr(d.w, v.Pointer()) + + // There were not any other types at the time this code was written, but + // fall back to letting the default fmt package handle it in case any new + // types are added. + default: + if v.CanInterface() { + fmt.Fprintf(d.w, "%v", v.Interface()) + } else { + fmt.Fprintf(d.w, "%v", v.String()) + } + } +} + +// fdump is a helper function to consolidate the logic from the various public +// methods which take varying writers and config states. +func fdump(cs *ConfigState, w io.Writer, a ...interface{}) { + for _, arg := range a { + if arg == nil { + w.Write(interfaceBytes) + w.Write(spaceBytes) + w.Write(nilAngleBytes) + w.Write(newlineBytes) + continue + } + + d := dumpState{w: w, cs: cs} + d.pointers = make(map[uintptr]int) + d.dump(reflect.ValueOf(arg)) + d.w.Write(newlineBytes) + } +} + +// Fdump formats and displays the passed arguments to io.Writer w. It formats +// exactly the same as Dump. +func Fdump(w io.Writer, a ...interface{}) { + fdump(&Config, w, a...) +} + +// Sdump returns a string with the passed arguments formatted exactly the same +// as Dump. +func Sdump(a ...interface{}) string { + var buf bytes.Buffer + fdump(&Config, &buf, a...) + return buf.String() +} + +/* +Dump displays the passed parameters to standard out with newlines, customizable +indentation, and additional debug information such as complete types and all +pointer addresses used to indirect to the final value. It provides the +following features over the built-in printing facilities provided by the fmt +package: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output + +The configuration options are controlled by an exported package global, +spew.Config. See ConfigState for options documentation. + +See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to +get the formatted result as a string. +*/ +func Dump(a ...interface{}) { + fdump(&Config, os.Stdout, a...) +} diff --git a/vendor/github.com/davecgh/go-spew/spew/format.go b/vendor/github.com/davecgh/go-spew/spew/format.go new file mode 100644 index 0000000..b04edb7 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/format.go @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" +) + +// supportedFlags is a list of all the character flags supported by fmt package. +const supportedFlags = "0-+# " + +// formatState implements the fmt.Formatter interface and contains information +// about the state of a formatting operation. The NewFormatter function can +// be used to get a new Formatter which can be used directly as arguments +// in standard fmt package printing calls. +type formatState struct { + value interface{} + fs fmt.State + depth int + pointers map[uintptr]int + ignoreNextType bool + cs *ConfigState +} + +// buildDefaultFormat recreates the original format string without precision +// and width information to pass in to fmt.Sprintf in the case of an +// unrecognized type. Unless new types are added to the language, this +// function won't ever be called. +func (f *formatState) buildDefaultFormat() (format string) { + buf := bytes.NewBuffer(percentBytes) + + for _, flag := range supportedFlags { + if f.fs.Flag(int(flag)) { + buf.WriteRune(flag) + } + } + + buf.WriteRune('v') + + format = buf.String() + return format +} + +// constructOrigFormat recreates the original format string including precision +// and width information to pass along to the standard fmt package. This allows +// automatic deferral of all format strings this package doesn't support. +func (f *formatState) constructOrigFormat(verb rune) (format string) { + buf := bytes.NewBuffer(percentBytes) + + for _, flag := range supportedFlags { + if f.fs.Flag(int(flag)) { + buf.WriteRune(flag) + } + } + + if width, ok := f.fs.Width(); ok { + buf.WriteString(strconv.Itoa(width)) + } + + if precision, ok := f.fs.Precision(); ok { + buf.Write(precisionBytes) + buf.WriteString(strconv.Itoa(precision)) + } + + buf.WriteRune(verb) + + format = buf.String() + return format +} + +// unpackValue returns values inside of non-nil interfaces when possible and +// ensures that types for values which have been unpacked from an interface +// are displayed when the show types flag is also set. +// This is useful for data types like structs, arrays, slices, and maps which +// can contain varying types packed inside an interface. +func (f *formatState) unpackValue(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Interface { + f.ignoreNextType = false + if !v.IsNil() { + v = v.Elem() + } + } + return v +} + +// formatPtr handles formatting of pointers by indirecting them as necessary. +func (f *formatState) formatPtr(v reflect.Value) { + // Display nil if top level pointer is nil. + showTypes := f.fs.Flag('#') + if v.IsNil() && (!showTypes || f.ignoreNextType) { + f.fs.Write(nilAngleBytes) + return + } + + // Remove pointers at or below the current depth from map used to detect + // circular refs. + for k, depth := range f.pointers { + if depth >= f.depth { + delete(f.pointers, k) + } + } + + // Keep list of all dereferenced pointers to possibly show later. + pointerChain := make([]uintptr, 0) + + // Figure out how many levels of indirection there are by derferencing + // pointers and unpacking interfaces down the chain while detecting circular + // references. + nilFound := false + cycleFound := false + indirects := 0 + ve := v + for ve.Kind() == reflect.Ptr { + if ve.IsNil() { + nilFound = true + break + } + indirects++ + addr := ve.Pointer() + pointerChain = append(pointerChain, addr) + if pd, ok := f.pointers[addr]; ok && pd < f.depth { + cycleFound = true + indirects-- + break + } + f.pointers[addr] = f.depth + + ve = ve.Elem() + if ve.Kind() == reflect.Interface { + if ve.IsNil() { + nilFound = true + break + } + ve = ve.Elem() + } + } + + // Display type or indirection level depending on flags. + if showTypes && !f.ignoreNextType { + f.fs.Write(openParenBytes) + f.fs.Write(bytes.Repeat(asteriskBytes, indirects)) + f.fs.Write([]byte(ve.Type().String())) + f.fs.Write(closeParenBytes) + } else { + if nilFound || cycleFound { + indirects += strings.Count(ve.Type().String(), "*") + } + f.fs.Write(openAngleBytes) + f.fs.Write([]byte(strings.Repeat("*", indirects))) + f.fs.Write(closeAngleBytes) + } + + // Display pointer information depending on flags. + if f.fs.Flag('+') && (len(pointerChain) > 0) { + f.fs.Write(openParenBytes) + for i, addr := range pointerChain { + if i > 0 { + f.fs.Write(pointerChainBytes) + } + printHexPtr(f.fs, addr) + } + f.fs.Write(closeParenBytes) + } + + // Display dereferenced value. + switch { + case nilFound: + f.fs.Write(nilAngleBytes) + + case cycleFound: + f.fs.Write(circularShortBytes) + + default: + f.ignoreNextType = true + f.format(ve) + } +} + +// format is the main workhorse for providing the Formatter interface. It +// uses the passed reflect value to figure out what kind of object we are +// dealing with and formats it appropriately. It is a recursive function, +// however circular data structures are detected and handled properly. +func (f *formatState) format(v reflect.Value) { + // Handle invalid reflect values immediately. + kind := v.Kind() + if kind == reflect.Invalid { + f.fs.Write(invalidAngleBytes) + return + } + + // Handle pointers specially. + if kind == reflect.Ptr { + f.formatPtr(v) + return + } + + // Print type information unless already handled elsewhere. + if !f.ignoreNextType && f.fs.Flag('#') { + f.fs.Write(openParenBytes) + f.fs.Write([]byte(v.Type().String())) + f.fs.Write(closeParenBytes) + } + f.ignoreNextType = false + + // Call Stringer/error interfaces if they exist and the handle methods + // flag is enabled. + if !f.cs.DisableMethods { + if (kind != reflect.Invalid) && (kind != reflect.Interface) { + if handled := handleMethods(f.cs, f.fs, v); handled { + return + } + } + } + + switch kind { + case reflect.Invalid: + // Do nothing. We should never get here since invalid has already + // been handled above. + + case reflect.Bool: + printBool(f.fs, v.Bool()) + + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + printInt(f.fs, v.Int(), 10) + + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + printUint(f.fs, v.Uint(), 10) + + case reflect.Float32: + printFloat(f.fs, v.Float(), 32) + + case reflect.Float64: + printFloat(f.fs, v.Float(), 64) + + case reflect.Complex64: + printComplex(f.fs, v.Complex(), 32) + + case reflect.Complex128: + printComplex(f.fs, v.Complex(), 64) + + case reflect.Slice: + if v.IsNil() { + f.fs.Write(nilAngleBytes) + break + } + fallthrough + + case reflect.Array: + f.fs.Write(openBracketBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + numEntries := v.Len() + for i := 0; i < numEntries; i++ { + if i > 0 { + f.fs.Write(spaceBytes) + } + f.ignoreNextType = true + f.format(f.unpackValue(v.Index(i))) + } + } + f.depth-- + f.fs.Write(closeBracketBytes) + + case reflect.String: + f.fs.Write([]byte(v.String())) + + case reflect.Interface: + // The only time we should get here is for nil interfaces due to + // unpackValue calls. + if v.IsNil() { + f.fs.Write(nilAngleBytes) + } + + case reflect.Ptr: + // Do nothing. We should never get here since pointers have already + // been handled above. + + case reflect.Map: + // nil maps should be indicated as different than empty maps + if v.IsNil() { + f.fs.Write(nilAngleBytes) + break + } + + f.fs.Write(openMapBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + keys := v.MapKeys() + if f.cs.SortKeys { + sortValues(keys, f.cs) + } + for i, key := range keys { + if i > 0 { + f.fs.Write(spaceBytes) + } + f.ignoreNextType = true + f.format(f.unpackValue(key)) + f.fs.Write(colonBytes) + f.ignoreNextType = true + f.format(f.unpackValue(v.MapIndex(key))) + } + } + f.depth-- + f.fs.Write(closeMapBytes) + + case reflect.Struct: + numFields := v.NumField() + f.fs.Write(openBraceBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + vt := v.Type() + for i := 0; i < numFields; i++ { + if i > 0 { + f.fs.Write(spaceBytes) + } + vtf := vt.Field(i) + if f.fs.Flag('+') || f.fs.Flag('#') { + f.fs.Write([]byte(vtf.Name)) + f.fs.Write(colonBytes) + } + f.format(f.unpackValue(v.Field(i))) + } + } + f.depth-- + f.fs.Write(closeBraceBytes) + + case reflect.Uintptr: + printHexPtr(f.fs, uintptr(v.Uint())) + + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + printHexPtr(f.fs, v.Pointer()) + + // There were not any other types at the time this code was written, but + // fall back to letting the default fmt package handle it if any get added. + default: + format := f.buildDefaultFormat() + if v.CanInterface() { + fmt.Fprintf(f.fs, format, v.Interface()) + } else { + fmt.Fprintf(f.fs, format, v.String()) + } + } +} + +// Format satisfies the fmt.Formatter interface. See NewFormatter for usage +// details. +func (f *formatState) Format(fs fmt.State, verb rune) { + f.fs = fs + + // Use standard formatting for verbs that are not v. + if verb != 'v' { + format := f.constructOrigFormat(verb) + fmt.Fprintf(fs, format, f.value) + return + } + + if f.value == nil { + if fs.Flag('#') { + fs.Write(interfaceBytes) + } + fs.Write(nilAngleBytes) + return + } + + f.format(reflect.ValueOf(f.value)) +} + +// newFormatter is a helper function to consolidate the logic from the various +// public methods which take varying config states. +func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter { + fs := &formatState{value: v, cs: cs} + fs.pointers = make(map[uintptr]int) + return fs +} + +/* +NewFormatter returns a custom formatter that satisfies the fmt.Formatter +interface. As a result, it integrates cleanly with standard fmt package +printing functions. The formatter is useful for inline printing of smaller data +types similar to the standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Typically this function shouldn't be called directly. It is much easier to make +use of the custom formatter by calling one of the convenience functions such as +Printf, Println, or Fprintf. +*/ +func NewFormatter(v interface{}) fmt.Formatter { + return newFormatter(&Config, v) +} diff --git a/vendor/github.com/davecgh/go-spew/spew/spew.go b/vendor/github.com/davecgh/go-spew/spew/spew.go new file mode 100644 index 0000000..32c0e33 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/spew.go @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "fmt" + "io" +) + +// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the formatted string as a value that satisfies error. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Errorf(format string, a ...interface{}) (err error) { + return fmt.Errorf(format, convertArgs(a)...) +} + +// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprint(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprint(w, convertArgs(a)...) +} + +// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, format, convertArgs(a)...) +} + +// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it +// passed with a default Formatter interface returned by NewFormatter. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, convertArgs(a)...) +} + +// Print is a wrapper for fmt.Print that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b)) +func Print(a ...interface{}) (n int, err error) { + return fmt.Print(convertArgs(a)...) +} + +// Printf is a wrapper for fmt.Printf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(format, convertArgs(a)...) +} + +// Println is a wrapper for fmt.Println that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b)) +func Println(a ...interface{}) (n int, err error) { + return fmt.Println(convertArgs(a)...) +} + +// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprint(a ...interface{}) string { + return fmt.Sprint(convertArgs(a)...) +} + +// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, convertArgs(a)...) +} + +// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it +// were passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprintln(a ...interface{}) string { + return fmt.Sprintln(convertArgs(a)...) +} + +// convertArgs accepts a slice of arguments and returns a slice of the same +// length with each argument converted to a default spew Formatter interface. +func convertArgs(args []interface{}) (formatters []interface{}) { + formatters = make([]interface{}, len(args)) + for index, arg := range args { + formatters[index] = NewFormatter(arg) + } + return formatters +} diff --git a/vendor/github.com/pmezard/go-difflib/LICENSE b/vendor/github.com/pmezard/go-difflib/LICENSE new file mode 100644 index 0000000..c67dad6 --- /dev/null +++ b/vendor/github.com/pmezard/go-difflib/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013, Patrick Mezard +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + The names of its contributors may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pmezard/go-difflib/difflib/difflib.go b/vendor/github.com/pmezard/go-difflib/difflib/difflib.go new file mode 100644 index 0000000..003e99f --- /dev/null +++ b/vendor/github.com/pmezard/go-difflib/difflib/difflib.go @@ -0,0 +1,772 @@ +// Package difflib is a partial port of Python difflib module. +// +// It provides tools to compare sequences of strings and generate textual diffs. +// +// The following class and functions have been ported: +// +// - SequenceMatcher +// +// - unified_diff +// +// - context_diff +// +// Getting unified diffs was the main goal of the port. Keep in mind this code +// is mostly suitable to output text differences in a human friendly way, there +// are no guarantees generated diffs are consumable by patch(1). +package difflib + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strings" +) + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func calculateRatio(matches, length int) float64 { + if length > 0 { + return 2.0 * float64(matches) / float64(length) + } + return 1.0 +} + +type Match struct { + A int + B int + Size int +} + +type OpCode struct { + Tag byte + I1 int + I2 int + J1 int + J2 int +} + +// SequenceMatcher compares sequence of strings. The basic +// algorithm predates, and is a little fancier than, an algorithm +// published in the late 1980's by Ratcliff and Obershelp under the +// hyperbolic name "gestalt pattern matching". The basic idea is to find +// the longest contiguous matching subsequence that contains no "junk" +// elements (R-O doesn't address junk). The same idea is then applied +// recursively to the pieces of the sequences to the left and to the right +// of the matching subsequence. This does not yield minimal edit +// sequences, but does tend to yield matches that "look right" to people. +// +// SequenceMatcher tries to compute a "human-friendly diff" between two +// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the +// longest *contiguous* & junk-free matching subsequence. That's what +// catches peoples' eyes. The Windows(tm) windiff has another interesting +// notion, pairing up elements that appear uniquely in each sequence. +// That, and the method here, appear to yield more intuitive difference +// reports than does diff. This method appears to be the least vulnerable +// to synching up on blocks of "junk lines", though (like blank lines in +// ordinary text files, or maybe "

" lines in HTML files). That may be +// because this is the only method of the 3 that has a *concept* of +// "junk" . +// +// Timing: Basic R-O is cubic time worst case and quadratic time expected +// case. SequenceMatcher is quadratic time for the worst case and has +// expected-case behavior dependent in a complicated way on how many +// elements the sequences have in common; best case time is linear. +type SequenceMatcher struct { + a []string + b []string + b2j map[string][]int + IsJunk func(string) bool + autoJunk bool + bJunk map[string]struct{} + matchingBlocks []Match + fullBCount map[string]int + bPopular map[string]struct{} + opCodes []OpCode +} + +func NewMatcher(a, b []string) *SequenceMatcher { + m := SequenceMatcher{autoJunk: true} + m.SetSeqs(a, b) + return &m +} + +func NewMatcherWithJunk(a, b []string, autoJunk bool, + isJunk func(string) bool) *SequenceMatcher { + + m := SequenceMatcher{IsJunk: isJunk, autoJunk: autoJunk} + m.SetSeqs(a, b) + return &m +} + +// Set two sequences to be compared. +func (m *SequenceMatcher) SetSeqs(a, b []string) { + m.SetSeq1(a) + m.SetSeq2(b) +} + +// Set the first sequence to be compared. The second sequence to be compared is +// not changed. +// +// SequenceMatcher computes and caches detailed information about the second +// sequence, so if you want to compare one sequence S against many sequences, +// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other +// sequences. +// +// See also SetSeqs() and SetSeq2(). +func (m *SequenceMatcher) SetSeq1(a []string) { + if &a == &m.a { + return + } + m.a = a + m.matchingBlocks = nil + m.opCodes = nil +} + +// Set the second sequence to be compared. The first sequence to be compared is +// not changed. +func (m *SequenceMatcher) SetSeq2(b []string) { + if &b == &m.b { + return + } + m.b = b + m.matchingBlocks = nil + m.opCodes = nil + m.fullBCount = nil + m.chainB() +} + +func (m *SequenceMatcher) chainB() { + // Populate line -> index mapping + b2j := map[string][]int{} + for i, s := range m.b { + indices := b2j[s] + indices = append(indices, i) + b2j[s] = indices + } + + // Purge junk elements + m.bJunk = map[string]struct{}{} + if m.IsJunk != nil { + junk := m.bJunk + for s, _ := range b2j { + if m.IsJunk(s) { + junk[s] = struct{}{} + } + } + for s, _ := range junk { + delete(b2j, s) + } + } + + // Purge remaining popular elements + popular := map[string]struct{}{} + n := len(m.b) + if m.autoJunk && n >= 200 { + ntest := n/100 + 1 + for s, indices := range b2j { + if len(indices) > ntest { + popular[s] = struct{}{} + } + } + for s, _ := range popular { + delete(b2j, s) + } + } + m.bPopular = popular + m.b2j = b2j +} + +func (m *SequenceMatcher) isBJunk(s string) bool { + _, ok := m.bJunk[s] + return ok +} + +// Find longest matching block in a[alo:ahi] and b[blo:bhi]. +// +// If IsJunk is not defined: +// +// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where +// alo <= i <= i+k <= ahi +// blo <= j <= j+k <= bhi +// and for all (i',j',k') meeting those conditions, +// k >= k' +// i <= i' +// and if i == i', j <= j' +// +// In other words, of all maximal matching blocks, return one that +// starts earliest in a, and of all those maximal matching blocks that +// start earliest in a, return the one that starts earliest in b. +// +// If IsJunk is defined, first the longest matching block is +// determined as above, but with the additional restriction that no +// junk element appears in the block. Then that block is extended as +// far as possible by matching (only) junk elements on both sides. So +// the resulting block never matches on junk except as identical junk +// happens to be adjacent to an "interesting" match. +// +// If no blocks match, return (alo, blo, 0). +func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match { + // CAUTION: stripping common prefix or suffix would be incorrect. + // E.g., + // ab + // acab + // Longest matching block is "ab", but if common prefix is + // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so + // strip, so ends up claiming that ab is changed to acab by + // inserting "ca" in the middle. That's minimal but unintuitive: + // "it's obvious" that someone inserted "ac" at the front. + // Windiff ends up at the same place as diff, but by pairing up + // the unique 'b's and then matching the first two 'a's. + besti, bestj, bestsize := alo, blo, 0 + + // find longest junk-free match + // during an iteration of the loop, j2len[j] = length of longest + // junk-free match ending with a[i-1] and b[j] + j2len := map[int]int{} + for i := alo; i != ahi; i++ { + // look at all instances of a[i] in b; note that because + // b2j has no junk keys, the loop is skipped if a[i] is junk + newj2len := map[int]int{} + for _, j := range m.b2j[m.a[i]] { + // a[i] matches b[j] + if j < blo { + continue + } + if j >= bhi { + break + } + k := j2len[j-1] + 1 + newj2len[j] = k + if k > bestsize { + besti, bestj, bestsize = i-k+1, j-k+1, k + } + } + j2len = newj2len + } + + // Extend the best by non-junk elements on each end. In particular, + // "popular" non-junk elements aren't in b2j, which greatly speeds + // the inner loop above, but also means "the best" match so far + // doesn't contain any junk *or* popular non-junk elements. + for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + !m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + // Now that we have a wholly interesting match (albeit possibly + // empty!), we may as well suck up the matching junk on each + // side of it too. Can't think of a good reason not to, and it + // saves post-processing the (possibly considerable) expense of + // figuring out what to do with it. In the case of an empty + // interesting match, this is clearly the right thing to do, + // because no other kind of match is possible in the regions. + for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + return Match{A: besti, B: bestj, Size: bestsize} +} + +// Return list of triples describing matching subsequences. +// +// Each triple is of the form (i, j, n), and means that +// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in +// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are +// adjacent triples in the list, and the second is not the last triple in the +// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe +// adjacent equal blocks. +// +// The last triple is a dummy, (len(a), len(b), 0), and is the only +// triple with n==0. +func (m *SequenceMatcher) GetMatchingBlocks() []Match { + if m.matchingBlocks != nil { + return m.matchingBlocks + } + + var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match + matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match { + match := m.findLongestMatch(alo, ahi, blo, bhi) + i, j, k := match.A, match.B, match.Size + if match.Size > 0 { + if alo < i && blo < j { + matched = matchBlocks(alo, i, blo, j, matched) + } + matched = append(matched, match) + if i+k < ahi && j+k < bhi { + matched = matchBlocks(i+k, ahi, j+k, bhi, matched) + } + } + return matched + } + matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) + + // It's possible that we have adjacent equal blocks in the + // matching_blocks list now. + nonAdjacent := []Match{} + i1, j1, k1 := 0, 0, 0 + for _, b := range matched { + // Is this block adjacent to i1, j1, k1? + i2, j2, k2 := b.A, b.B, b.Size + if i1+k1 == i2 && j1+k1 == j2 { + // Yes, so collapse them -- this just increases the length of + // the first block by the length of the second, and the first + // block so lengthened remains the block to compare against. + k1 += k2 + } else { + // Not adjacent. Remember the first block (k1==0 means it's + // the dummy we started with), and make the second block the + // new block to compare against. + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + i1, j1, k1 = i2, j2, k2 + } + } + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + + nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0}) + m.matchingBlocks = nonAdjacent + return m.matchingBlocks +} + +// Return list of 5-tuples describing how to turn a into b. +// +// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple +// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the +// tuple preceding it, and likewise for j1 == the previous j2. +// +// The tags are characters, with these meanings: +// +// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2] +// +// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case. +// +// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. +// +// 'e' (equal): a[i1:i2] == b[j1:j2] +func (m *SequenceMatcher) GetOpCodes() []OpCode { + if m.opCodes != nil { + return m.opCodes + } + i, j := 0, 0 + matching := m.GetMatchingBlocks() + opCodes := make([]OpCode, 0, len(matching)) + for _, m := range matching { + // invariant: we've pumped out correct diffs to change + // a[:i] into b[:j], and the next matching block is + // a[ai:ai+size] == b[bj:bj+size]. So we need to pump + // out a diff to change a[i:ai] into b[j:bj], pump out + // the matching block, and move (i,j) beyond the match + ai, bj, size := m.A, m.B, m.Size + tag := byte(0) + if i < ai && j < bj { + tag = 'r' + } else if i < ai { + tag = 'd' + } else if j < bj { + tag = 'i' + } + if tag > 0 { + opCodes = append(opCodes, OpCode{tag, i, ai, j, bj}) + } + i, j = ai+size, bj+size + // the list of matching blocks is terminated by a + // sentinel with size 0 + if size > 0 { + opCodes = append(opCodes, OpCode{'e', ai, i, bj, j}) + } + } + m.opCodes = opCodes + return m.opCodes +} + +// Isolate change clusters by eliminating ranges with no changes. +// +// Return a generator of groups with up to n lines of context. +// Each group is in the same format as returned by GetOpCodes(). +func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { + if n < 0 { + n = 3 + } + codes := m.GetOpCodes() + if len(codes) == 0 { + codes = []OpCode{OpCode{'e', 0, 1, 0, 1}} + } + // Fixup leading and trailing groups if they show no changes. + if codes[0].Tag == 'e' { + c := codes[0] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} + } + if codes[len(codes)-1].Tag == 'e' { + c := codes[len(codes)-1] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} + } + nn := n + n + groups := [][]OpCode{} + group := []OpCode{} + for _, c := range codes { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + // End the current group and start a new one whenever + // there is a large range with no changes. + if c.Tag == 'e' && i2-i1 > nn { + group = append(group, OpCode{c.Tag, i1, min(i2, i1+n), + j1, min(j2, j1+n)}) + groups = append(groups, group) + group = []OpCode{} + i1, j1 = max(i1, i2-n), max(j1, j2-n) + } + group = append(group, OpCode{c.Tag, i1, i2, j1, j2}) + } + if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { + groups = append(groups, group) + } + return groups +} + +// Return a measure of the sequences' similarity (float in [0,1]). +// +// Where T is the total number of elements in both sequences, and +// M is the number of matches, this is 2.0*M / T. +// Note that this is 1 if the sequences are identical, and 0 if +// they have nothing in common. +// +// .Ratio() is expensive to compute if you haven't already computed +// .GetMatchingBlocks() or .GetOpCodes(), in which case you may +// want to try .QuickRatio() or .RealQuickRation() first to get an +// upper bound. +func (m *SequenceMatcher) Ratio() float64 { + matches := 0 + for _, m := range m.GetMatchingBlocks() { + matches += m.Size + } + return calculateRatio(matches, len(m.a)+len(m.b)) +} + +// Return an upper bound on ratio() relatively quickly. +// +// This isn't defined beyond that it is an upper bound on .Ratio(), and +// is faster to compute. +func (m *SequenceMatcher) QuickRatio() float64 { + // viewing a and b as multisets, set matches to the cardinality + // of their intersection; this counts the number of matches + // without regard to order, so is clearly an upper bound + if m.fullBCount == nil { + m.fullBCount = map[string]int{} + for _, s := range m.b { + m.fullBCount[s] = m.fullBCount[s] + 1 + } + } + + // avail[x] is the number of times x appears in 'b' less the + // number of times we've seen it in 'a' so far ... kinda + avail := map[string]int{} + matches := 0 + for _, s := range m.a { + n, ok := avail[s] + if !ok { + n = m.fullBCount[s] + } + avail[s] = n - 1 + if n > 0 { + matches += 1 + } + } + return calculateRatio(matches, len(m.a)+len(m.b)) +} + +// Return an upper bound on ratio() very quickly. +// +// This isn't defined beyond that it is an upper bound on .Ratio(), and +// is faster to compute than either .Ratio() or .QuickRatio(). +func (m *SequenceMatcher) RealQuickRatio() float64 { + la, lb := len(m.a), len(m.b) + return calculateRatio(min(la, lb), la+lb) +} + +// Convert range to the "ed" format +func formatRangeUnified(start, stop int) string { + // Per the diff spec at http://www.unix.org/single_unix_specification/ + beginning := start + 1 // lines start numbering with one + length := stop - start + if length == 1 { + return fmt.Sprintf("%d", beginning) + } + if length == 0 { + beginning -= 1 // empty ranges begin at line just before the range + } + return fmt.Sprintf("%d,%d", beginning, length) +} + +// Unified diff parameters +type UnifiedDiff struct { + A []string // First sequence lines + FromFile string // First file name + FromDate string // First file time + B []string // Second sequence lines + ToFile string // Second file name + ToDate string // Second file time + Eol string // Headers end of line, defaults to LF + Context int // Number of context lines +} + +// Compare two sequences of lines; generate the delta as a unified diff. +// +// Unified diffs are a compact way of showing line changes and a few +// lines of context. The number of context lines is set by 'n' which +// defaults to three. +// +// By default, the diff control lines (those with ---, +++, or @@) are +// created with a trailing newline. This is helpful so that inputs +// created from file.readlines() result in diffs that are suitable for +// file.writelines() since both the inputs and outputs have trailing +// newlines. +// +// For inputs that do not have trailing newlines, set the lineterm +// argument to "" so that the output will be uniformly newline free. +// +// The unidiff format normally has a header for filenames and modification +// times. Any or all of these may be specified using strings for +// 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. +// The modification times are normally expressed in the ISO 8601 format. +func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error { + buf := bufio.NewWriter(writer) + defer buf.Flush() + wf := func(format string, args ...interface{}) error { + _, err := buf.WriteString(fmt.Sprintf(format, args...)) + return err + } + ws := func(s string) error { + _, err := buf.WriteString(s) + return err + } + + if len(diff.Eol) == 0 { + diff.Eol = "\n" + } + + started := false + m := NewMatcher(diff.A, diff.B) + for _, g := range m.GetGroupedOpCodes(diff.Context) { + if !started { + started = true + fromDate := "" + if len(diff.FromDate) > 0 { + fromDate = "\t" + diff.FromDate + } + toDate := "" + if len(diff.ToDate) > 0 { + toDate = "\t" + diff.ToDate + } + if diff.FromFile != "" || diff.ToFile != "" { + err := wf("--- %s%s%s", diff.FromFile, fromDate, diff.Eol) + if err != nil { + return err + } + err = wf("+++ %s%s%s", diff.ToFile, toDate, diff.Eol) + if err != nil { + return err + } + } + } + first, last := g[0], g[len(g)-1] + range1 := formatRangeUnified(first.I1, last.I2) + range2 := formatRangeUnified(first.J1, last.J2) + if err := wf("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil { + return err + } + for _, c := range g { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + if c.Tag == 'e' { + for _, line := range diff.A[i1:i2] { + if err := ws(" " + line); err != nil { + return err + } + } + continue + } + if c.Tag == 'r' || c.Tag == 'd' { + for _, line := range diff.A[i1:i2] { + if err := ws("-" + line); err != nil { + return err + } + } + } + if c.Tag == 'r' || c.Tag == 'i' { + for _, line := range diff.B[j1:j2] { + if err := ws("+" + line); err != nil { + return err + } + } + } + } + } + return nil +} + +// Like WriteUnifiedDiff but returns the diff a string. +func GetUnifiedDiffString(diff UnifiedDiff) (string, error) { + w := &bytes.Buffer{} + err := WriteUnifiedDiff(w, diff) + return string(w.Bytes()), err +} + +// Convert range to the "ed" format. +func formatRangeContext(start, stop int) string { + // Per the diff spec at http://www.unix.org/single_unix_specification/ + beginning := start + 1 // lines start numbering with one + length := stop - start + if length == 0 { + beginning -= 1 // empty ranges begin at line just before the range + } + if length <= 1 { + return fmt.Sprintf("%d", beginning) + } + return fmt.Sprintf("%d,%d", beginning, beginning+length-1) +} + +type ContextDiff UnifiedDiff + +// Compare two sequences of lines; generate the delta as a context diff. +// +// Context diffs are a compact way of showing line changes and a few +// lines of context. The number of context lines is set by diff.Context +// which defaults to three. +// +// By default, the diff control lines (those with *** or ---) are +// created with a trailing newline. +// +// For inputs that do not have trailing newlines, set the diff.Eol +// argument to "" so that the output will be uniformly newline free. +// +// The context diff format normally has a header for filenames and +// modification times. Any or all of these may be specified using +// strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate. +// The modification times are normally expressed in the ISO 8601 format. +// If not specified, the strings default to blanks. +func WriteContextDiff(writer io.Writer, diff ContextDiff) error { + buf := bufio.NewWriter(writer) + defer buf.Flush() + var diffErr error + wf := func(format string, args ...interface{}) { + _, err := buf.WriteString(fmt.Sprintf(format, args...)) + if diffErr == nil && err != nil { + diffErr = err + } + } + ws := func(s string) { + _, err := buf.WriteString(s) + if diffErr == nil && err != nil { + diffErr = err + } + } + + if len(diff.Eol) == 0 { + diff.Eol = "\n" + } + + prefix := map[byte]string{ + 'i': "+ ", + 'd': "- ", + 'r': "! ", + 'e': " ", + } + + started := false + m := NewMatcher(diff.A, diff.B) + for _, g := range m.GetGroupedOpCodes(diff.Context) { + if !started { + started = true + fromDate := "" + if len(diff.FromDate) > 0 { + fromDate = "\t" + diff.FromDate + } + toDate := "" + if len(diff.ToDate) > 0 { + toDate = "\t" + diff.ToDate + } + if diff.FromFile != "" || diff.ToFile != "" { + wf("*** %s%s%s", diff.FromFile, fromDate, diff.Eol) + wf("--- %s%s%s", diff.ToFile, toDate, diff.Eol) + } + } + + first, last := g[0], g[len(g)-1] + ws("***************" + diff.Eol) + + range1 := formatRangeContext(first.I1, last.I2) + wf("*** %s ****%s", range1, diff.Eol) + for _, c := range g { + if c.Tag == 'r' || c.Tag == 'd' { + for _, cc := range g { + if cc.Tag == 'i' { + continue + } + for _, line := range diff.A[cc.I1:cc.I2] { + ws(prefix[cc.Tag] + line) + } + } + break + } + } + + range2 := formatRangeContext(first.J1, last.J2) + wf("--- %s ----%s", range2, diff.Eol) + for _, c := range g { + if c.Tag == 'r' || c.Tag == 'i' { + for _, cc := range g { + if cc.Tag == 'd' { + continue + } + for _, line := range diff.B[cc.J1:cc.J2] { + ws(prefix[cc.Tag] + line) + } + } + break + } + } + } + return diffErr +} + +// Like WriteContextDiff but returns the diff a string. +func GetContextDiffString(diff ContextDiff) (string, error) { + w := &bytes.Buffer{} + err := WriteContextDiff(w, diff) + return string(w.Bytes()), err +} + +// Split a string on "\n" while preserving them. The output can be used +// as input for UnifiedDiff and ContextDiff structures. +func SplitLines(s string) []string { + lines := strings.SplitAfter(s, "\n") + lines[len(lines)-1] += "\n" + return lines +} diff --git a/vendor/github.com/stretchr/testify/LICENSE b/vendor/github.com/stretchr/testify/LICENSE new file mode 100644 index 0000000..473b670 --- /dev/null +++ b/vendor/github.com/stretchr/testify/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell + +Please consider promoting this project if you find it useful. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT +OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go new file mode 100644 index 0000000..aa1c2b9 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go @@ -0,0 +1,484 @@ +/* +* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen +* THIS FILE MUST NOT BE EDITED BY HAND + */ + +package assert + +import ( + http "net/http" + url "net/url" + time "time" +) + +// Conditionf uses a Comparison to assert a complex condition. +func Conditionf(t TestingT, comp Comparison, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Condition(t, comp, append([]interface{}{msg}, args...)...) +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Containsf(t, "Hello World", "World", "error message %s", "formatted") +// assert.Containsf(t, ["Hello", "World"], "World", "error message %s", "formatted") +// assert.Containsf(t, {"Hello": "World"}, "Hello", "error message %s", "formatted") +func Containsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Contains(t, s, contains, append([]interface{}{msg}, args...)...) +} + +// DirExistsf checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func DirExistsf(t TestingT, path string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return DirExists(t, path, append([]interface{}{msg}, args...)...) +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// assert.ElementsMatchf(t, [1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return ElementsMatch(t, listA, listB, append([]interface{}{msg}, args...)...) +} + +// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// assert.Emptyf(t, obj, "error message %s", "formatted") +func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Empty(t, object, append([]interface{}{msg}, args...)...) +} + +// Equalf asserts that two objects are equal. +// +// assert.Equalf(t, 123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Equal(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// assert.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted") +func EqualErrorf(t TestingT, theError error, errString string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return EqualError(t, theError, errString, append([]interface{}{msg}, args...)...) +} + +// EqualValuesf asserts that two objects are equal or convertable to the same types +// and equal. +// +// assert.EqualValuesf(t, uint32(123, "error message %s", "formatted"), int32(123)) +func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return EqualValues(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if assert.Errorf(t, err, "error message %s", "formatted") { +// assert.Equal(t, expectedErrorf, err) +// } +func Errorf(t TestingT, err error, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Error(t, err, append([]interface{}{msg}, args...)...) +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// assert.Exactlyf(t, int32(123, "error message %s", "formatted"), int64(123)) +func Exactlyf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Exactly(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Failf reports a failure through +func Failf(t TestingT, failureMessage string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Fail(t, failureMessage, append([]interface{}{msg}, args...)...) +} + +// FailNowf fails test +func FailNowf(t TestingT, failureMessage string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return FailNow(t, failureMessage, append([]interface{}{msg}, args...)...) +} + +// Falsef asserts that the specified value is false. +// +// assert.Falsef(t, myBool, "error message %s", "formatted") +func Falsef(t TestingT, value bool, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return False(t, value, append([]interface{}{msg}, args...)...) +} + +// FileExistsf checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func FileExistsf(t TestingT, path string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return FileExists(t, path, append([]interface{}{msg}, args...)...) +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPBodyContains(t, handler, method, url, values, str, append([]interface{}{msg}, args...)...) +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPBodyNotContains(t, handler, method, url, values, str, append([]interface{}{msg}, args...)...) +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// assert.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPError(t, handler, method, url, values, append([]interface{}{msg}, args...)...) +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPRedirect(t, handler, method, url, values, append([]interface{}{msg}, args...)...) +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPSuccess(t, handler, method, url, values, append([]interface{}{msg}, args...)...) +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// assert.Implementsf(t, (*MyInterface, "error message %s", "formatted")(nil), new(MyObject)) +func Implementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Implements(t, interfaceObject, object, append([]interface{}{msg}, args...)...) +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// assert.InDeltaf(t, math.Pi, (22 / 7.0, "error message %s", "formatted"), 0.01) +func InDeltaf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InDelta(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValuesf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InDeltaMapValues(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func InDeltaSlicef(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InDeltaSlice(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func InEpsilonf(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InEpsilon(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...) +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InEpsilonSlice(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...) +} + +// IsTypef asserts that the specified objects are of the same type. +func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return IsType(t, expectedType, object, append([]interface{}{msg}, args...)...) +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// assert.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func JSONEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return JSONEq(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// assert.Lenf(t, mySlice, 3, "error message %s", "formatted") +func Lenf(t TestingT, object interface{}, length int, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Len(t, object, length, append([]interface{}{msg}, args...)...) +} + +// Nilf asserts that the specified object is nil. +// +// assert.Nilf(t, err, "error message %s", "formatted") +func Nilf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Nil(t, object, append([]interface{}{msg}, args...)...) +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoErrorf(t, err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func NoErrorf(t TestingT, err error, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NoError(t, err, append([]interface{}{msg}, args...)...) +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContainsf(t, "Hello World", "Earth", "error message %s", "formatted") +// assert.NotContainsf(t, ["Hello", "World"], "Earth", "error message %s", "formatted") +// assert.NotContainsf(t, {"Hello": "World"}, "Earth", "error message %s", "formatted") +func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotContains(t, s, contains, append([]interface{}{msg}, args...)...) +} + +// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if assert.NotEmptyf(t, obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotEmpty(t, object, append([]interface{}{msg}, args...)...) +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// assert.NotEqualf(t, obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotEqual(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// NotNilf asserts that the specified object is not nil. +// +// assert.NotNilf(t, err, "error message %s", "formatted") +func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotNil(t, object, append([]interface{}{msg}, args...)...) +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanicsf(t, func(){ RemainCalm() }, "error message %s", "formatted") +func NotPanicsf(t TestingT, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotPanics(t, f, append([]interface{}{msg}, args...)...) +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// assert.NotRegexpf(t, regexp.MustCompile("starts", "error message %s", "formatted"), "it's starting") +// assert.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted") +func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotRegexp(t, rx, str, append([]interface{}{msg}, args...)...) +} + +// NotSubsetf asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// assert.NotSubsetf(t, [1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]", "error message %s", "formatted") +func NotSubsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotSubset(t, list, subset, append([]interface{}{msg}, args...)...) +} + +// NotZerof asserts that i is not the zero value for its type. +func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotZero(t, i, append([]interface{}{msg}, args...)...) +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") +func Panicsf(t TestingT, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Panics(t, f, append([]interface{}{msg}, args...)...) +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// assert.PanicsWithValuef(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func PanicsWithValuef(t TestingT, expected interface{}, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return PanicsWithValue(t, expected, f, append([]interface{}{msg}, args...)...) +} + +// Regexpf asserts that a specified regexp matches a string. +// +// assert.Regexpf(t, regexp.MustCompile("start", "error message %s", "formatted"), "it's starting") +// assert.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted") +func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Regexp(t, rx, str, append([]interface{}{msg}, args...)...) +} + +// Subsetf asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// assert.Subsetf(t, [1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]", "error message %s", "formatted") +func Subsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Subset(t, list, subset, append([]interface{}{msg}, args...)...) +} + +// Truef asserts that the specified value is true. +// +// assert.Truef(t, myBool, "error message %s", "formatted") +func Truef(t TestingT, value bool, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return True(t, value, append([]interface{}{msg}, args...)...) +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// assert.WithinDurationf(t, time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return WithinDuration(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// Zerof asserts that i is the zero value for its type. +func Zerof(t TestingT, i interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Zero(t, i, append([]interface{}{msg}, args...)...) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go.tmpl b/vendor/github.com/stretchr/testify/assert/assertion_format.go.tmpl new file mode 100644 index 0000000..d2bb0b8 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go.tmpl @@ -0,0 +1,5 @@ +{{.CommentFormat}} +func {{.DocInfo.Name}}f(t TestingT, {{.ParamsFormat}}) bool { + if h, ok := t.(tHelper); ok { h.Helper() } + return {{.DocInfo.Name}}(t, {{.ForwardedParamsFormat}}) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go new file mode 100644 index 0000000..de39f79 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -0,0 +1,956 @@ +/* +* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen +* THIS FILE MUST NOT BE EDITED BY HAND + */ + +package assert + +import ( + http "net/http" + url "net/url" + time "time" +) + +// Condition uses a Comparison to assert a complex condition. +func (a *Assertions) Condition(comp Comparison, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Condition(a.t, comp, msgAndArgs...) +} + +// Conditionf uses a Comparison to assert a complex condition. +func (a *Assertions) Conditionf(comp Comparison, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Conditionf(a.t, comp, msg, args...) +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Contains("Hello World", "World") +// a.Contains(["Hello", "World"], "World") +// a.Contains({"Hello": "World"}, "Hello") +func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Contains(a.t, s, contains, msgAndArgs...) +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Containsf("Hello World", "World", "error message %s", "formatted") +// a.Containsf(["Hello", "World"], "World", "error message %s", "formatted") +// a.Containsf({"Hello": "World"}, "Hello", "error message %s", "formatted") +func (a *Assertions) Containsf(s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Containsf(a.t, s, contains, msg, args...) +} + +// DirExists checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExists(path string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return DirExists(a.t, path, msgAndArgs...) +} + +// DirExistsf checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExistsf(path string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return DirExistsf(a.t, path, msg, args...) +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// a.ElementsMatch([1, 3, 2, 3], [1, 3, 3, 2]) +func (a *Assertions) ElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// a.ElementsMatchf([1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ElementsMatchf(a.t, listA, listB, msg, args...) +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Empty(obj) +func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Empty(a.t, object, msgAndArgs...) +} + +// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Emptyf(obj, "error message %s", "formatted") +func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Emptyf(a.t, object, msg, args...) +} + +// Equal asserts that two objects are equal. +// +// a.Equal(123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Equal(a.t, expected, actual, msgAndArgs...) +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualError(err, expectedErrorString) +func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualError(a.t, theError, errString, msgAndArgs...) +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualErrorf(err, expectedErrorString, "error message %s", "formatted") +func (a *Assertions) EqualErrorf(theError error, errString string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualErrorf(a.t, theError, errString, msg, args...) +} + +// EqualValues asserts that two objects are equal or convertable to the same types +// and equal. +// +// a.EqualValues(uint32(123), int32(123)) +func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualValues(a.t, expected, actual, msgAndArgs...) +} + +// EqualValuesf asserts that two objects are equal or convertable to the same types +// and equal. +// +// a.EqualValuesf(uint32(123, "error message %s", "formatted"), int32(123)) +func (a *Assertions) EqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualValuesf(a.t, expected, actual, msg, args...) +} + +// Equalf asserts that two objects are equal. +// +// a.Equalf(123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Equalf(a.t, expected, actual, msg, args...) +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Error(err) { +// assert.Equal(t, expectedError, err) +// } +func (a *Assertions) Error(err error, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Error(a.t, err, msgAndArgs...) +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Errorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedErrorf, err) +// } +func (a *Assertions) Errorf(err error, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Errorf(a.t, err, msg, args...) +} + +// Exactly asserts that two objects are equal in value and type. +// +// a.Exactly(int32(123), int64(123)) +func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Exactly(a.t, expected, actual, msgAndArgs...) +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// a.Exactlyf(int32(123, "error message %s", "formatted"), int64(123)) +func (a *Assertions) Exactlyf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Exactlyf(a.t, expected, actual, msg, args...) +} + +// Fail reports a failure through +func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Fail(a.t, failureMessage, msgAndArgs...) +} + +// FailNow fails test +func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FailNow(a.t, failureMessage, msgAndArgs...) +} + +// FailNowf fails test +func (a *Assertions) FailNowf(failureMessage string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FailNowf(a.t, failureMessage, msg, args...) +} + +// Failf reports a failure through +func (a *Assertions) Failf(failureMessage string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Failf(a.t, failureMessage, msg, args...) +} + +// False asserts that the specified value is false. +// +// a.False(myBool) +func (a *Assertions) False(value bool, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return False(a.t, value, msgAndArgs...) +} + +// Falsef asserts that the specified value is false. +// +// a.Falsef(myBool, "error message %s", "formatted") +func (a *Assertions) Falsef(value bool, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Falsef(a.t, value, msg, args...) +} + +// FileExists checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExists(path string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FileExists(a.t, path, msgAndArgs...) +} + +// FileExistsf checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExistsf(path string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FileExistsf(a.t, path, msg, args...) +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyNotContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyNotContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPError(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// a.HTTPErrorf(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func (a *Assertions) HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPErrorf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPRedirect(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirectf(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func (a *Assertions) HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPRedirectf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPSuccess(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// a.HTTPSuccessf(myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPSuccessf(a.t, handler, method, url, values, msg, args...) +} + +// Implements asserts that an object is implemented by the specified interface. +// +// a.Implements((*MyInterface)(nil), new(MyObject)) +func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Implements(a.t, interfaceObject, object, msgAndArgs...) +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// a.Implementsf((*MyInterface, "error message %s", "formatted")(nil), new(MyObject)) +func (a *Assertions) Implementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Implementsf(a.t, interfaceObject, object, msg, args...) +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// a.InDelta(math.Pi, (22 / 7.0), 0.01) +func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDelta(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValues(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaMapValues(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValuesf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaMapValuesf(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlicef(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaSlicef(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// a.InDeltaf(math.Pi, (22 / 7.0, "error message %s", "formatted"), 0.01) +func (a *Assertions) InDeltaf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaf(a.t, expected, actual, delta, msg, args...) +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilonSlice(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlicef(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilonSlicef(a.t, expected, actual, epsilon, msg, args...) +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilonf(a.t, expected, actual, epsilon, msg, args...) +} + +// IsType asserts that the specified objects are of the same type. +func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsType(a.t, expectedType, object, msgAndArgs...) +} + +// IsTypef asserts that the specified objects are of the same type. +func (a *Assertions) IsTypef(expectedType interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsTypef(a.t, expectedType, object, msg, args...) +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return JSONEq(a.t, expected, actual, msgAndArgs...) +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// a.JSONEqf(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func (a *Assertions) JSONEqf(expected string, actual string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return JSONEqf(a.t, expected, actual, msg, args...) +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// a.Len(mySlice, 3) +func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Len(a.t, object, length, msgAndArgs...) +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// a.Lenf(mySlice, 3, "error message %s", "formatted") +func (a *Assertions) Lenf(object interface{}, length int, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Lenf(a.t, object, length, msg, args...) +} + +// Nil asserts that the specified object is nil. +// +// a.Nil(err) +func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Nil(a.t, object, msgAndArgs...) +} + +// Nilf asserts that the specified object is nil. +// +// a.Nilf(err, "error message %s", "formatted") +func (a *Assertions) Nilf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Nilf(a.t, object, msg, args...) +} + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoError(err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NoError(a.t, err, msgAndArgs...) +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoErrorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoErrorf(err error, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NoErrorf(a.t, err, msg, args...) +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContains("Hello World", "Earth") +// a.NotContains(["Hello", "World"], "Earth") +// a.NotContains({"Hello": "World"}, "Earth") +func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotContains(a.t, s, contains, msgAndArgs...) +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContainsf("Hello World", "Earth", "error message %s", "formatted") +// a.NotContainsf(["Hello", "World"], "Earth", "error message %s", "formatted") +// a.NotContainsf({"Hello": "World"}, "Earth", "error message %s", "formatted") +func (a *Assertions) NotContainsf(s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotContainsf(a.t, s, contains, msg, args...) +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmpty(obj) { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEmpty(a.t, object, msgAndArgs...) +} + +// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmptyf(obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmptyf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEmptyf(a.t, object, msg, args...) +} + +// NotEqual asserts that the specified values are NOT equal. +// +// a.NotEqual(obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEqual(a.t, expected, actual, msgAndArgs...) +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// a.NotEqualf(obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEqualf(a.t, expected, actual, msg, args...) +} + +// NotNil asserts that the specified object is not nil. +// +// a.NotNil(err) +func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotNil(a.t, object, msgAndArgs...) +} + +// NotNilf asserts that the specified object is not nil. +// +// a.NotNilf(err, "error message %s", "formatted") +func (a *Assertions) NotNilf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotNilf(a.t, object, msg, args...) +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanics(func(){ RemainCalm() }) +func (a *Assertions) NotPanics(f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotPanics(a.t, f, msgAndArgs...) +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanicsf(func(){ RemainCalm() }, "error message %s", "formatted") +func (a *Assertions) NotPanicsf(f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotPanicsf(a.t, f, msg, args...) +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// a.NotRegexp(regexp.MustCompile("starts"), "it's starting") +// a.NotRegexp("^start", "it's not starting") +func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotRegexp(a.t, rx, str, msgAndArgs...) +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// a.NotRegexpf(regexp.MustCompile("starts", "error message %s", "formatted"), "it's starting") +// a.NotRegexpf("^start", "it's not starting", "error message %s", "formatted") +func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotRegexpf(a.t, rx, str, msg, args...) +} + +// NotSubset asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// a.NotSubset([1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]") +func (a *Assertions) NotSubset(list interface{}, subset interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotSubset(a.t, list, subset, msgAndArgs...) +} + +// NotSubsetf asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// a.NotSubsetf([1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]", "error message %s", "formatted") +func (a *Assertions) NotSubsetf(list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotSubsetf(a.t, list, subset, msg, args...) +} + +// NotZero asserts that i is not the zero value for its type. +func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotZero(a.t, i, msgAndArgs...) +} + +// NotZerof asserts that i is not the zero value for its type. +func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotZerof(a.t, i, msg, args...) +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panics(func(){ GoCrazy() }) +func (a *Assertions) Panics(f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Panics(a.t, f, msgAndArgs...) +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValue("crazy error", func(){ GoCrazy() }) +func (a *Assertions) PanicsWithValue(expected interface{}, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return PanicsWithValue(a.t, expected, f, msgAndArgs...) +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValuef("crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) PanicsWithValuef(expected interface{}, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return PanicsWithValuef(a.t, expected, f, msg, args...) +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panicsf(func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) Panicsf(f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Panicsf(a.t, f, msg, args...) +} + +// Regexp asserts that a specified regexp matches a string. +// +// a.Regexp(regexp.MustCompile("start"), "it's starting") +// a.Regexp("start...$", "it's not starting") +func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Regexp(a.t, rx, str, msgAndArgs...) +} + +// Regexpf asserts that a specified regexp matches a string. +// +// a.Regexpf(regexp.MustCompile("start", "error message %s", "formatted"), "it's starting") +// a.Regexpf("start...$", "it's not starting", "error message %s", "formatted") +func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Regexpf(a.t, rx, str, msg, args...) +} + +// Subset asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// a.Subset([1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]") +func (a *Assertions) Subset(list interface{}, subset interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Subset(a.t, list, subset, msgAndArgs...) +} + +// Subsetf asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// a.Subsetf([1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]", "error message %s", "formatted") +func (a *Assertions) Subsetf(list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Subsetf(a.t, list, subset, msg, args...) +} + +// True asserts that the specified value is true. +// +// a.True(myBool) +func (a *Assertions) True(value bool, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return True(a.t, value, msgAndArgs...) +} + +// Truef asserts that the specified value is true. +// +// a.Truef(myBool, "error message %s", "formatted") +func (a *Assertions) Truef(value bool, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Truef(a.t, value, msg, args...) +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// a.WithinDuration(time.Now(), time.Now(), 10*time.Second) +func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinDuration(a.t, expected, actual, delta, msgAndArgs...) +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// a.WithinDurationf(time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinDurationf(a.t, expected, actual, delta, msg, args...) +} + +// Zero asserts that i is the zero value for its type. +func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Zero(a.t, i, msgAndArgs...) +} + +// Zerof asserts that i is the zero value for its type. +func (a *Assertions) Zerof(i interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Zerof(a.t, i, msg, args...) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl b/vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl new file mode 100644 index 0000000..188bb9e --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl @@ -0,0 +1,5 @@ +{{.CommentWithoutT "a"}} +func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) bool { + if h, ok := a.t.(tHelper); ok { h.Helper() } + return {{.DocInfo.Name}}(a.t, {{.ForwardedParams}}) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go new file mode 100644 index 0000000..5bdec56 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -0,0 +1,1394 @@ +package assert + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "math" + "os" + "reflect" + "regexp" + "runtime" + "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/davecgh/go-spew/spew" + "github.com/pmezard/go-difflib/difflib" +) + +//go:generate go run ../_codegen/main.go -output-package=assert -template=assertion_format.go.tmpl + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + Errorf(format string, args ...interface{}) +} + +// ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful +// for table driven tests. +type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) bool + +// ValueAssertionFunc is a common function prototype when validating a single value. Can be useful +// for table driven tests. +type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) bool + +// BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful +// for table driven tests. +type BoolAssertionFunc func(TestingT, bool, ...interface{}) bool + +// ValuesAssertionFunc is a common function prototype when validating an error value. Can be useful +// for table driven tests. +type ErrorAssertionFunc func(TestingT, error, ...interface{}) bool + +// Comparison a custom function that returns true on success and false on failure +type Comparison func() (success bool) + +/* + Helper functions +*/ + +// ObjectsAreEqual determines if two objects are considered equal. +// +// This function does no assertion of any kind. +func ObjectsAreEqual(expected, actual interface{}) bool { + if expected == nil || actual == nil { + return expected == actual + } + + exp, ok := expected.([]byte) + if !ok { + return reflect.DeepEqual(expected, actual) + } + + act, ok := actual.([]byte) + if !ok { + return false + } + if exp == nil || act == nil { + return exp == nil && act == nil + } + return bytes.Equal(exp, act) +} + +// ObjectsAreEqualValues gets whether two objects are equal, or if their +// values are equal. +func ObjectsAreEqualValues(expected, actual interface{}) bool { + if ObjectsAreEqual(expected, actual) { + return true + } + + actualType := reflect.TypeOf(actual) + if actualType == nil { + return false + } + expectedValue := reflect.ValueOf(expected) + if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) { + // Attempt comparison after type conversion + return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual) + } + + return false +} + +/* CallerInfo is necessary because the assert functions use the testing object +internally, causing it to print the file:line of the assert method, rather than where +the problem actually occurred in calling code.*/ + +// CallerInfo returns an array of strings containing the file and line number +// of each stack frame leading from the current test to the assert call that +// failed. +func CallerInfo() []string { + + pc := uintptr(0) + file := "" + line := 0 + ok := false + name := "" + + callers := []string{} + for i := 0; ; i++ { + pc, file, line, ok = runtime.Caller(i) + if !ok { + // The breaks below failed to terminate the loop, and we ran off the + // end of the call stack. + break + } + + // This is a huge edge case, but it will panic if this is the case, see #180 + if file == "" { + break + } + + f := runtime.FuncForPC(pc) + if f == nil { + break + } + name = f.Name() + + // testing.tRunner is the standard library function that calls + // tests. Subtests are called directly by tRunner, without going through + // the Test/Benchmark/Example function that contains the t.Run calls, so + // with subtests we should break when we hit tRunner, without adding it + // to the list of callers. + if name == "testing.tRunner" { + break + } + + parts := strings.Split(file, "/") + file = parts[len(parts)-1] + if len(parts) > 1 { + dir := parts[len(parts)-2] + if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" { + callers = append(callers, fmt.Sprintf("%s:%d", file, line)) + } + } + + // Drop the package + segments := strings.Split(name, ".") + name = segments[len(segments)-1] + if isTest(name, "Test") || + isTest(name, "Benchmark") || + isTest(name, "Example") { + break + } + } + + return callers +} + +// Stolen from the `go test` tool. +// isTest tells whether name looks like a test (or benchmark, according to prefix). +// It is a Test (say) if there is a character after Test that is not a lower-case letter. +// We don't want TesticularCancer. +func isTest(name, prefix string) bool { + if !strings.HasPrefix(name, prefix) { + return false + } + if len(name) == len(prefix) { // "Test" is ok + return true + } + rune, _ := utf8.DecodeRuneInString(name[len(prefix):]) + return !unicode.IsLower(rune) +} + +func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { + if len(msgAndArgs) == 0 || msgAndArgs == nil { + return "" + } + if len(msgAndArgs) == 1 { + return msgAndArgs[0].(string) + } + if len(msgAndArgs) > 1 { + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } + return "" +} + +// Aligns the provided message so that all lines after the first line start at the same location as the first line. +// Assumes that the first line starts at the correct location (after carriage return, tab, label, spacer and tab). +// The longestLabelLen parameter specifies the length of the longest label in the output (required becaues this is the +// basis on which the alignment occurs). +func indentMessageLines(message string, longestLabelLen int) string { + outBuf := new(bytes.Buffer) + + for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ { + // no need to align first line because it starts at the correct location (after the label) + if i != 0 { + // append alignLen+1 spaces to align with "{{longestLabel}}:" before adding tab + outBuf.WriteString("\n\t" + strings.Repeat(" ", longestLabelLen+1) + "\t") + } + outBuf.WriteString(scanner.Text()) + } + + return outBuf.String() +} + +type failNower interface { + FailNow() +} + +// FailNow fails test +func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + Fail(t, failureMessage, msgAndArgs...) + + // We cannot extend TestingT with FailNow() and + // maintain backwards compatibility, so we fallback + // to panicking when FailNow is not available in + // TestingT. + // See issue #263 + + if t, ok := t.(failNower); ok { + t.FailNow() + } else { + panic("test failed and t is missing `FailNow()`") + } + return false +} + +// Fail reports a failure through +func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + content := []labeledContent{ + {"Error Trace", strings.Join(CallerInfo(), "\n\t\t\t")}, + {"Error", failureMessage}, + } + + // Add test name if the Go version supports it + if n, ok := t.(interface { + Name() string + }); ok { + content = append(content, labeledContent{"Test", n.Name()}) + } + + message := messageFromMsgAndArgs(msgAndArgs...) + if len(message) > 0 { + content = append(content, labeledContent{"Messages", message}) + } + + t.Errorf("\n%s", ""+labeledOutput(content...)) + + return false +} + +type labeledContent struct { + label string + content string +} + +// labeledOutput returns a string consisting of the provided labeledContent. Each labeled output is appended in the following manner: +// +// \t{{label}}:{{align_spaces}}\t{{content}}\n +// +// The initial carriage return is required to undo/erase any padding added by testing.T.Errorf. The "\t{{label}}:" is for the label. +// If a label is shorter than the longest label provided, padding spaces are added to make all the labels match in length. Once this +// alignment is achieved, "\t{{content}}\n" is added for the output. +// +// If the content of the labeledOutput contains line breaks, the subsequent lines are aligned so that they start at the same location as the first line. +func labeledOutput(content ...labeledContent) string { + longestLabel := 0 + for _, v := range content { + if len(v.label) > longestLabel { + longestLabel = len(v.label) + } + } + var output string + for _, v := range content { + output += "\t" + v.label + ":" + strings.Repeat(" ", longestLabel-len(v.label)) + "\t" + indentMessageLines(v.content, longestLabel) + "\n" + } + return output +} + +// Implements asserts that an object is implemented by the specified interface. +// +// assert.Implements(t, (*MyInterface)(nil), new(MyObject)) +func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + interfaceType := reflect.TypeOf(interfaceObject).Elem() + + if object == nil { + return Fail(t, fmt.Sprintf("Cannot check if nil implements %v", interfaceType), msgAndArgs...) + } + if !reflect.TypeOf(object).Implements(interfaceType) { + return Fail(t, fmt.Sprintf("%T must implement %v", object, interfaceType), msgAndArgs...) + } + + return true +} + +// IsType asserts that the specified objects are of the same type. +func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if !ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType)) { + return Fail(t, fmt.Sprintf("Object expected to be of type %v, but was %v", reflect.TypeOf(expectedType), reflect.TypeOf(object)), msgAndArgs...) + } + + return true +} + +// Equal asserts that two objects are equal. +// +// assert.Equal(t, 123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if err := validateEqualArgs(expected, actual); err != nil { + return Fail(t, fmt.Sprintf("Invalid operation: %#v == %#v (%s)", + expected, actual, err), msgAndArgs...) + } + + if !ObjectsAreEqual(expected, actual) { + diff := diff(expected, actual) + expected, actual = formatUnequalValues(expected, actual) + return Fail(t, fmt.Sprintf("Not equal: \n"+ + "expected: %s\n"+ + "actual : %s%s", expected, actual, diff), msgAndArgs...) + } + + return true + +} + +// formatUnequalValues takes two values of arbitrary types and returns string +// representations appropriate to be presented to the user. +// +// If the values are not of like type, the returned strings will be prefixed +// with the type name, and the value will be enclosed in parenthesis similar +// to a type conversion in the Go grammar. +func formatUnequalValues(expected, actual interface{}) (e string, a string) { + if reflect.TypeOf(expected) != reflect.TypeOf(actual) { + return fmt.Sprintf("%T(%#v)", expected, expected), + fmt.Sprintf("%T(%#v)", actual, actual) + } + + return fmt.Sprintf("%#v", expected), + fmt.Sprintf("%#v", actual) +} + +// EqualValues asserts that two objects are equal or convertable to the same types +// and equal. +// +// assert.EqualValues(t, uint32(123), int32(123)) +func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if !ObjectsAreEqualValues(expected, actual) { + diff := diff(expected, actual) + expected, actual = formatUnequalValues(expected, actual) + return Fail(t, fmt.Sprintf("Not equal: \n"+ + "expected: %s\n"+ + "actual : %s%s", expected, actual, diff), msgAndArgs...) + } + + return true + +} + +// Exactly asserts that two objects are equal in value and type. +// +// assert.Exactly(t, int32(123), int64(123)) +func Exactly(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + aType := reflect.TypeOf(expected) + bType := reflect.TypeOf(actual) + + if aType != bType { + return Fail(t, fmt.Sprintf("Types expected to match exactly\n\t%v != %v", aType, bType), msgAndArgs...) + } + + return Equal(t, expected, actual, msgAndArgs...) + +} + +// NotNil asserts that the specified object is not nil. +// +// assert.NotNil(t, err) +func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !isNil(object) { + return true + } + return Fail(t, "Expected value not to be nil.", msgAndArgs...) +} + +// isNil checks if a specified object is nil or not, without Failing. +func isNil(object interface{}) bool { + if object == nil { + return true + } + + value := reflect.ValueOf(object) + kind := value.Kind() + if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() { + return true + } + + return false +} + +// Nil asserts that the specified object is nil. +// +// assert.Nil(t, err) +func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if isNil(object) { + return true + } + return Fail(t, fmt.Sprintf("Expected nil, but got: %#v", object), msgAndArgs...) +} + +// isEmpty gets whether the specified object is considered empty or not. +func isEmpty(object interface{}) bool { + + // get nil case out of the way + if object == nil { + return true + } + + objValue := reflect.ValueOf(object) + + switch objValue.Kind() { + // collection types are empty when they have no element + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + return objValue.Len() == 0 + // pointers are empty if nil or if the value they point to is empty + case reflect.Ptr: + if objValue.IsNil() { + return true + } + deref := objValue.Elem().Interface() + return isEmpty(deref) + // for all other types, compare against the zero value + default: + zero := reflect.Zero(objValue.Type()) + return reflect.DeepEqual(object, zero.Interface()) + } +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// assert.Empty(t, obj) +func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + pass := isEmpty(object) + if !pass { + Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...) + } + + return pass + +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if assert.NotEmpty(t, obj) { +// assert.Equal(t, "two", obj[1]) +// } +func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + pass := !isEmpty(object) + if !pass { + Fail(t, fmt.Sprintf("Should NOT be empty, but was %v", object), msgAndArgs...) + } + + return pass + +} + +// getLen try to get length of object. +// return (false, 0) if impossible. +func getLen(x interface{}) (ok bool, length int) { + v := reflect.ValueOf(x) + defer func() { + if e := recover(); e != nil { + ok = false + } + }() + return true, v.Len() +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// assert.Len(t, mySlice, 3) +func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + ok, l := getLen(object) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", object), msgAndArgs...) + } + + if l != length { + return Fail(t, fmt.Sprintf("\"%s\" should have %d item(s), but has %d", object, length, l), msgAndArgs...) + } + return true +} + +// True asserts that the specified value is true. +// +// assert.True(t, myBool) +func True(t TestingT, value bool, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if h, ok := t.(interface { + Helper() + }); ok { + h.Helper() + } + + if value != true { + return Fail(t, "Should be true", msgAndArgs...) + } + + return true + +} + +// False asserts that the specified value is false. +// +// assert.False(t, myBool) +func False(t TestingT, value bool, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if value != false { + return Fail(t, "Should be false", msgAndArgs...) + } + + return true + +} + +// NotEqual asserts that the specified values are NOT equal. +// +// assert.NotEqual(t, obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if err := validateEqualArgs(expected, actual); err != nil { + return Fail(t, fmt.Sprintf("Invalid operation: %#v != %#v (%s)", + expected, actual, err), msgAndArgs...) + } + + if ObjectsAreEqual(expected, actual) { + return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...) + } + + return true + +} + +// containsElement try loop over the list check if the list includes the element. +// return (false, false) if impossible. +// return (true, false) if element was not found. +// return (true, true) if element was found. +func includeElement(list interface{}, element interface{}) (ok, found bool) { + + listValue := reflect.ValueOf(list) + elementValue := reflect.ValueOf(element) + defer func() { + if e := recover(); e != nil { + ok = false + found = false + } + }() + + if reflect.TypeOf(list).Kind() == reflect.String { + return true, strings.Contains(listValue.String(), elementValue.String()) + } + + if reflect.TypeOf(list).Kind() == reflect.Map { + mapKeys := listValue.MapKeys() + for i := 0; i < len(mapKeys); i++ { + if ObjectsAreEqual(mapKeys[i].Interface(), element) { + return true, true + } + } + return true, false + } + + for i := 0; i < listValue.Len(); i++ { + if ObjectsAreEqual(listValue.Index(i).Interface(), element) { + return true, true + } + } + return true, false + +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Contains(t, "Hello World", "World") +// assert.Contains(t, ["Hello", "World"], "World") +// assert.Contains(t, {"Hello": "World"}, "Hello") +func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + ok, found := includeElement(s, contains) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) + } + if !found { + return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", s, contains), msgAndArgs...) + } + + return true + +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContains(t, "Hello World", "Earth") +// assert.NotContains(t, ["Hello", "World"], "Earth") +// assert.NotContains(t, {"Hello": "World"}, "Earth") +func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + ok, found := includeElement(s, contains) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) + } + if found { + return Fail(t, fmt.Sprintf("\"%s\" should not contain \"%s\"", s, contains), msgAndArgs...) + } + + return true + +} + +// Subset asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// assert.Subset(t, [1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]") +func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if subset == nil { + return true // we consider nil to be equal to the nil set + } + + subsetValue := reflect.ValueOf(subset) + defer func() { + if e := recover(); e != nil { + ok = false + } + }() + + listKind := reflect.TypeOf(list).Kind() + subsetKind := reflect.TypeOf(subset).Kind() + + if listKind != reflect.Array && listKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) + } + + if subsetKind != reflect.Array && subsetKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) + } + + for i := 0; i < subsetValue.Len(); i++ { + element := subsetValue.Index(i).Interface() + ok, found := includeElement(list, element) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) + } + if !found { + return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", list, element), msgAndArgs...) + } + } + + return true +} + +// NotSubset asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// assert.NotSubset(t, [1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]") +func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if subset == nil { + return Fail(t, fmt.Sprintf("nil is the empty set which is a subset of every set"), msgAndArgs...) + } + + subsetValue := reflect.ValueOf(subset) + defer func() { + if e := recover(); e != nil { + ok = false + } + }() + + listKind := reflect.TypeOf(list).Kind() + subsetKind := reflect.TypeOf(subset).Kind() + + if listKind != reflect.Array && listKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) + } + + if subsetKind != reflect.Array && subsetKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) + } + + for i := 0; i < subsetValue.Len(); i++ { + element := subsetValue.Index(i).Interface() + ok, found := includeElement(list, element) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) + } + if !found { + return true + } + } + + return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) +func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if isEmpty(listA) && isEmpty(listB) { + return true + } + + aKind := reflect.TypeOf(listA).Kind() + bKind := reflect.TypeOf(listB).Kind() + + if aKind != reflect.Array && aKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", listA, aKind), msgAndArgs...) + } + + if bKind != reflect.Array && bKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", listB, bKind), msgAndArgs...) + } + + aValue := reflect.ValueOf(listA) + bValue := reflect.ValueOf(listB) + + aLen := aValue.Len() + bLen := bValue.Len() + + if aLen != bLen { + return Fail(t, fmt.Sprintf("lengths don't match: %d != %d", aLen, bLen), msgAndArgs...) + } + + // Mark indexes in bValue that we already used + visited := make([]bool, bLen) + for i := 0; i < aLen; i++ { + element := aValue.Index(i).Interface() + found := false + for j := 0; j < bLen; j++ { + if visited[j] { + continue + } + if ObjectsAreEqual(bValue.Index(j).Interface(), element) { + visited[j] = true + found = true + break + } + } + if !found { + return Fail(t, fmt.Sprintf("element %s appears more times in %s than in %s", element, aValue, bValue), msgAndArgs...) + } + } + + return true +} + +// Condition uses a Comparison to assert a complex condition. +func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + result := comp() + if !result { + Fail(t, "Condition failed!", msgAndArgs...) + } + return result +} + +// PanicTestFunc defines a func that should be passed to the assert.Panics and assert.NotPanics +// methods, and represents a simple func that takes no arguments, and returns nothing. +type PanicTestFunc func() + +// didPanic returns true if the function passed to it panics. Otherwise, it returns false. +func didPanic(f PanicTestFunc) (bool, interface{}) { + + didPanic := false + var message interface{} + func() { + + defer func() { + if message = recover(); message != nil { + didPanic = true + } + }() + + // call the target function + f() + + }() + + return didPanic, message + +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panics(t, func(){ GoCrazy() }) +func Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if funcDidPanic, panicValue := didPanic(f); !funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...) + } + + return true +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// assert.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) +func PanicsWithValue(t TestingT, expected interface{}, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + funcDidPanic, panicValue := didPanic(f) + if !funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...) + } + if panicValue != expected { + return Fail(t, fmt.Sprintf("func %#v should panic with value:\t%#v\n\tPanic value:\t%#v", f, expected, panicValue), msgAndArgs...) + } + + return true +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanics(t, func(){ RemainCalm() }) +func NotPanics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if funcDidPanic, panicValue := didPanic(f); funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should not panic\n\tPanic value:\t%v", f, panicValue), msgAndArgs...) + } + + return true +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second) +func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + dt := expected.Sub(actual) + if dt < -delta || dt > delta { + return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...) + } + + return true +} + +func toFloat(x interface{}) (float64, bool) { + var xf float64 + xok := true + + switch xn := x.(type) { + case uint8: + xf = float64(xn) + case uint16: + xf = float64(xn) + case uint32: + xf = float64(xn) + case uint64: + xf = float64(xn) + case int: + xf = float64(xn) + case int8: + xf = float64(xn) + case int16: + xf = float64(xn) + case int32: + xf = float64(xn) + case int64: + xf = float64(xn) + case float32: + xf = float64(xn) + case float64: + xf = float64(xn) + case time.Duration: + xf = float64(xn) + default: + xok = false + } + + return xf, xok +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// assert.InDelta(t, math.Pi, (22 / 7.0), 0.01) +func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + af, aok := toFloat(expected) + bf, bok := toFloat(actual) + + if !aok || !bok { + return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...) + } + + if math.IsNaN(af) { + return Fail(t, fmt.Sprintf("Expected must not be NaN"), msgAndArgs...) + } + + if math.IsNaN(bf) { + return Fail(t, fmt.Sprintf("Expected %v with delta %v, but was NaN", expected, delta), msgAndArgs...) + } + + dt := af - bf + if dt < -delta || dt > delta { + return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...) + } + + return true +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if expected == nil || actual == nil || + reflect.TypeOf(actual).Kind() != reflect.Slice || + reflect.TypeOf(expected).Kind() != reflect.Slice { + return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + } + + actualSlice := reflect.ValueOf(actual) + expectedSlice := reflect.ValueOf(expected) + + for i := 0; i < actualSlice.Len(); i++ { + result := InDelta(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), delta, msgAndArgs...) + if !result { + return result + } + } + + return true +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if expected == nil || actual == nil || + reflect.TypeOf(actual).Kind() != reflect.Map || + reflect.TypeOf(expected).Kind() != reflect.Map { + return Fail(t, "Arguments must be maps", msgAndArgs...) + } + + expectedMap := reflect.ValueOf(expected) + actualMap := reflect.ValueOf(actual) + + if expectedMap.Len() != actualMap.Len() { + return Fail(t, "Arguments must have the same number of keys", msgAndArgs...) + } + + for _, k := range expectedMap.MapKeys() { + ev := expectedMap.MapIndex(k) + av := actualMap.MapIndex(k) + + if !ev.IsValid() { + return Fail(t, fmt.Sprintf("missing key %q in expected map", k), msgAndArgs...) + } + + if !av.IsValid() { + return Fail(t, fmt.Sprintf("missing key %q in actual map", k), msgAndArgs...) + } + + if !InDelta( + t, + ev.Interface(), + av.Interface(), + delta, + msgAndArgs..., + ) { + return false + } + } + + return true +} + +func calcRelativeError(expected, actual interface{}) (float64, error) { + af, aok := toFloat(expected) + if !aok { + return 0, fmt.Errorf("expected value %q cannot be converted to float", expected) + } + if af == 0 { + return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error") + } + bf, bok := toFloat(actual) + if !bok { + return 0, fmt.Errorf("actual value %q cannot be converted to float", actual) + } + + return math.Abs(af-bf) / math.Abs(af), nil +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + actualEpsilon, err := calcRelativeError(expected, actual) + if err != nil { + return Fail(t, err.Error(), msgAndArgs...) + } + if actualEpsilon > epsilon { + return Fail(t, fmt.Sprintf("Relative error is too high: %#v (expected)\n"+ + " < %#v (actual)", epsilon, actualEpsilon), msgAndArgs...) + } + + return true +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if expected == nil || actual == nil || + reflect.TypeOf(actual).Kind() != reflect.Slice || + reflect.TypeOf(expected).Kind() != reflect.Slice { + return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + } + + actualSlice := reflect.ValueOf(actual) + expectedSlice := reflect.ValueOf(expected) + + for i := 0; i < actualSlice.Len(); i++ { + result := InEpsilon(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), epsilon) + if !result { + return result + } + } + + return true +} + +/* + Errors +*/ + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoError(t, err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if err != nil { + return Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err), msgAndArgs...) + } + + return true +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if assert.Error(t, err) { +// assert.Equal(t, expectedError, err) +// } +func Error(t TestingT, err error, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if err == nil { + return Fail(t, "An error is expected but got nil.", msgAndArgs...) + } + + return true +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// assert.EqualError(t, err, expectedErrorString) +func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !Error(t, theError, msgAndArgs...) { + return false + } + expected := errString + actual := theError.Error() + // don't need to use deep equals here, we know they are both strings + if expected != actual { + return Fail(t, fmt.Sprintf("Error message not equal:\n"+ + "expected: %q\n"+ + "actual : %q", expected, actual), msgAndArgs...) + } + return true +} + +// matchRegexp return true if a specified regexp matches a string. +func matchRegexp(rx interface{}, str interface{}) bool { + + var r *regexp.Regexp + if rr, ok := rx.(*regexp.Regexp); ok { + r = rr + } else { + r = regexp.MustCompile(fmt.Sprint(rx)) + } + + return (r.FindStringIndex(fmt.Sprint(str)) != nil) + +} + +// Regexp asserts that a specified regexp matches a string. +// +// assert.Regexp(t, regexp.MustCompile("start"), "it's starting") +// assert.Regexp(t, "start...$", "it's not starting") +func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + match := matchRegexp(rx, str) + + if !match { + Fail(t, fmt.Sprintf("Expect \"%v\" to match \"%v\"", str, rx), msgAndArgs...) + } + + return match +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// assert.NotRegexp(t, "^start", "it's not starting") +func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + match := matchRegexp(rx, str) + + if match { + Fail(t, fmt.Sprintf("Expect \"%v\" to NOT match \"%v\"", str, rx), msgAndArgs...) + } + + return !match + +} + +// Zero asserts that i is the zero value for its type. +func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if i != nil && !reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { + return Fail(t, fmt.Sprintf("Should be zero, but was %v", i), msgAndArgs...) + } + return true +} + +// NotZero asserts that i is not the zero value for its type. +func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if i == nil || reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { + return Fail(t, fmt.Sprintf("Should not be zero, but was %v", i), msgAndArgs...) + } + return true +} + +// FileExists checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func FileExists(t TestingT, path string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + info, err := os.Lstat(path) + if err != nil { + if os.IsNotExist(err) { + return Fail(t, fmt.Sprintf("unable to find file %q", path), msgAndArgs...) + } + return Fail(t, fmt.Sprintf("error when running os.Lstat(%q): %s", path, err), msgAndArgs...) + } + if info.IsDir() { + return Fail(t, fmt.Sprintf("%q is a directory", path), msgAndArgs...) + } + return true +} + +// DirExists checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + info, err := os.Lstat(path) + if err != nil { + if os.IsNotExist(err) { + return Fail(t, fmt.Sprintf("unable to find file %q", path), msgAndArgs...) + } + return Fail(t, fmt.Sprintf("error when running os.Lstat(%q): %s", path, err), msgAndArgs...) + } + if !info.IsDir() { + return Fail(t, fmt.Sprintf("%q is a file", path), msgAndArgs...) + } + return true +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + var expectedJSONAsInterface, actualJSONAsInterface interface{} + + if err := json.Unmarshal([]byte(expected), &expectedJSONAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Expected value ('%s') is not valid json.\nJSON parsing error: '%s'", expected, err.Error()), msgAndArgs...) + } + + if err := json.Unmarshal([]byte(actual), &actualJSONAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Input ('%s') needs to be valid json.\nJSON parsing error: '%s'", actual, err.Error()), msgAndArgs...) + } + + return Equal(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...) +} + +func typeAndKind(v interface{}) (reflect.Type, reflect.Kind) { + t := reflect.TypeOf(v) + k := t.Kind() + + if k == reflect.Ptr { + t = t.Elem() + k = t.Kind() + } + return t, k +} + +// diff returns a diff of both values as long as both are of the same type and +// are a struct, map, slice or array. Otherwise it returns an empty string. +func diff(expected interface{}, actual interface{}) string { + if expected == nil || actual == nil { + return "" + } + + et, ek := typeAndKind(expected) + at, _ := typeAndKind(actual) + + if et != at { + return "" + } + + if ek != reflect.Struct && ek != reflect.Map && ek != reflect.Slice && ek != reflect.Array && ek != reflect.String { + return "" + } + + var e, a string + if ek != reflect.String { + e = spewConfig.Sdump(expected) + a = spewConfig.Sdump(actual) + } else { + e = expected.(string) + a = actual.(string) + } + + diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(e), + B: difflib.SplitLines(a), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + + return "\n\nDiff:\n" + diff +} + +// validateEqualArgs checks whether provided arguments can be safely used in the +// Equal/NotEqual functions. +func validateEqualArgs(expected, actual interface{}) error { + if isFunction(expected) || isFunction(actual) { + return errors.New("cannot take func type as argument") + } + return nil +} + +func isFunction(arg interface{}) bool { + if arg == nil { + return false + } + return reflect.TypeOf(arg).Kind() == reflect.Func +} + +var spewConfig = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, +} + +type tHelper interface { + Helper() +} diff --git a/vendor/github.com/stretchr/testify/assert/doc.go b/vendor/github.com/stretchr/testify/assert/doc.go new file mode 100644 index 0000000..c9dccc4 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/doc.go @@ -0,0 +1,45 @@ +// Package assert provides a set of comprehensive testing tools for use with the normal Go testing system. +// +// Example Usage +// +// The following is a complete example using assert in a standard test function: +// import ( +// "testing" +// "github.com/stretchr/testify/assert" +// ) +// +// func TestSomething(t *testing.T) { +// +// var a string = "Hello" +// var b string = "Hello" +// +// assert.Equal(t, a, b, "The two words should be the same.") +// +// } +// +// if you assert many times, use the format below: +// +// import ( +// "testing" +// "github.com/stretchr/testify/assert" +// ) +// +// func TestSomething(t *testing.T) { +// assert := assert.New(t) +// +// var a string = "Hello" +// var b string = "Hello" +// +// assert.Equal(a, b, "The two words should be the same.") +// } +// +// Assertions +// +// Assertions allow you to easily write test code, and are global funcs in the `assert` package. +// All assertion functions take, as the first argument, the `*testing.T` object provided by the +// testing framework. This allows the assertion funcs to write the failings and other details to +// the correct place. +// +// Every assertion function also takes an optional string message as the final argument, +// allowing custom error messages to be appended to the message the assertion method outputs. +package assert diff --git a/vendor/github.com/stretchr/testify/assert/errors.go b/vendor/github.com/stretchr/testify/assert/errors.go new file mode 100644 index 0000000..ac9dc9d --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/errors.go @@ -0,0 +1,10 @@ +package assert + +import ( + "errors" +) + +// AnError is an error instance useful for testing. If the code does not care +// about error specifics, and only needs to return the error for example, this +// error should be used to make the test code more readable. +var AnError = errors.New("assert.AnError general error for testing") diff --git a/vendor/github.com/stretchr/testify/assert/forward_assertions.go b/vendor/github.com/stretchr/testify/assert/forward_assertions.go new file mode 100644 index 0000000..9ad5685 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/forward_assertions.go @@ -0,0 +1,16 @@ +package assert + +// Assertions provides assertion methods around the +// TestingT interface. +type Assertions struct { + t TestingT +} + +// New makes a new Assertions object for the specified TestingT. +func New(t TestingT) *Assertions { + return &Assertions{ + t: t, + } +} + +//go:generate go run ../_codegen/main.go -output-package=assert -template=assertion_forward.go.tmpl -include-format-funcs diff --git a/vendor/github.com/stretchr/testify/assert/http_assertions.go b/vendor/github.com/stretchr/testify/assert/http_assertions.go new file mode 100644 index 0000000..df46fa7 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/http_assertions.go @@ -0,0 +1,143 @@ +package assert + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" +) + +// httpCode is a helper that returns HTTP code of the response. It returns -1 and +// an error if building a new request fails. +func httpCode(handler http.HandlerFunc, method, url string, values url.Values) (int, error) { + w := httptest.NewRecorder() + req, err := http.NewRequest(method, url, nil) + if err != nil { + return -1, err + } + req.URL.RawQuery = values.Encode() + handler(w, req) + return w.Code, nil +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccess(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + code, err := httpCode(handler, method, url, values) + if err != nil { + Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err)) + return false + } + + isSuccessCode := code >= http.StatusOK && code <= http.StatusPartialContent + if !isSuccessCode { + Fail(t, fmt.Sprintf("Expected HTTP success status code for %q but received %d", url+"?"+values.Encode(), code)) + } + + return isSuccessCode +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirect(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + code, err := httpCode(handler, method, url, values) + if err != nil { + Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err)) + return false + } + + isRedirectCode := code >= http.StatusMultipleChoices && code <= http.StatusTemporaryRedirect + if !isRedirectCode { + Fail(t, fmt.Sprintf("Expected HTTP redirect status code for %q but received %d", url+"?"+values.Encode(), code)) + } + + return isRedirectCode +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPError(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + code, err := httpCode(handler, method, url, values) + if err != nil { + Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err)) + return false + } + + isErrorCode := code >= http.StatusBadRequest + if !isErrorCode { + Fail(t, fmt.Sprintf("Expected HTTP error status code for %q but received %d", url+"?"+values.Encode(), code)) + } + + return isErrorCode +} + +// HTTPBody is a helper that returns HTTP body of the response. It returns +// empty string if building a new request fails. +func HTTPBody(handler http.HandlerFunc, method, url string, values url.Values) string { + w := httptest.NewRecorder() + req, err := http.NewRequest(method, url+"?"+values.Encode(), nil) + if err != nil { + return "" + } + handler(w, req) + return w.Body.String() +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + body := HTTPBody(handler, method, url, values) + + contains := strings.Contains(body, fmt.Sprint(str)) + if !contains { + Fail(t, fmt.Sprintf("Expected response body for \"%s\" to contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body)) + } + + return contains +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + body := HTTPBody(handler, method, url, values) + + contains := strings.Contains(body, fmt.Sprint(str)) + if contains { + Fail(t, fmt.Sprintf("Expected response body for \"%s\" to NOT contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body)) + } + + return !contains +}