Add zone parsing, and test code separately
parent
8a8924a2d3
commit
8d9828d390
|
@ -8,6 +8,22 @@ on:
|
|||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.15
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Test code
|
||||
run: make test
|
||||
|
||||
vet:
|
||||
name: Vet
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -68,10 +84,7 @@ jobs:
|
|||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: make
|
||||
|
||||
- name: Test
|
||||
run: make test
|
||||
run: make TAGS=production
|
||||
|
||||
|
||||
# vim: set et ts=2 sw=2:
|
||||
|
|
2
Makefile
2
Makefile
|
@ -21,7 +21,7 @@ datetime: *.go
|
|||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(GO) test -v
|
||||
$(GO) test -cover -bench=. -v
|
||||
|
||||
|
||||
DATASETS = \
|
||||
|
|
2
app.go
2
app.go
|
@ -16,7 +16,7 @@ type Datetime struct {
|
|||
}
|
||||
|
||||
// NewDatetime creates an application instance. It assumes certain resources
|
||||
// like templates and data exist.
|
||||
// like templates and data exist
|
||||
func NewDatetime() (*Datetime, error) {
|
||||
// Data
|
||||
tmpl, err := template.ParseGlob("templates/*")
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
func TestChooseTemplate(t *testing.T) {
|
||||
tmpl, err := template.ParseGlob("templates/*")
|
||||
if err != nil {
|
||||
t.Errorf("unable to load templates: %v", err)
|
||||
panic(err)
|
||||
}
|
||||
app := &Datetime{tmpl: tmpl}
|
||||
|
||||
|
|
|
@ -11,6 +11,10 @@ func TestChooseResponseType(t *testing.T) {
|
|||
}
|
||||
r = chooseResponseType("application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
|
||||
if r != responseAny {
|
||||
t.Errorf("expecting any, got %v", r)
|
||||
}
|
||||
r = chooseResponseType("text/plain;q=,text/html")
|
||||
if r != responseHTML {
|
||||
t.Errorf("expecting html, got %v", r)
|
||||
}
|
||||
}
|
||||
|
|
33
url_test.go
33
url_test.go
|
@ -21,7 +21,7 @@ func TestURLParse(t *testing.T) {
|
|||
u := mustURLParse("http://test/2020-06-02T14:00+08:00/Singapore,Malaysia")
|
||||
got, err := ParseRequest(u)
|
||||
if err != nil {
|
||||
t.Errorf("mismatch: got error %v", err)
|
||||
t.Errorf("got error %v", err)
|
||||
return
|
||||
}
|
||||
want := Request{
|
||||
|
@ -29,13 +29,13 @@ func TestURLParse(t *testing.T) {
|
|||
[]string{"Singapore", "Malaysia"},
|
||||
}
|
||||
if !cmp.Equal(got, want) {
|
||||
t.Errorf("mismatch: \n%v", cmp.Diff(got, want))
|
||||
t.Errorf("%v", cmp.Diff(got, want))
|
||||
}
|
||||
|
||||
u = mustURLParse("http://test/2019-04-30T18:00:00Z/Nowhere")
|
||||
got, err = ParseRequest(u)
|
||||
if err != nil {
|
||||
t.Errorf("mismatch: got error %v", err)
|
||||
t.Errorf("got error %v", err)
|
||||
return
|
||||
}
|
||||
want = Request{
|
||||
|
@ -43,7 +43,7 @@ func TestURLParse(t *testing.T) {
|
|||
[]string{"Nowhere"},
|
||||
}
|
||||
if !cmp.Equal(got, want) {
|
||||
t.Errorf("mismatch: \n%v", cmp.Diff(got, want))
|
||||
t.Errorf("%v", cmp.Diff(got, want))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,31 +51,38 @@ func TestURLParseFail(t *testing.T) {
|
|||
u := mustURLParse("http://test/2002-08-30T14:00+06:00/")
|
||||
_, err := ParseRequest(u)
|
||||
if !errors.Is(err, ErrMissingComponent) {
|
||||
t.Errorf("mismatch: got error %v, want error %v", err, ErrMissingComponent)
|
||||
return
|
||||
t.Errorf("got error %v, want error %v", err, ErrMissingComponent)
|
||||
}
|
||||
|
||||
u = mustURLParse("http://test/")
|
||||
_, err = ParseRequest(u)
|
||||
if !errors.Is(err, ErrMissingComponent) {
|
||||
t.Errorf("mismatch: got error %v, want error %v", err, ErrMissingComponent)
|
||||
return
|
||||
t.Errorf("got error %v, want error %v", err, ErrMissingComponent)
|
||||
}
|
||||
|
||||
u = mustURLParse("http://test")
|
||||
_, err = ParseRequest(u)
|
||||
if !errors.Is(err, ErrMissingComponent) {
|
||||
t.Errorf("got error %v, want error %v", err, ErrMissingComponent)
|
||||
}
|
||||
|
||||
u = mustURLParse("http://test/hi/hi/hi")
|
||||
_, err = ParseRequest(u)
|
||||
if !errors.Is(err, ErrTooManyComponent) {
|
||||
t.Errorf("got error %v, want error %v", err, ErrTooManyComponent)
|
||||
}
|
||||
|
||||
u = mustURLParse("http://test/2000-01-13T00:00Z08:00/hi")
|
||||
_, err = ParseRequest(u)
|
||||
_, isParseError := err.(*time.ParseError)
|
||||
if !isParseError {
|
||||
t.Errorf("mismatch: got error %v, want time.ParseError", err)
|
||||
return
|
||||
t.Errorf("got error %v, want time.ParseError", err)
|
||||
}
|
||||
|
||||
u = mustURLParse("http://test/2000-01-13 00:00+08:00/hi")
|
||||
_, err = ParseRequest(u)
|
||||
_, isParseError = err.(*time.ParseError)
|
||||
if !isParseError {
|
||||
t.Errorf("mismatch: got error %v, want time.ParseError", err)
|
||||
return
|
||||
t.Errorf("got error %v, want time.ParseError", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/serverwentdown/datetime.link/data"
|
||||
)
|
||||
|
||||
// ErrZoneNotFound is thrown when a zone string has no match
|
||||
var ErrZoneNotFound = errors.New("zone not found")
|
||||
|
||||
// ErrZoneOffsetInvalid is thrown when a zone string is an invalid zone offset
|
||||
var ErrZoneOffsetInvalid = errors.New("offset zone invalid")
|
||||
|
||||
var zoneOffsetRegexp = regexp.MustCompile(`^[+-][0-9]{2}:[0-9]{2}$`)
|
||||
|
||||
// SearchCities looks up a city by it's reference
|
||||
func SearchCities(cities map[string]*data.City, city string) (*data.City, error) {
|
||||
// For now, simple map read will do
|
||||
if city, ok := cities[city]; ok {
|
||||
return city, nil
|
||||
}
|
||||
return nil, ErrZoneNotFound
|
||||
}
|
||||
|
||||
// ParseZoneOffset parses a zone string into a time.Location
|
||||
func ParseZoneOffset(zone string) (*time.Location, error) {
|
||||
if !zoneOffsetRegexp.MatchString(zone) {
|
||||
return nil, ErrZoneOffsetInvalid
|
||||
}
|
||||
// Assume that if it satisfies the regex, it satisfies the length and won't
|
||||
// fail to parse
|
||||
d := 0
|
||||
if zone[0] == '+' {
|
||||
d = 1
|
||||
}
|
||||
if zone[0] == '-' {
|
||||
d = -1
|
||||
}
|
||||
h, _ := strconv.ParseUint(zone[1:1+2], 10, 64)
|
||||
// Allow hour offsets greater that 24
|
||||
m, _ := strconv.ParseUint(zone[1+3:1+3+2], 10, 64)
|
||||
if m >= 60 {
|
||||
return nil, ErrZoneOffsetInvalid
|
||||
}
|
||||
return time.FixedZone("UTC"+zone, d*(int(h)*60*60+int(m)*60)), nil
|
||||
}
|
||||
|
||||
// Zone represents any form of zone offset: this could be a city or a fixed
|
||||
// offset from UTC
|
||||
type Zone struct {
|
||||
// City is optional
|
||||
City *data.City
|
||||
// Offset is optional
|
||||
Offset *time.Location
|
||||
}
|
||||
|
||||
// ResolveZone resolves a zone string into a Zone
|
||||
func ResolveZone(cities map[string]*data.City, zone string) (Zone, error) {
|
||||
// Try parsing as a zone offset
|
||||
offset, err := ParseZoneOffset(zone)
|
||||
if err == nil {
|
||||
return Zone{
|
||||
Offset: offset,
|
||||
}, nil
|
||||
}
|
||||
// Parse as a city
|
||||
city, err := SearchCities(cities, zone)
|
||||
if err == nil {
|
||||
return Zone{
|
||||
City: city,
|
||||
}, nil
|
||||
}
|
||||
return Zone{}, err
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/serverwentdown/datetime.link/data"
|
||||
)
|
||||
|
||||
func TestParseZoneOffset(t *testing.T) {
|
||||
loc, err := ParseZoneOffset("+08:00")
|
||||
if err != nil {
|
||||
t.Errorf("want error %v, got error %v", nil, err)
|
||||
} else {
|
||||
time := time.Date(2020, time.November, 8, 23, 9, 0, 0, loc).Unix()
|
||||
want := int64(1604848140)
|
||||
if time != want {
|
||||
t.Errorf("got %d, want %d", time, want)
|
||||
}
|
||||
}
|
||||
|
||||
loc, err = ParseZoneOffset("-00:30")
|
||||
if err != nil {
|
||||
t.Errorf("want error %v, got error %v", nil, err)
|
||||
} else {
|
||||
time := time.Date(2020, time.November, 8, 14, 39, 0, 0, loc).Unix()
|
||||
want := int64(1604848140)
|
||||
if time != want {
|
||||
t.Errorf("got %d, want %d", time, want)
|
||||
}
|
||||
}
|
||||
|
||||
loc, err = ParseZoneOffset("-0030")
|
||||
if err != ErrZoneOffsetInvalid {
|
||||
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
|
||||
}
|
||||
|
||||
loc, err = ParseZoneOffset("00:30")
|
||||
if err != ErrZoneOffsetInvalid {
|
||||
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
|
||||
}
|
||||
|
||||
loc, err = ParseZoneOffset("+08:60")
|
||||
if err != ErrZoneOffsetInvalid {
|
||||
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
|
||||
}
|
||||
|
||||
loc, err = ParseZoneOffset("+08:-6")
|
||||
if err != ErrZoneOffsetInvalid {
|
||||
t.Errorf("want error %v, got error %v", ErrZoneOffsetInvalid, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchCities(t *testing.T) {
|
||||
cities, err := data.ReadCities()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
city, err := SearchCities(cities, "Singapore-SG")
|
||||
if err != nil {
|
||||
t.Errorf("want error %v, got error %v", nil, err)
|
||||
}
|
||||
wantName := "Singapore"
|
||||
wantZone := "Asia/Singapore"
|
||||
if city.Name != wantName || city.Timezone != wantZone {
|
||||
t.Errorf("want %v %v, got %v", wantName, wantZone, city)
|
||||
}
|
||||
|
||||
city, err = SearchCities(cities, "Yuzhno_Sakhalinsk-Sakhalin_Oblast-RU")
|
||||
if err != nil {
|
||||
t.Errorf("want error %v, got error %v", nil, err)
|
||||
}
|
||||
wantName = "Yuzhno-Sakhalinsk"
|
||||
wantZone = "Asia/Sakhalin"
|
||||
if city.Name != wantName || city.Timezone != wantZone {
|
||||
t.Errorf("want %v %v, got %v", wantName, wantZone, city)
|
||||
}
|
||||
|
||||
_, err = SearchCities(cities, "Nowhere")
|
||||
if err != ErrZoneNotFound {
|
||||
t.Errorf("want error %v, got error %v", ErrZoneNotFound, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReadCities(b *testing.B) {
|
||||
// This does take quite a while
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = data.ReadCities()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSearchCities(b *testing.B) {
|
||||
cities, err := data.ReadCities()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = SearchCities(cities, "Yuzhno_Sakhalinsk-Sakhalin_Oblast-RU")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue