value_test.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package opt
  4. import (
  5. "encoding/json"
  6. "reflect"
  7. "testing"
  8. jsonv2 "github.com/go-json-experiment/json"
  9. "tailscale.com/types/bools"
  10. "tailscale.com/util/must"
  11. )
  12. var (
  13. _ jsonv2.MarshalerTo = (*Value[bool])(nil)
  14. _ jsonv2.UnmarshalerFrom = (*Value[bool])(nil)
  15. )
  16. type testStruct struct {
  17. Int int `json:",omitempty,omitzero"`
  18. Str string `json:",omitempty"`
  19. }
  20. func TestValue(t *testing.T) {
  21. tests := []struct {
  22. name string
  23. in any
  24. jsonv2 bool
  25. want string // JSON
  26. wantBack any
  27. }{
  28. {
  29. name: "null_for_unset",
  30. in: struct {
  31. True Value[bool]
  32. False Value[bool]
  33. Unset Value[bool]
  34. ExplicitUnset Value[bool]
  35. }{
  36. True: ValueOf(true),
  37. False: ValueOf(false),
  38. ExplicitUnset: Value[bool]{},
  39. },
  40. want: `{"True":true,"False":false,"Unset":null,"ExplicitUnset":null}`,
  41. wantBack: struct {
  42. True Value[bool]
  43. False Value[bool]
  44. Unset Value[bool]
  45. ExplicitUnset Value[bool]
  46. }{
  47. True: ValueOf(true),
  48. False: ValueOf(false),
  49. Unset: Value[bool]{},
  50. ExplicitUnset: Value[bool]{},
  51. },
  52. },
  53. {
  54. name: "null_for_unset_jsonv2",
  55. in: struct {
  56. True Value[bool]
  57. False Value[bool]
  58. Unset Value[bool]
  59. ExplicitUnset Value[bool]
  60. }{
  61. True: ValueOf(true),
  62. False: ValueOf(false),
  63. ExplicitUnset: Value[bool]{},
  64. },
  65. jsonv2: true,
  66. want: `{"True":true,"False":false,"Unset":null,"ExplicitUnset":null}`,
  67. wantBack: struct {
  68. True Value[bool]
  69. False Value[bool]
  70. Unset Value[bool]
  71. ExplicitUnset Value[bool]
  72. }{
  73. True: ValueOf(true),
  74. False: ValueOf(false),
  75. Unset: Value[bool]{},
  76. ExplicitUnset: Value[bool]{},
  77. },
  78. },
  79. {
  80. name: "null_for_unset_omitzero",
  81. in: struct {
  82. True Value[bool] `json:",omitzero"`
  83. False Value[bool] `json:",omitzero"`
  84. Unset Value[bool] `json:",omitzero"`
  85. ExplicitUnset Value[bool] `json:",omitzero"`
  86. }{
  87. True: ValueOf(true),
  88. False: ValueOf(false),
  89. ExplicitUnset: Value[bool]{},
  90. },
  91. want: bools.IfElse(
  92. // Detect whether v1 "encoding/json" supports `omitzero` or not.
  93. // TODO(Go1.24): Remove this after `omitzero` is supported.
  94. string(must.Get(json.Marshal(struct {
  95. X int `json:",omitzero"`
  96. }{}))) == `{}`,
  97. `{"True":true,"False":false}`, // omitzero supported
  98. `{"True":true,"False":false,"Unset":null,"ExplicitUnset":null}`), // omitzero not supported
  99. wantBack: struct {
  100. True Value[bool] `json:",omitzero"`
  101. False Value[bool] `json:",omitzero"`
  102. Unset Value[bool] `json:",omitzero"`
  103. ExplicitUnset Value[bool] `json:",omitzero"`
  104. }{
  105. True: ValueOf(true),
  106. False: ValueOf(false),
  107. Unset: Value[bool]{},
  108. ExplicitUnset: Value[bool]{},
  109. },
  110. },
  111. {
  112. name: "null_for_unset_omitzero_jsonv2",
  113. in: struct {
  114. True Value[bool] `json:",omitzero"`
  115. False Value[bool] `json:",omitzero"`
  116. Unset Value[bool] `json:",omitzero"`
  117. ExplicitUnset Value[bool] `json:",omitzero"`
  118. }{
  119. True: ValueOf(true),
  120. False: ValueOf(false),
  121. ExplicitUnset: Value[bool]{},
  122. },
  123. jsonv2: true,
  124. want: `{"True":true,"False":false}`,
  125. wantBack: struct {
  126. True Value[bool] `json:",omitzero"`
  127. False Value[bool] `json:",omitzero"`
  128. Unset Value[bool] `json:",omitzero"`
  129. ExplicitUnset Value[bool] `json:",omitzero"`
  130. }{
  131. True: ValueOf(true),
  132. False: ValueOf(false),
  133. Unset: Value[bool]{},
  134. ExplicitUnset: Value[bool]{},
  135. },
  136. },
  137. {
  138. name: "string",
  139. in: struct {
  140. EmptyString Value[string]
  141. NonEmpty Value[string]
  142. Unset Value[string]
  143. }{
  144. EmptyString: ValueOf(""),
  145. NonEmpty: ValueOf("value"),
  146. Unset: Value[string]{},
  147. },
  148. want: `{"EmptyString":"","NonEmpty":"value","Unset":null}`,
  149. wantBack: struct {
  150. EmptyString Value[string]
  151. NonEmpty Value[string]
  152. Unset Value[string]
  153. }{ValueOf(""), ValueOf("value"), Value[string]{}},
  154. },
  155. {
  156. name: "integer",
  157. in: struct {
  158. Zero Value[int]
  159. NonZero Value[int]
  160. Unset Value[int]
  161. }{
  162. Zero: ValueOf(0),
  163. NonZero: ValueOf(42),
  164. Unset: Value[int]{},
  165. },
  166. want: `{"Zero":0,"NonZero":42,"Unset":null}`,
  167. wantBack: struct {
  168. Zero Value[int]
  169. NonZero Value[int]
  170. Unset Value[int]
  171. }{ValueOf(0), ValueOf(42), Value[int]{}},
  172. },
  173. {
  174. name: "struct",
  175. in: struct {
  176. Zero Value[testStruct]
  177. NonZero Value[testStruct]
  178. Unset Value[testStruct]
  179. }{
  180. Zero: ValueOf(testStruct{}),
  181. NonZero: ValueOf(testStruct{Int: 42, Str: "String"}),
  182. Unset: Value[testStruct]{},
  183. },
  184. want: `{"Zero":{},"NonZero":{"Int":42,"Str":"String"},"Unset":null}`,
  185. wantBack: struct {
  186. Zero Value[testStruct]
  187. NonZero Value[testStruct]
  188. Unset Value[testStruct]
  189. }{ValueOf(testStruct{}), ValueOf(testStruct{Int: 42, Str: "String"}), Value[testStruct]{}},
  190. },
  191. {
  192. name: "struct_ptr",
  193. in: struct {
  194. Zero Value[*testStruct]
  195. NonZero Value[*testStruct]
  196. Unset Value[*testStruct]
  197. }{
  198. Zero: ValueOf(&testStruct{}),
  199. NonZero: ValueOf(&testStruct{Int: 42, Str: "String"}),
  200. Unset: Value[*testStruct]{},
  201. },
  202. want: `{"Zero":{},"NonZero":{"Int":42,"Str":"String"},"Unset":null}`,
  203. wantBack: struct {
  204. Zero Value[*testStruct]
  205. NonZero Value[*testStruct]
  206. Unset Value[*testStruct]
  207. }{ValueOf(&testStruct{}), ValueOf(&testStruct{Int: 42, Str: "String"}), Value[*testStruct]{}},
  208. },
  209. {
  210. name: "nil-slice-and-map",
  211. in: struct {
  212. Slice Value[[]int]
  213. Map Value[map[string]int]
  214. }{
  215. Slice: ValueOf[[]int](nil), // marshalled as []
  216. Map: ValueOf[map[string]int](nil), // marshalled as {}
  217. },
  218. want: `{"Slice":[],"Map":{}}`,
  219. wantBack: struct {
  220. Slice Value[[]int]
  221. Map Value[map[string]int]
  222. }{ValueOf([]int{}), ValueOf(map[string]int{})},
  223. },
  224. }
  225. for _, tt := range tests {
  226. t.Run(tt.name, func(t *testing.T) {
  227. var j []byte
  228. var err error
  229. if tt.jsonv2 {
  230. j, err = jsonv2.Marshal(tt.in)
  231. } else {
  232. j, err = json.Marshal(tt.in)
  233. }
  234. if err != nil {
  235. t.Fatal(err)
  236. }
  237. if string(j) != tt.want {
  238. t.Errorf("wrong JSON:\n got: %s\nwant: %s\n", j, tt.want)
  239. }
  240. wantBack := tt.in
  241. if tt.wantBack != nil {
  242. wantBack = tt.wantBack
  243. }
  244. // And back again:
  245. newVal := reflect.New(reflect.TypeOf(tt.in))
  246. out := newVal.Interface()
  247. if tt.jsonv2 {
  248. err = jsonv2.Unmarshal(j, out)
  249. } else {
  250. err = json.Unmarshal(j, out)
  251. }
  252. if err != nil {
  253. t.Fatalf("Unmarshal %#q: %v", j, err)
  254. }
  255. got := newVal.Elem().Interface()
  256. if !reflect.DeepEqual(got, wantBack) {
  257. t.Errorf("value mismatch\n got: %+v\nwant: %+v\n", got, wantBack)
  258. }
  259. })
  260. }
  261. }
  262. func TestValueEqual(t *testing.T) {
  263. tests := []struct {
  264. o Value[bool]
  265. v Value[bool]
  266. want bool
  267. }{
  268. {ValueOf(true), ValueOf(true), true},
  269. {ValueOf(true), ValueOf(false), false},
  270. {ValueOf(true), Value[bool]{}, false},
  271. {ValueOf(false), ValueOf(false), true},
  272. {ValueOf(false), ValueOf(true), false},
  273. {ValueOf(false), Value[bool]{}, false},
  274. {Value[bool]{}, Value[bool]{}, true},
  275. {Value[bool]{}, ValueOf(true), false},
  276. {Value[bool]{}, ValueOf(false), false},
  277. }
  278. for _, tt := range tests {
  279. if got := tt.o.Equal(tt.v); got != tt.want {
  280. t.Errorf("(%v).Equals(%v) = %v; want %v", tt.o, tt.v, got, tt.want)
  281. }
  282. }
  283. }
  284. func TestIncomparableValueEqual(t *testing.T) {
  285. tests := []struct {
  286. o Value[[]bool]
  287. v Value[[]bool]
  288. want bool
  289. }{
  290. {ValueOf([]bool{}), ValueOf([]bool{}), false},
  291. {ValueOf([]bool{true}), ValueOf([]bool{true}), false},
  292. {Value[[]bool]{}, ValueOf([]bool{}), false},
  293. {ValueOf([]bool{}), Value[[]bool]{}, false},
  294. {Value[[]bool]{}, Value[[]bool]{}, true},
  295. }
  296. for _, tt := range tests {
  297. if got := tt.o.Equal(tt.v); got != tt.want {
  298. t.Errorf("(%v).Equals(%v) = %v; want %v", tt.o, tt.v, got, tt.want)
  299. }
  300. }
  301. }