mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-28 14:53:44 +00:00
cmd/tailscale/cli: add subcommand: tailscale debug location
Updates tailscale/corp#29968 Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
parent
c87f44b687
commit
ae68c7865c
195
cmd/tailscale/cli/debug-location.go
Normal file
195
cmd/tailscale/cli/debug-location.go
Normal file
@ -0,0 +1,195 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/geo"
|
||||
)
|
||||
|
||||
var locationCmd = func() *ffcli.Command {
|
||||
if !envknob.UseWIPCode() {
|
||||
return nil
|
||||
}
|
||||
return &ffcli.Command{
|
||||
Name: "location",
|
||||
Exec: runLocation,
|
||||
ShortHelp: "Print or change location data, for testing",
|
||||
ShortUsage: "" +
|
||||
" Print all fields: tailscale debug location\n" +
|
||||
" Print a field: tailscale debug location FIELD\n" +
|
||||
" Clear a field: tailscale debug location FIELD=\n" +
|
||||
" Change field[s]: tailscale debug location FIELD=VALUE [...]",
|
||||
LongHelp: "" +
|
||||
"FIELDS\n" +
|
||||
locationFields.help(),
|
||||
}
|
||||
}
|
||||
|
||||
func runLocation(ctx context.Context, args []string) error {
|
||||
var getks []locationGetK
|
||||
var setvs []locationSetV
|
||||
|
||||
if len(args) == 0 {
|
||||
// Print all fields:
|
||||
for _, k := range slices.Sorted(maps.Keys(locationFields)) {
|
||||
getk := locationGetK{
|
||||
get: locationFields[k].get,
|
||||
k: k,
|
||||
}
|
||||
getks = append(getks, getk)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse all args first, to avoid having to abort halfway through.
|
||||
for _, arg := range args {
|
||||
k, v, set := strings.Cut(arg, "=")
|
||||
field, known := locationFields[k]
|
||||
if !known {
|
||||
return fmt.Errorf("unknown field: %s", k)
|
||||
}
|
||||
|
||||
if set {
|
||||
// Change or clear these fields:
|
||||
setv := locationSetV{
|
||||
set: field.set,
|
||||
k: k,
|
||||
v: v,
|
||||
}
|
||||
setvs = append(setvs, setv)
|
||||
} else {
|
||||
// Print a field:
|
||||
getk := locationGetK{
|
||||
get: field.get,
|
||||
k: k,
|
||||
}
|
||||
getks = append(getks, getk)
|
||||
}
|
||||
}
|
||||
|
||||
if len(getks) > 0 && len(setvs) > 0 {
|
||||
gk, sv := getks[0], setvs[0]
|
||||
return fmt.Errorf("cannot mix %s and %s=%q", gk.k, sv.k, sv.v)
|
||||
}
|
||||
|
||||
if len(setvs) > 0 {
|
||||
// Perform the change or clear:
|
||||
prefs := &ipn.MaskedPrefs{Prefs: ipn.Prefs{}}
|
||||
for _, sv := range setvs {
|
||||
if err := sv.set(prefs, sv.v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx = apitype.RequestReasonKey.WithValue(ctx, "debug location")
|
||||
if _, err := localClient.EditPrefs(ctx, prefs); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
// TODO(sfllaw): [LocalBackend.applyPrefsToHostinfoLocked]
|
||||
// [Auto.SetHostinfo]
|
||||
// [Hostinfo.RoutableIPs] corresponds to --advertise-routes
|
||||
}
|
||||
|
||||
prefs, err := localClient.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch len(getks) {
|
||||
case 1:
|
||||
// Print one fields, without key name:
|
||||
fmt.Printf("%s", getks[0].get(prefs))
|
||||
default:
|
||||
// Print multiple fields:
|
||||
for _, gk := range getks {
|
||||
fmt.Printf("%s=%s", gk.k, gk.get(prefs))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type locationFieldsT map[string]locationField
|
||||
|
||||
var locationFields = locationFieldsT{
|
||||
"city": {
|
||||
get: func(p *ipn.Prefs) string {
|
||||
return p.LocationCity
|
||||
},
|
||||
set: func(p *ipn.MaskedPrefs, v string) error {
|
||||
p.LocationCity = v
|
||||
p.LocationCitySet = true
|
||||
return nil
|
||||
},
|
||||
help: " city=NAME\n" +
|
||||
"\tNAME of this node’s city",
|
||||
},
|
||||
"coords": {
|
||||
get: func(p *ipn.Prefs) string {
|
||||
return p.LocationCoords.FormatLatLng()
|
||||
},
|
||||
set: func(p *ipn.MaskedPrefs, v string) error {
|
||||
pt, err := geo.ParsePoint(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pt = pt.Quantize()
|
||||
|
||||
s, err := pt.MarshalText()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.LocationCoords = s
|
||||
p.LocationCoordsSet = true
|
||||
return nil
|
||||
},
|
||||
help: " coords=(+|-)LATITUDE(+|-)LONGITUDE\n" +
|
||||
"\tLATITUDE and LONGITUDE for this node, in decimal degrees \"+45.5-73.6\"",
|
||||
},
|
||||
"country": {
|
||||
get: func(p *ipn.Prefs) string {
|
||||
return p.LocationCountry
|
||||
},
|
||||
set: func(p *ipn.MaskedPrefs, v string) error {
|
||||
p.LocationCountry = v
|
||||
p.LocationCountrySet = true
|
||||
return nil
|
||||
},
|
||||
help: " country=NAME\n" +
|
||||
"\tNAME of this node’s country",
|
||||
},
|
||||
}
|
||||
|
||||
func (lf locationFieldsT) help() string {
|
||||
var txt []string
|
||||
for _, k := range slices.Sorted(maps.Keys(lf)) {
|
||||
txt = append(txt, lf[k].help)
|
||||
}
|
||||
return strings.Join(txt, "\n")
|
||||
}
|
||||
|
||||
type locationField struct {
|
||||
get func(*ipn.Prefs) string
|
||||
set func(*ipn.MaskedPrefs, string) error
|
||||
help string
|
||||
}
|
||||
|
||||
type locationGetK struct {
|
||||
get func(*ipn.Prefs) string
|
||||
k string
|
||||
help string
|
||||
}
|
||||
|
||||
type locationSetV struct {
|
||||
set func(*ipn.MaskedPrefs, string) error
|
||||
k string
|
||||
v string
|
||||
}
|
@ -374,6 +374,7 @@ func debugCmd() *ffcli.Command {
|
||||
ShortHelp: "Print the current set of candidate peer relay servers",
|
||||
Exec: runPeerRelayServers,
|
||||
},
|
||||
ccall(locationCmd),
|
||||
}...),
|
||||
}
|
||||
}
|
||||
|
24
ipn/prefs.go
24
ipn/prefs.go
@ -51,7 +51,7 @@ func IsLoginServerSynonym(val any) bool {
|
||||
|
||||
// Prefs are the user modifiable settings of the Tailscale node agent.
|
||||
// When you add a Pref to this struct, remember to add a corresponding
|
||||
// field in MaskedPrefs, and check your field for equality in Prefs.Equals().
|
||||
// field in [MaskedPrefs], and check your field for equality in Prefs.Equals().
|
||||
type Prefs struct {
|
||||
// ControlURL is the URL of the control server to use.
|
||||
//
|
||||
@ -294,6 +294,25 @@ type Prefs struct {
|
||||
// We can maybe do that once we're sure which module should persist
|
||||
// it (backend or frontend?)
|
||||
Persist *persist.Persist `json:"Config"`
|
||||
|
||||
// Location fields configure the location overrides for the client node.
|
||||
// These overrides are self-reported hints to the control server, to
|
||||
// help users pick an appropriate node based on its location.
|
||||
//
|
||||
// LocationCoords are the geographical coordinates that give the
|
||||
// approximate location for this node. The coordinates are encoded as
|
||||
// ±latitude±longitude in decimal degrees. These coordinates are never
|
||||
// shared with Tailscale peers.
|
||||
//
|
||||
// LocationCountry and LocationCity are top-level and local-level names
|
||||
// that describe the location for this node. They may be actual country
|
||||
// and city names, in any language or localization; but they can also be
|
||||
// arbitrary groupings. Tailscale clients may use these names to group
|
||||
// nodes by LocationCountry and then LocationCity when presenting a
|
||||
// hierarchical node selector.
|
||||
LocationCity string `json:",omitempty"`
|
||||
LocationCoords string `json:",omitempty"`
|
||||
LocationCountry string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// AutoUpdatePrefs are the auto update settings for the node agent.
|
||||
@ -371,6 +390,9 @@ type MaskedPrefs struct {
|
||||
NetfilterKindSet bool `json:",omitempty"`
|
||||
DriveSharesSet bool `json:",omitempty"`
|
||||
RelayServerPortSet bool `json:",omitempty"`
|
||||
LocationCitySet bool `json:",omitempty"`
|
||||
LocationCoordsSet bool `json:",omitempty"`
|
||||
LocationCountrySet bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// SetsInternal reports whether mp has any of the Internal*Set field bools set
|
||||
|
@ -797,8 +797,13 @@ type Location struct {
|
||||
// IATA, ICAO or ISO 3166-2 codes are recommended ("YSE")
|
||||
CityCode string `json:",omitempty"`
|
||||
|
||||
// Latitude, Longitude are optional geographical coordinates of the node, in degrees.
|
||||
// No particular accuracy level is promised; the coordinates may simply be the center of the city or country.
|
||||
// Latitude and Longitude are optional geographical coordinates of the
|
||||
// node, in decimal degrees. No particular accuracy level is promised;
|
||||
// the coordinates may simply be the center of the city or country.
|
||||
//
|
||||
// To reliably distinguish between an empty {Latitude, Longitude} pair
|
||||
// and the value {Latitude: 0, Longitude: 0}, the latter may be encoded
|
||||
// as {Latitude: 0, Longitude: math.SmallestNonzeroFloat32}.
|
||||
Latitude float64 `json:",omitempty"`
|
||||
Longitude float64 `json:",omitempty"`
|
||||
|
||||
|
@ -153,6 +153,124 @@ func (p Point) String() string {
|
||||
return lat.String() + " " + lng.String()
|
||||
}
|
||||
|
||||
// FormatLatLng returns a compact encoding of p: "±latitude±longitude" where
|
||||
// latitude and longitude are in decimal degrees. If p was not initialized, this
|
||||
// will return "nowhere".
|
||||
func (p Point) FormatLatLng() string {
|
||||
lat, lng, err := p.LatLng()
|
||||
if err != nil {
|
||||
if err == ErrBadPoint {
|
||||
return "nowhere"
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var b []byte
|
||||
b, err = lat.AppendText(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b, err = lng.AppendText(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// ParseLatLng parses the output of [FormatLatLng] and returns its [Point]. If s
|
||||
// is an empty string, or is "nowhere", then this function returns the zero
|
||||
// Point.
|
||||
func ParseLatLng(s string) (Point, error) {
|
||||
var zero Point
|
||||
if s == "" || s == "nowhere" {
|
||||
return zero, nil
|
||||
}
|
||||
|
||||
type State int
|
||||
const (
|
||||
start State = iota + 1
|
||||
latInt
|
||||
latDec
|
||||
lngInt
|
||||
lngDec
|
||||
done
|
||||
)
|
||||
|
||||
var latI int // index of last character + 1
|
||||
state := start
|
||||
for i, last := 0, len(s)-1; i <= last; i++ {
|
||||
c := s[i]
|
||||
switch state {
|
||||
case start:
|
||||
switch c {
|
||||
case '-', '+': // must start with sign: either + or -
|
||||
state = latInt
|
||||
default:
|
||||
return zero, fmt.Errorf("%w: invalid syntax: %q", ErrBadPoint, s)
|
||||
}
|
||||
case latInt:
|
||||
switch c {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
case '.':
|
||||
state = latDec
|
||||
case '-', '+':
|
||||
latI = i
|
||||
state = lngInt
|
||||
default:
|
||||
return zero, fmt.Errorf("%w: invalid syntax: %q", ErrBadPoint, s)
|
||||
}
|
||||
case latDec:
|
||||
switch c {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
case '-', '+':
|
||||
latI = i
|
||||
state = lngInt
|
||||
default:
|
||||
return zero, fmt.Errorf("%w: invalid syntax: %q", ErrBadPoint, s)
|
||||
}
|
||||
case lngInt:
|
||||
switch c {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
case '.':
|
||||
state = lngDec
|
||||
default:
|
||||
return zero, fmt.Errorf("%w: invalid syntax: %q", ErrBadPoint, s)
|
||||
}
|
||||
case lngDec:
|
||||
switch c {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
// no-op
|
||||
default:
|
||||
return zero, fmt.Errorf("%w: invalid syntax: %q", ErrBadPoint, s)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid state: %d", state))
|
||||
}
|
||||
}
|
||||
|
||||
// Did we see both lat and lng?
|
||||
switch state {
|
||||
case lngInt, lngDec:
|
||||
// no-op
|
||||
default:
|
||||
return zero, fmt.Errorf("%w: invalid syntax: %q", ErrBadPoint, s)
|
||||
}
|
||||
|
||||
// Latitude
|
||||
lat, err := strconv.ParseFloat(string(s[0:latI]), 64)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("%w: invalid latitude: %w", ErrBadPoint, err)
|
||||
}
|
||||
|
||||
// Longitude
|
||||
lng, err := strconv.ParseFloat(string(s[latI:]), 64)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("%w: invalid longitude: %w", ErrBadPoint, err)
|
||||
}
|
||||
|
||||
return MakePoint(Degrees(lat), Degrees(lng)), nil
|
||||
}
|
||||
|
||||
// AppendBinary implements [encoding.BinaryAppender]. The output consists of two
|
||||
// float32s in big-endian byte order: latitude and longitude offset by 180°.
|
||||
// If p is not a valid, the output will be an 8-byte zero value.
|
||||
|
@ -56,6 +56,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat geo.Degrees
|
||||
wantLng geo.Degrees
|
||||
wantString string
|
||||
wantLatLng string
|
||||
wantText string
|
||||
}{
|
||||
{
|
||||
@ -65,6 +66,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +0.0,
|
||||
wantLng: +0.0,
|
||||
wantString: "+0° +0°",
|
||||
wantLatLng: "+0+0",
|
||||
wantText: "POINT (0 0)",
|
||||
},
|
||||
{
|
||||
@ -74,6 +76,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +90.0,
|
||||
wantLng: +0.0,
|
||||
wantString: "+90° +0°",
|
||||
wantLatLng: "+90+0",
|
||||
wantText: "POINT (0 90)",
|
||||
},
|
||||
{
|
||||
@ -83,6 +86,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: -90.0,
|
||||
wantLng: +0.0,
|
||||
wantString: "-90° +0°",
|
||||
wantLatLng: "-90+0",
|
||||
wantText: "POINT (0 -90)",
|
||||
},
|
||||
{
|
||||
@ -92,6 +96,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +90.0,
|
||||
wantLng: +0.0,
|
||||
wantString: "+90° +0°",
|
||||
wantLatLng: "+90+0",
|
||||
wantText: "POINT (0 90)",
|
||||
},
|
||||
{
|
||||
@ -101,6 +106,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: -90.0,
|
||||
wantLng: +0.0,
|
||||
wantString: "-90° +0°",
|
||||
wantLatLng: "-90+0",
|
||||
wantText: "POINT (0 -90)",
|
||||
},
|
||||
{
|
||||
@ -110,6 +116,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +89.0,
|
||||
wantLng: +0.0,
|
||||
wantString: "+89° +0°",
|
||||
wantLatLng: "+89+0",
|
||||
wantText: "POINT (0 89)",
|
||||
},
|
||||
{
|
||||
@ -119,6 +126,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +89.0,
|
||||
wantLng: +180.0,
|
||||
wantString: "+89° +180°",
|
||||
wantLatLng: "+89+180",
|
||||
wantText: "POINT (180 89)",
|
||||
},
|
||||
{
|
||||
@ -128,6 +136,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: -89.0,
|
||||
wantLng: +0.0,
|
||||
wantString: "-89° +0°",
|
||||
wantLatLng: "-89+0",
|
||||
wantText: "POINT (0 -89)",
|
||||
},
|
||||
{
|
||||
@ -137,6 +146,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: -89.0,
|
||||
wantLng: +180.0,
|
||||
wantString: "-89° +180°",
|
||||
wantLatLng: "-89+180",
|
||||
wantText: "POINT (180 -89)",
|
||||
},
|
||||
{
|
||||
@ -146,6 +156,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +0.0,
|
||||
wantLng: +180.0,
|
||||
wantString: "+0° +180°",
|
||||
wantLatLng: "+0+180",
|
||||
wantText: "POINT (180 0)",
|
||||
},
|
||||
{
|
||||
@ -155,6 +166,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +0.0,
|
||||
wantLng: +180.0,
|
||||
wantString: "+0° +180°",
|
||||
wantLatLng: "+0+180",
|
||||
wantText: "POINT (180 0)",
|
||||
},
|
||||
{
|
||||
@ -164,6 +176,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +1.0,
|
||||
wantLng: +180.0,
|
||||
wantString: "+1° +180°",
|
||||
wantLatLng: "+1+180",
|
||||
wantText: "POINT (180 1)",
|
||||
},
|
||||
{
|
||||
@ -173,6 +186,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: -1.0,
|
||||
wantLng: +180.0,
|
||||
wantString: "-1° +180°",
|
||||
wantLatLng: "-1+180",
|
||||
wantText: "POINT (180 -1)",
|
||||
},
|
||||
{
|
||||
@ -182,6 +196,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: -1.0,
|
||||
wantLng: +180.0,
|
||||
wantString: "-1° +180°",
|
||||
wantLatLng: "-1+180",
|
||||
wantText: "POINT (180 -1)",
|
||||
},
|
||||
{
|
||||
@ -191,6 +206,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +1.0,
|
||||
wantLng: +180.0,
|
||||
wantString: "+1° +180°",
|
||||
wantLatLng: "+1+180",
|
||||
wantText: "POINT (180 1)",
|
||||
},
|
||||
{
|
||||
@ -200,6 +216,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +0.0,
|
||||
wantLng: +1.0,
|
||||
wantString: "+0° +1°",
|
||||
wantLatLng: "+0+1",
|
||||
wantText: "POINT (1 0)",
|
||||
},
|
||||
{
|
||||
@ -209,6 +226,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +0.0,
|
||||
wantLng: +1.0,
|
||||
wantString: "+0° +1°",
|
||||
wantLatLng: "+0+1",
|
||||
wantText: "POINT (1 0)",
|
||||
},
|
||||
{
|
||||
@ -218,6 +236,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: -1.0,
|
||||
wantLng: +1.0,
|
||||
wantString: "-1° +1°",
|
||||
wantLatLng: "-1+1",
|
||||
wantText: "POINT (1 -1)",
|
||||
},
|
||||
{
|
||||
@ -227,6 +246,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +1.0,
|
||||
wantLng: +1.0,
|
||||
wantString: "+1° +1°",
|
||||
wantLatLng: "+1+1",
|
||||
wantText: "POINT (1 1)",
|
||||
},
|
||||
{
|
||||
@ -236,6 +256,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +1.0,
|
||||
wantLng: +1.0,
|
||||
wantString: "+1° +1°",
|
||||
wantLatLng: "+1+1",
|
||||
wantText: "POINT (1 1)",
|
||||
},
|
||||
{
|
||||
@ -245,6 +266,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: -1.0,
|
||||
wantLng: +1.0,
|
||||
wantString: "-1° +1°",
|
||||
wantLatLng: "-1+1",
|
||||
wantText: "POINT (1 -1)",
|
||||
},
|
||||
{
|
||||
@ -254,6 +276,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +0.0,
|
||||
wantLng: +180.0,
|
||||
wantString: "+0° +180°",
|
||||
wantLatLng: "+0+180",
|
||||
wantText: "POINT (180 0)",
|
||||
},
|
||||
{
|
||||
@ -263,6 +286,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +0.0,
|
||||
wantLng: +180.0,
|
||||
wantString: "+0° +180°",
|
||||
wantLatLng: "+0+180",
|
||||
wantText: "POINT (180 0)",
|
||||
},
|
||||
{
|
||||
@ -272,6 +296,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +0.0,
|
||||
wantLng: +179.0,
|
||||
wantString: "+0° +179°",
|
||||
wantLatLng: "+0+179",
|
||||
wantText: "POINT (179 0)",
|
||||
},
|
||||
{
|
||||
@ -281,6 +306,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +0.0,
|
||||
wantLng: -179.0,
|
||||
wantString: "+0° -179°",
|
||||
wantLatLng: "+0-179",
|
||||
wantText: "POINT (-179 0)",
|
||||
},
|
||||
{
|
||||
@ -290,6 +316,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +0.0,
|
||||
wantLng: -179.0,
|
||||
wantString: "+0° -179°",
|
||||
wantLatLng: "+0-179",
|
||||
wantText: "POINT (-179 0)",
|
||||
},
|
||||
{
|
||||
@ -299,6 +326,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +0.0,
|
||||
wantLng: +179.0,
|
||||
wantString: "+0° +179°",
|
||||
wantLatLng: "+0+179",
|
||||
wantText: "POINT (179 0)",
|
||||
},
|
||||
{
|
||||
@ -308,6 +336,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: +45.508888,
|
||||
wantLng: -73.561668,
|
||||
wantString: "+45.508888° -73.561668°",
|
||||
wantLatLng: "+45.508888-73.561668",
|
||||
wantText: "POINT (-73.561668 45.508888)",
|
||||
},
|
||||
{
|
||||
@ -317,6 +346,7 @@ func TestPoint(t *testing.T) {
|
||||
wantLat: 57.550480044655636,
|
||||
wantLng: -98.41680517868062,
|
||||
wantString: "+57.550480044655636° -98.41680517868062°",
|
||||
wantLatLng: "+57.550480044655636-98.41680517868062",
|
||||
wantText: "POINT (-98.41680517868062 57.550480044655636)",
|
||||
},
|
||||
} {
|
||||
@ -338,6 +368,19 @@ func TestPoint(t *testing.T) {
|
||||
t.Errorf("String: got %q, wantString %q", got, tt.wantString)
|
||||
}
|
||||
|
||||
ll := p.FormatLatLng()
|
||||
if ll != tt.wantLatLng {
|
||||
t.Errorf("FormatLatLng: got %q, wantLatLng %q", ll, tt.wantLatLng)
|
||||
}
|
||||
|
||||
q, err := geo.ParseLatLng(ll)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseLatLng: err %q, expected nil", err)
|
||||
}
|
||||
if q != p {
|
||||
t.Errorf("ParseLatLng: got %v, want %v", q, p)
|
||||
}
|
||||
|
||||
txt, err := p.MarshalText()
|
||||
if err != nil {
|
||||
t.Errorf("Text: err %q, expected nil", err)
|
||||
@ -350,7 +393,7 @@ func TestPoint(t *testing.T) {
|
||||
t.Fatalf("MarshalBinary: err %q, expected nil", err)
|
||||
}
|
||||
|
||||
var q geo.Point
|
||||
q = geo.Point{}
|
||||
if err := q.UnmarshalBinary(b); err != nil {
|
||||
t.Fatalf("UnmarshalBinary: err %q, expected nil", err)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user