package geo import ( "math" "strconv" "strings" "testing" ) // parseCase holds one input to ParseCoordinates and its expectation. // expected is nil for inputs that must fail; errPart is then the // text the error message must contain. type parseCase struct { in string expected *Coordinates errPart string } func TestParseCoordinates(t *testing.T) { var cases = []parseCase{ {in: `52.37,4.90`, expected: &Coordinates{52.37, 4.90}}, {in: `-33.87,151.21`, expected: &Coordinates{-33.87, 151.21}}, {in: `1,2`, expected: &Coordinates{1, 2}}, {in: `+1.5,-2.5`, expected: &Coordinates{1.5, -2.5}}, {in: `1e1,2.5e0`, expected: &Coordinates{10, 2.5}}, {in: `0x1p2,3`, expected: &Coordinates{4, 3}}, // ParseCoordinates does not check value ranges. {in: `inf,1`, expected: &Coordinates{math.Inf(1), 1}}, {in: ``, errPart: `invalid string`}, {in: `52.37`, errPart: `invalid string`}, {in: `52.37,4.90,1`, errPart: `invalid string`}, {in: `a,b`, errPart: `latitude`}, {in: `1,b`, errPart: `longitude`}, {in: `1,`, errPart: `longitude`}, {in: `,2`, errPart: `latitude`}, // ParseFloat does not skip whitespace. {in: ` 52.37 , 4.90 `, errPart: `latitude`}, // ParseFloat rejects values outside the float64 range. {in: `1e999,2`, errPart: `latitude`}, } for _, c := range cases { t.Run(c.in, c.testParseCoordinates) } } // testParseCoordinates checks the expected result or error for one parse case. func (c parseCase) testParseCoordinates(t *testing.T) { got, err := ParseCoordinates(c.in) if c.expected == nil { if err == nil { t.Fatalf("got %v, want error", got) } if got != nil { t.Fatalf("got %v and error %v, want nil", got, err) } if !strings.Contains(err.Error(), c.errPart) { t.Fatalf("error %q, want %q", err.Error(), c.errPart) } return } if err != nil { t.Fatalf("error = %v", err) } if got == nil { t.Fatalf("got nil, want %v", c.expected) } if got.Latitude != c.expected.Latitude { t.Errorf( "Latitude = %v, want %v", got.Latitude, c.expected.Latitude, ) } if got.Longitude != c.expected.Longitude { t.Errorf( "Longitude = %v, want %v", got.Longitude, c.expected.Longitude, ) } } // TestParseCoordinatesNaN checks that ParseCoordinates accepts nan. // It does not check value ranges. func TestParseCoordinatesNaN(t *testing.T) { got, err := ParseCoordinates("nan,2") if err != nil { t.Fatalf("error = %v", err) } if got == nil { t.Fatal("got nil, want coordinates") } if !math.IsNaN(got.Latitude) { t.Errorf("Latitude = %v, want NaN", got.Latitude) } if got.Longitude != 2 { t.Errorf("Longitude = %v, want 2", got.Longitude) } } // FuzzParseCoordinates checks that ParseCoordinates handles any input // without panicking, that it does not return a result and an error // together, and that a successful result matches strconv.ParseFloat // on each comma-separated part. func FuzzParseCoordinates(f *testing.F) { f.Add(`52.37,4.90`) f.Add(`-33.8688,151.2093`) f.Add(`nan,inf`) f.Add(``) f.Add(`52.37`) f.Add(`52.37,4.90,1`) f.Add(`a,b`) f.Add(`1,`) f.Add(`,2`) f.Add(`1e999,2`) f.Fuzz(fuzzParseCoordinates) } func fuzzParseCoordinates(t *testing.T, s string) { got, err := ParseCoordinates(s) if err != nil && got != nil { t.Fatalf( "ParseCoordinates(%q) = %v with error %v, want nil", s, got, err, ) } // The input must hold exactly two comma-separated parts. parts := strings.Split(s, `,`) if len(parts) != 2 { if err == nil { t.Fatalf( "ParseCoordinates(%q) = %v, want error", s, got, ) } return } // Each part must hold a valid float64 value. expectedLat, latErr := strconv.ParseFloat(parts[0], 64) expectedLon, lonErr := strconv.ParseFloat(parts[1], 64) if latErr != nil || lonErr != nil { if err == nil { t.Fatalf( "ParseCoordinates(%q) = %v, want error", s, got, ) } return } if err != nil { t.Fatalf("ParseCoordinates(%q) error = %v", s, err) } if got == nil { t.Fatalf("ParseCoordinates(%q) = nil, want coordinates", s) } // Both values come from strconv.ParseFloat. The bit comparison // reports NaN as equal; the == operator does not. if math.Float64bits(got.Latitude) != math.Float64bits(expectedLat) { t.Fatalf( "ParseCoordinates(%q).Latitude = %v, want %v", s, got.Latitude, expectedLat, ) } if math.Float64bits(got.Longitude) != math.Float64bits(expectedLon) { t.Fatalf( "ParseCoordinates(%q).Longitude = %v, want %v", s, got.Longitude, expectedLon, ) } }